# CVE-2024-4367 (PDF.js XSS) - https://chatgpt.com/share/696a0778-f45c-8003-95bf-d7b06ecb099a - https://codeanlabs.com/2024/05/cve-2024-4367-arbitrary-js-execution-in-pdf-js/ ![image](https://hackmd.io/_uploads/SJT9xUvHWg.png) ```javascript getPathGenerator(t, e) { if (void 0 !== this.compiledGlyphs[e]) return this.compiledGlyphs[e]; let i; try { i = t.get(this.loadedName + "_path_" + e); } catch (t) { if (!this.ignoreErrors) throw t; (0, s.warn)(`getPathGenerator - ignoring character: "${t}".`); return (this.compiledGlyphs[e] = function (t, e) { }); } if (this.isEvalSupported && s.FeatureTest.isEvalSupported) { const t = []; for (const e of i) { const i = void 0 !== e.args ? e.args.join(",") : ""; t.push("c.", e.cmd, "(", i, ");\n"); } return (this.compiledGlyphs[e] = new Function( "c", "size", t.join("") )); } return (this.compiledGlyphs[e] = function (t, e) { for (const s of i) { "scale" === s.cmd && (s.args = [e, -e]); t[s.cmd].apply(t, s.args); } }); } ``` ## Giới thiệu PDF.js được phát hiện bởi Codean Labs. PDF.js là một trình xem PDF dựa trên JavaScript do Mozilla duy trì Điều này ảnh hưởng đến tất cả người dùng Firefox (<126) vì Firefox sử dụng PDF.js để hiển thị các tệp PDF, nhưng cũng ảnh hưởng nghiêm trọng đến nhiều ứng dụng web và ứng dụng dựa trên Electron (gián tiếp) sử dụng PDF.js cho chức năng xem trước ## Chức năng Có hai trường hợp sử dụng phổ biến cho PDF.js: - Thứ nhất, nó là trình xem PDF tích hợp sẵn của Firefox. Nếu bạn sử dụng Firefox và đã từng tải xuống hoặc duyệt đến một tệp PDF, bạn sẽ thấy nó hoạt động - Thứ hai, nó được đóng gói thành một mô-đun Node có tên là ` pdfjs-distPDF.js`, với khoảng 2,7 triệu lượt tải xuống hàng tuần theo NPM. Ở dạng này, các trang web có thể sử dụng nó để cung cấp chức năng xem trước PDF nhúng. Điều này được sử dụng bởi mọi thứ, từ các nền tảng lưu trữ Git đến các ứng dụng ghi chú. Ứng dụng mà bạn đang nghĩ đến bây giờ rất có thể đang sử dụng PDF.js. Định dạng PDF nổi tiếng là phức tạp. Với sự hỗ trợ cho nhiều loại phương tiện khác nhau, khả năng hiển thị phông chữ phức tạp và thậm chí cả các đoạn mã script cơ bản, Với lượng logic phân tích cú pháp lớn như vậy, chắc chắn sẽ có một số lỗi, và PDF.js cũng không ngoại lệ. Tuy nhiên, điều làm cho nó độc đáo là nó được viết bằng JavaScript thay vì C hoặc C++. Điều này có nghĩa là không có nguy cơ xảy ra sự cố lỗi bộ nhớ, nhưng như chúng ta sẽ thấy, nó cũng đi kèm với những rủi ro riêng ## Hiển thị ký tự lỗi này không liên quan đến chức năng lập trình (JavaScript!) của định dạng PDF. Thay vào đó, nó là một sơ suất trong một phần cụ thể của mã hiển thị phông chữ. Phông chữ trong PDF có thể có nhiều định dạng khác nhau, một số định dạng ít phổ biến hơn những định dạng khác Đối với các định dạng hiện đại như TrueType, PDF.js chủ yếu dựa vào trình kết xuất phông chữ của trình duyệt Trong các trường hợp khác, nó phải tự động chuyển đổi mô tả ký tự (glyph) thành các đường cong trên trang. Để tối ưu hóa hiệu suất, một hàm path generator được biên dịch trước cho mỗi ký tự glyph. Nếu được hỗ trợ, điều này được thực hiện bằng cách tạo một JavaScript Function đối tượng với phần thân ( jsBuf) chứa các hướng dẫn tạo nên đường dẫn: ![image](https://hackmd.io/_uploads/S1VkawPHZx.png) dùng để biên dịch (compile) danh sách lệnh vẽ glyph thành JavaScript runtime nhằm tăng tốc độ render. Mục đích tổng quát - Chuyển các lệnh vẽ (cmds) thành một hàm JavaScript thực thi trực tiếp, thay vì phải interpret từng lệnh mỗi lần render. ```javascript // If we can, compile cmds into JS for MAXIMUM SPEED... if (this.isEvalSupported && FeatureTest.isEvalSupported) { const jsBuf = []; for (const current of cmds) { const args = current.args !== undefined ? current.args.join(",") : ""; jsBuf.push("c.", current.cmd, "(", args, ");\n"); } // eslint-disable-next-line no-new-func console.log(jsBuf.join("")); return (this.compiledGlyphs[character] = new Function( "c", "size", jsBuf.join("") )); } ``` Từ góc độ kẻ tấn công, điều này thực sự thú vị: nếu bằng cách nào đó chúng ta có thể kiểm soát cmdsviệc đưa các ký tự này vào phần Functionthân và chèn mã của riêng mình, mã đó sẽ được thực thi ngay khi ký tự đó được hiển thị. Điều kiện kích hoạt ``` if (this.isEvalSupported && FeatureTest.isEvalSupported) { ``` Chỉ chạy khi: Trình duyệt cho phép eval / new Function pdf.js xác định môi trường an toàn để eval Nếu không thỏa điều kiện → pdf.js sẽ dùng interpreter chậm hơn. 3. cmds là gì? cmds là danh sách lệnh vẽ glyph, ví dụ: ``` [ { cmd: "moveTo", args: [0, 0] }, { cmd: "lineTo", args: [10, 10] }, { cmd: "fill" } ] ``` Mỗi lệnh tương ứng với method của CanvasRenderingContext2D (c). Xây dựng JavaScript source code ```javascript const jsBuf = []; for (const current of cmds) { const args = current.args !== undefined ? current.args.join(",") : ""; jsBuf.push("c.", current.cmd, "(", args, ");\n"); } ``` Ví dụ output của jsBuf.join(""): ``` c.moveTo(0,0); c.lineTo(10,10); c.fill(); ``` 👉 Tạo source code JavaScript dưới dạng string Điểm nguy hiểm nhất: new Function ```javascript return new Function("c", "size", jsBuf.join("")); ``` Tương đương: ```javascript function anonymous(c, size) { c.moveTo(0,0); c.lineTo(10,10); c.fill(); } ``` Hàm này: - Được cache theo character - Được gọi lại nhiều lần để vẽ glyph | Cách | Mô tả | Tốc độ | | ----------- | ---------------------------- | ------------- | | Interpreter | Loop từng cmd mỗi lần render | Chậm | | Compiled JS | Trình duyệt JIT (Just-In-Time compilation) optimize | **Rất nhanh** | JIT (Just-In-Time compilation) là kỹ thuật biên dịch mã ngay tại thời điểm chạy, thay vì biên dịch toàn bộ trước (AOT) hoặc thông dịch từng dòng. 1. Hiểu nhanh trong 1 câu JIT = chuyển code “đang chạy” thành mã máy tối ưu ngay lúc cần để chạy nhanh hơn. | Mô hình | Mô tả | Ví dụ | | ----------------------- | ------------------------ | ---------------------- | | **AOT (Ahead-Of-Time)** | Biên dịch trước khi chạy | C/C++, Go | | **Interpreter** | Đọc – chạy từng dòng | Python (thuần), PHP | | **JIT** | Chạy trước, tối ưu sau | Java, JavaScript, .NET | ``` new Function("c", "size", jsCode) ``` pdf.js tạo code JS động, sau đó: Trình duyệt JIT compile hàm này sang native code Lần gọi sau render glyph cực nhanh 👉 pdf.js tự tạo mini-JIT ở tầng JS JIT là kỹ thuật biên dịch code ngay lúc chạy để tăng hiệu năng, nhưng nếu kết hợp với dữ liệu không tin cậy sẽ tạo ra bề mặt tấn công rất lớn. 7. Vấn đề bảo mật (liên quan CVE-2024-4367) Đây chính là bề mặt tấn công (attack surface) nguy hiểm: 🚨 Nguyên nhân gốc rễ current.cmd và current.args xuất phát từ PDF PDF là input do attacker kiểm soát Ghép chuỗi → new Function(...) 👉 Nếu sanitize không tuyệt đối, attacker có thể: Inject JavaScript Thoát khỏi context vẽ Dẫn tới XSS trong PDF viewer 8. Ví dụ ý tưởng khai thác (logic, không PoC) Nếu attacker kiểm soát được: ``` current.cmd = "fill); alert(1); //" ``` Code sinh ra: ``` c.fill); alert(1); //(args); ``` → JavaScript injection (pdf.js đã có nhiều lớp filter, nhưng CVE xuất phát từ thiếu kiểm soát một edge case) Vì sao Mozilla phải vá? new Function = eval Eval + user-controlled input = high risk PDF.js chạy trong origin của Firefox new Function() biên dịch và thực thi JavaScript từ string tại runtime, giống eval. | Tiêu chí | eval | new Function | | ----------------------------- | -------------- | ---------------- | | Có thực thi string | ✅ | ✅ | | Tạo function mới | ❌ | ✅ | | Scope | Scope hiện tại | **Global scope** | | Bị CSP chặn bởi `unsafe-eval` | ✅ | ✅ | | Nguy cơ injection | Cao | Cao | User-controlled data → string → JavaScript execution ``` PDF input → cmds[] → current.cmd / current.args → jsBuf.join("") → new Function(...) → JS execution ``` Chúng ta hãy xem danh sách các lệnh này được tạo ra như thế nào. Theo logic trở lại lớp CompiledFont chúng ta tìm thấy phương thức compileGlyph(...). Phương thức này khởi tạo mảng cmds với một vài lệnh( save, transform, scalevà restore), và chuyển giao cho một phương thức compileGlyphImpl(...) để điền vào các lệnh hiển thị thực tế: ```javascript compileGlyph(code, glyphId) { if (!code || code.length === 0 || code[0] === 14) { return NOOP; } let fontMatrix = this.fontMatrix; ... const cmds = [ { cmd: "save" }, { cmd: "transform", args: fontMatrix.slice() }, { cmd: "scale", args: ["size", "-size"] }, ]; this.compileGlyphImpl(code, cmds, glyphId); cmds.push({ cmd: "restore" }); return cmds; } ``` Đoạn này tạo ra danh sách lệnh vẽ (command list – cmds) cho một glyph trước khi các lệnh này được render hoặc JIT-compile. . Mục đích tổng quát Chuyển dữ liệu glyph trong font PDF thành một chuỗi lệnh vẽ canvas, sau đó: - hoặc interpret - hoặc compile thành JavaScript (đoạn new Function đã phân tích) 2. Điều kiện thoát sớm (early return) ```javascript if (!code || code.length === 0 || code[0] === 14) { return NOOP; } ``` !code hoặc code.length === 0 → glyph rỗng, không có gì để vẽ code[0] === 14 → EndChar trong Type 1 / CFF font 👉 Trả về NOOP (không vẽ gì) fontMatrix là gì? let fontMatrix = this.fontMatrix; Ma trận biến đổi từ font space → user space Do PDF định nghĩa Dùng để scale / rotate / mirror glyph Khởi tạo danh sách lệnh cmds ```javascript const cmds = [ { cmd: "save" }, { cmd: "transform", args: fontMatrix.slice() }, { cmd: "scale", args: ["size", "-size"] }, ]; ``` Các lệnh này làm gì? 1️⃣ save ``` c.save(); ``` Lưu trạng thái canvas hiện tại 2️⃣ transform ``` c.transform(a, b, c, d, e, f); ``` Áp dụng fontMatrix Định vị glyph đúng vị trí, đúng tỷ lệ 3️⃣ scale ``` c.scale(size, -size); ``` Scale theo kích thước font -size để lật trục Y (PDF vs Canvas khác hệ tọa độ) 📌 Ba lệnh này là khung chung cho mọi glyph compileGlyphImpl(...) – phần quan trọng nhất ``` this.compileGlyphImpl(code, cmds, glyphId); ``` 👉 Hàm này: Parse glyph bytecode (code) Chuyển từng toán tử font (moveto, lineto, curveto…) Thành các lệnh: ``` { cmd: "moveTo", args: [...] } { cmd: "lineTo", args: [...] } { cmd: "bezierCurveTo", args: [...] } ``` 📌 Đây là nơi: Dữ liệu đến trực tiếp từ PDF Bề mặt tấn công bắt đầu hình thành 6. Kết thúc glyph ``` cmds.push({ cmd: "restore" }); ``` Tương đương: ``` c.restore(); ``` Trả canvas về trạng thái ban đầu Tránh ảnh hưởng glyph tiếp theo 7. Giá trị trả về ``` return cmds; ``` ``` [ { cmd: "save" }, { cmd: "transform", args: [...] }, { cmd: "scale", args: ["size", "-size"] }, { cmd: "moveTo", args: [0, 0] }, { cmd: "lineTo", args: [10, 20] }, { cmd: "closePath" }, { cmd: "fill" }, { cmd: "restore" } ] ``` Danh sách này sau đó: - hoặc được loop thực thi - hoặc được compile thành JS bằng new Function ``` PDF font glyph → compileGlyphImpl → cmds[] → string concat → new Function(...) → JS execution ``` Đoạn code compileGlyph(...) chịu trách nhiệm khởi tạo và xây dựng danh sách lệnh vẽ glyph từ dữ liệu font trong PDF. Danh sách này bao gồm các lệnh canvas chung (save, transform, scale, restore) và các lệnh vẽ cụ thể do compileGlyphImpl(...) sinh ra, trước khi được thực thi hoặc biên dịch thành JavaScript nhằm tối ưu hiệu năng hiển thị. Nếu chúng ta thêm mã vào PDF.js để ghi lại Functioncác đối tượng được tạo ra, chúng ta sẽ thấy rằng mã được tạo ra thực sự chứa các lệnh đó: Nếu chúng ta thêm mã vào PDF.js để ghi lại Functioncác đối tượng được tạo ra, chúng ta sẽ thấy rằng mã được tạo ra thực sự chứa các lệnh đó: ``` c.save(); c.transform(0.001,0,0,0.001,0,0); c.scale(size,-size); c.moveTo(0,0); c.restore(); ``` 2. Vì sao các lệnh vẽ khác “có vẻ an toàn”? Các lệnh như: moveTo lineTo quadraticCurveTo bezierCurveTo Đều có đặc điểm: Đối số là số học Được parse từ bytecode font Không có chỗ chèn string tùy ý ➡️ Chúng chỉ ảnh hưởng hình học, không ảnh hưởng cú pháp JS. Đến đây, chúng ta có thể kiểm tra mã phân tích cú pháp phông chữ và các lệnh cũng như đối số khác nhau có thể được tạo ra bởi các ký tự, chẳng hạn như quadraticCurveTovà bezierCurveTo, nhưng tất cả điều này dường như khá vô hại và không có khả năng kiểm soát bất cứ thứ gì ngoài các con số. Tuy nhiên, điều thú vị hơn nhiều lại là transformlệnh mà chúng ta đã thấy ở trên: 3. Điểm bất thường: transform ``` { cmd: "transform", args: fontMatrix.slice() } ``` Điều quan trọng ở đây: fontMatrix được copy nguyên mảng Sau đó: ``` args.join(",") ``` Và được nhét trực tiếp vào body của Function 👉 Không có: Ép kiểu Validate Quote ("...") 4. Giả định nguy hiểm của code Code ngầm giả định: fontMatrix luôn là mảng số Ví dụ mong đợi: [0.001, 0, 0, 0.001, 0, 0] Sinh code: c.transform(0.001,0,0,0.001,0,0); 👉 Hợp lệ, an toàn. 5. Nhưng nếu fontMatrix chứa string thì sao? Giả sử attacker kiểm soát được: ``` fontMatrix = [ "0.001", "0", "0", "0.001", "0); alert(1); //" ] ``` Khi .join(","): ``` 0.001,0,0,0.001,0); alert(1); // ``` JS được tạo: ``` c.transform(0.001,0,0,0.001,0); alert(1); //); ``` 👉 JavaScript injection hoàn chỉnh ## Nhập FontMatrix Giá trị fontMatrixmặc định là `[0.001, 0, 0, 0.001, 0, 0]` Nhưng trong thực tế: Font có thể tự định nghĩa FontMatrix Giá trị này nằm trong metadata của font nhúng trong PDF Mỗi định dạng font (Type1, CFF, TrueType, …) có cách parse khác nhau Cách thực hiện chính xác điều này khác nhau tùy thuộc vào định dạng phông chữ 2. Ví dụ với Type1 font Đoạn code parser: ```javascript extractFontHeader(properties) { let token; while ((token = this.getToken()) !== null) { if (token !== "/") { continue; } token = this.getToken(); switch (token) { case "FontMatrix": const matrix = this.readNumberArray(); properties.fontMatrix = matrix; break; ... } ... } ... } ``` Điều quan trọng ở đây: Parser: Tìm keyword /FontMatrix Gọi readNumberArray() Chỉ đọc mảng số 👉 Không đọc string 👉 Không eval PostScript 👉 Không cho phép biểu thức tùy ý 3. Vì sao phần này “không thú vị” đối với exploit? Mặc dù: Type1 font về lý thuyết có thể chứa PostScript tùy ý Nhưng: Không PDF reader nào thực thi PostScript đầy đủ PDF.js chỉ: Parse các key quen thuộc Với kiểu dữ liệu cố định 📌 Ở đây: FontMatrix → number array only 4. CFF parser cũng tương tự Tác giả cho biết: Parser cho CFF / CFF2 / OpenType-CFF Cũng: Đọc FontMatrix Ép về số 👉 Không có dấu hiệu: Chuỗi Object Hay code injection trực tiếp 5. Kết luận tạm thời (và rất quan trọng) Tại thời điểm này của phân tích, có vẻ như attacker chỉ kiểm soát được các số trong fontMatrix, không thể chèn string. Điều này dẫn đến nhận định: Giả định của pdf.js (“fontMatrix là mảng số”) → có vẻ đúng → chưa thấy đường khai thác rõ ràng 6. Nhưng tại sao tác giả vẫn tiếp tục phân tích? Đây là điểm tinh tế: Parser dự định đọc số Nhưng câu hỏi thực sự là: Liệu mọi con đường dữ liệu đều đảm bảo fontMatrix chỉ chứa Number object chuẩn của JS? 💥 Rất nhiều CVE sinh ra ở chỗ: Kiểu dữ liệu bị phá vỡ gián tiếp Không phải ở parser “chính thống” 7. Vai trò của đoạn này trong toàn bộ exploit chain Đoạn này có tác dụng: ❌ Loại trừ hướng khai thác “ngây thơ” (chèn string trực tiếp trong Type1/CFF) ✅ Chuẩn bị nền để: Tìm con đường khác Ví dụ: Object thay vì primitive Getter / toString Prototype pollution Type confusion 👉 Đây là bước “loại trừ giả thuyết” trước khi tìm bug thật ## Bypass đầu váo ép kiểu số 1. Phát hiện quan trọng: fontMatrix không chỉ đến từ font file Trước đó đã thấy: - fontMatrix có thể đến từ metadata bên trong font - Nhưng parser font chỉ đọc number array → tưởng như an toàn 👉 Tuy nhiên, PDF.js còn một nguồn khác cho fontMatrix. 2. Nguồn thứ hai: PDF Font dictionary Trong PartialEvaluator.translateFont(...): fontMatrix: dict.getArray("FontMatrix") || FONT_IDENTITY_MATRIX, 📌 dict ở đây là: Font dictionary trong PDF Không phải font binary Do PDF định nghĩa trực tiếp ➡️ Điều này có nghĩa: PDF có thể tự định nghĩa FontMatrix, độc lập với font file 3. Cấu trúc font trong PDF PDF định nghĩa font qua nhiều object: Font object FontDescriptor FontFile Ví dụ: ```pdf 1 0 obj << /Type /Font /Subtype /Type1 /FontDescriptor 2 0 R /BaseFont /FooBarFont >> endobj 2 0 obj << /Type /FontDescriptor /FontName /FooBarFont /FontFile 3 0 R /ItalicAngle 0 /Flags 4 >> endobj 3 0 obj << /Length 100 >> ... (actual binary font data) ... endobj ``` 👉 dict chính là object 1 0 obj 4. Ta có thể thêm FontMatrix ở đâu? Hoàn toàn hợp lệ theo PDF spec: ``` /FontMatrix [1 2 3 4 5 6] ``` → PDF.js sẽ đọc nó bằng: dict.getArray("FontMatrix") Biến dictđược tham chiếu trong đoạn mã trên đề cập đến Fontđối tượng. Do đó, chúng ta có thể định nghĩa một FontMatrixmảng tùy chỉnh như sau: ``` 1 0 obj << /Type /Font /Subtype /Type1 /FontDescriptor 2 0 R /BaseFont /FooBarFont /FontMatrix [1 2 3 4 5 6] % <----- >> endobj ``` 5. Vấn đề ghi đè (và cách vượt qua) Ban đầu: Font file ghi đè FontMatrix Nên giá trị từ PDF không có hiệu lực 💡 Nhưng: Type1 font không định nghĩa FontMatrix nội bộ Khi đó: PDF-defined FontMatrix trở thành authoritative 👉 Đây là chi tiết cực kỳ quan trọng. Khi cố gắng thực hiện điều này, ban đầu có vẻ như nó không hoạt động, vì các transformthao tác trong Functioncác phần thân được tạo ra vẫn sử dụng ma trận mặc định. Tuy nhiên, điều này xảy ra vì chính tệp phông chữ đang ghi đè lên giá trị. May mắn thay, khi sử dụng phông chữ Type1 không có FontMatrixđịnh nghĩa nội bộ, giá trị do PDF chỉ định sẽ được coi là chính xác vì fontMatrixgiá trị đó không bị ghi đè. Giờ đây, khi chúng ta có thể điều khiển mảng này từ một đối tượng PDF, chúng ta có được sự linh hoạt cần thiết, vì PDF hỗ trợ nhiều hơn chỉ các kiểu dữ liệu số cơ bản. Hãy thử chèn một giá trị kiểu chuỗi thay vì một số (trong PDF, chuỗi được phân tách bằng dấu ngoặc đơn): ``` /FontMatrix [1 2 3 4 5 (foobar)] ``` 6. PDF array ≠ JavaScript number array Đây là điểm chết người. PDF cho phép: Number Boolean Name String Object Reference Ví dụ hợp lệ trong PDF: ``` /FontMatrix [1 2 3 4 5 (foobar)] ``` 📌 (foobar) là string trong PDF 7. Chuỗi này đi thẳng vào new Function Quy trình: ``` PDF FontMatrix → dict.getArray() → JS array (có string) → args.join(",") → new Function(...) ``` Kết quả JS sinh ra: ``` c.transform(1,2,3,4,5,foobar); ``` ❌ Không có dấu "foobar" ❌ Không escape ❌ Không type-check 👉 JavaScript syntax injection 8. Vì sao đây là game over? foobar được parse như identifier Attacker có thể thay bằng: ``` (FontMatrix [1 2 3 4 5 (alert(1))]) ``` JS sinh ra: ``` c.transform(1,2,3,4,5,alert(1)); ``` ➡️ JavaScript execution trong PDF.js ➡️ XSS trong Firefox 9. Root Cause (chuẩn CVE) PDF.js tin tưởng rằng FontMatrix là mảng số, nhưng lại cho phép giá trị này được lấy trực tiếp từ PDF Font dictionary mà không thực hiện kiểm tra kiểu dữ liệu. Khi kết hợp với cơ chế sinh mã JavaScript động bằng new Function, một chuỗi do attacker kiểm soát có thể được chèn nguyên văn vào thân hàm, dẫn đến thực thi mã JavaScript tùy ý. 10. Chuỗi khai thác hoàn chỉnh (rất quan trọng) ``` PDF object (FontMatrix string) → dict.getArray() → cmds[] → jsBuf.join() → new Function() → JS execution (XSS) ``` 11. Kết luận ngắn gọn Bằng cách định nghĩa FontMatrix trực tiếp trong Font dictionary của PDF và lợi dụng việc PDF.js không kiểm tra kiểu phần tử của mảng này, attacker có thể chèn chuỗi tùy ý vào mã JavaScript được sinh động, từ đó đạt được thực thi mã trong ngữ cảnh PDF viewer. Đến đây, lỗ hổng đã được chứng minh hoàn chỉnh. ## Sự khai thác và tác động Việc chèn mã JavaScript tùy ý giờ đây chỉ là vấn đề điều chỉnh cú pháp cho đúng. Dưới đây là một ví dụ kinh điển kích hoạt cảnh báo, bằng cách trước tiên đóng c.transform(...)hàm và sử dụng dấu ngoặc đơn cuối cùng: ``` /FontMatrix [1 2 3 4 5 (0\); alert\('foobar')] ``` Kết quả hoàn toàn đúng như dự đoán: ![image](https://hackmd.io/_uploads/SyMzQtDHbe.png) 1. Khai thác (Exploitation) Sau khi xác nhận có thể chèn chuỗi tùy ý vào FontMatrix và chuỗi này được nhúng trực tiếp vào thân new Function(), việc khai thác chỉ còn là điều khiển cú pháp JavaScript sao cho hợp lệ. 1.1. Kỹ thuật chèn mã Payload mẫu: /FontMatrix [1 2 3 4 5 (0\); alert\('foobar')] Giải thích: - 0\) → đóng lời gọi c.transform(...) - ; alert('foobar') → chèn lệnh JavaScript tùy ý - Dấu \ dùng để escape theo cú pháp PDF string - Dấu ) dư phía sau được “ăn” bởi cú pháp đã bị phá vỡ 1.2. JavaScript được sinh ra Code thực tế trong Function: ``` c.save(); c.transform(1,2,3,4,5,0); alert('foobar'); c.scale(size,-size); c.moveTo(0,0); c.restore(); ``` 👉 JavaScript được thực thi ngay khi glyph được render, không cần tương tác người dùng. 2. Ngữ cảnh thực thi JavaScript Để chứng minh bối cảnh chạy mã, PoC hiển thị: alert(window.origin) 2.1. Origin của PDF.js Kết quả: resource://pdf.js Không phải: file:///C:/... 2.2. Ý nghĩa bảo mật ✅ Không truy cập trực tiếp local filesystem ❌ Nhưng không phải sandbox yếu PDF.js trong Firefox có: Quyền mở hộp thoại download Có thể trigger download tới file:// URL Truy cập các object nội bộ của PDF viewer 3. Rò rỉ thông tin nhạy cảm 3.1. Lộ đường dẫn file cục bộ PDF.js lưu đường dẫn thật của file tại: window.PDFViewerApplication.url 👉 Attacker có thể biết: Người dùng mở file nào Đường dẫn chính xác trên máy Thời điểm và hành vi sử dụng ➡️ Đây là privacy leak nghiêm trọng, đặc biệt trong: Doanh nghiệp Tài liệu nội bộ Hồ sơ pháp lý / tài chính 4. Tác động khi PDF.js được nhúng (embedded) Đây là kịch bản nguy hiểm nhất. 4.1. PDF.js trong web app Nhiều ứng dụng: Nhúng PDF.js trực tiếp Dùng cùng origin với website 👉 Khi đó: CVE-2024-4367 trở thành một XSS đầy đủ trên domain của ứng dụng 4.2. Hệ quả có thể xảy ra Đánh cắp cookie / token Thực hiện hành động thay mặt người dùng Đọc dữ liệu nội bộ qua API Account takeover 5. Trường hợp Electron – mức độ Critical Trong ứng dụng Electron: PDF.js chạy trong renderer Nếu: Không bật sandbox Cho phép nodeIntegration 👉 JavaScript injection ⇒ Native Code Execution 📌 Tác giả xác nhận: Ít nhất một ứng dụng Electron phổ biến đã bị ảnh hưởng theo cách này. | Môi trường | Tác động | | -------------------------- | ----------------------------------- | | Firefox (standalone) | XSS trong PDF viewer + privacy leak | | Web app embed PDF.js | Full XSS trên domain | | Electron app (unsandboxed) | **RCE (Critical)** | ``` PDF FontMatrix → dict.getArray() → JS array (có string) → args.join(",") → new Function(...) ``` ## Khắc phục Dưới đây là **phần “Mitigation & Affected versions” được giải thích và viết lại rõ ràng theo văn phong báo cáo kỹ thuật / CVE**, thống nhất với các phần trước của CVE-2024-4367. --- ## Biện pháp khắc phục (Mitigation) ### 1. Cập nhật PDF.js (khuyến nghị mạnh nhất) Biện pháp hiệu quả và triệt để nhất là **nâng cấp PDF.js lên phiên bản 4.2.67 hoặc cao hơn**, trong đó lỗ hổng đã được vá hoàn toàn. * Các thư viện wrapper phổ biến như **react-pdf** cũng đã phát hành phiên bản đã vá * Do nhiều thư viện cấp cao **nhúng tĩnh (statically embed) PDF.js**, Mozilla khuyến nghị: > **Kiểm tra đệ quy toàn bộ thư mục `node_modules` để tìm các file `pdf.js`**, tránh bỏ sót phiên bản dễ bị tấn công 📌 Điều này đặc biệt quan trọng trong: * Ứng dụng frontend lớn * Monorepo * Electron app * Các SDK bên thứ ba --- ### 2. Trường hợp dùng PDF.js headless (server-side) Các use-case: * Phân tích PDF * Trích xuất metadata * Thống kê nội dung * Không render canvas / không chạy glyph rendering 👉 **Có khả năng không bị ảnh hưởng**, do: * Code path sinh `new Function()` chỉ được kích hoạt khi render glyph ⚠️ Tuy nhiên: * Chưa được kiểm thử đầy đủ * **Vẫn nên cập nhật** để tránh rủi ro tiềm ẩn hoặc thay đổi hành vi trong tương lai --- ### 3. Workaround: vô hiệu hóa code path dễ bị tấn công Nếu chưa thể nâng cấp ngay, có thể áp dụng biện pháp tạm thời: ```js isEvalSupported = false ``` Tác dụng: * Vô hiệu hóa nhánh code: * Dùng `new Function` * JIT compile glyph commands * PDF.js sẽ fallback sang interpreter an toàn hơn (nhưng chậm hơn) 👉 **Cắt đứt hoàn toàn đường khai thác CVE-2024-4367** --- ### 4. Bảo vệ bằng Content Security Policy (CSP) Nếu ứng dụng áp dụng CSP nghiêm ngặt, ví dụ: ``` script-src 'self'; ``` và **không cho phép**: * `unsafe-eval` * `new Function` 👉 Lỗ hổng **không thể bị khai thác** 📌 Lý do: * `new Function` bị CSP coi là **eval-like** * Trình duyệt sẽ chặn thực thi --- ## Các phiên bản bị ảnh hưởng (Affected versions) Phân tích của **Rob Wu** cho thấy: * **Code dễ bị tấn công tồn tại từ phiên bản đầu tiên của PDF.js** * Tuy nhiên: * Có một số phiên bản **không khai thác được** do **lỗi đánh máy (typo) vô tình chặn đường khai thác** * Sau đó lỗi typo được sửa → **lỗ hổng bị tái xuất hiện** ⚠️ Quan trọng: > Các phiên bản “unaffected” cũ **vẫn không an toàn**, vì dính **CVE-2018-5158** --- ### Bảng tổng hợp phiên bản | Phiên bản | Trạng thái | Ghi chú | | ------------------------ | ----------------------- | ------------------- | | **v4.2.67** (29/04/2024) | ✅ Không ảnh hưởng | Đã vá | | v4.1.392 (11/04/2024) | ❌ Bị ảnh hưởng | Trước khi vá | | v1.10.88 (27/10/2017) | ❌ Bị ảnh hưởng | Lỗ hổng tái xuất | | v1.9.426 (15/08/2017) | ⚠️ Không khai thác được | Nhưng dính CVE khác | | v1.5.188 (21/04/2016) | ⚠️ Không khai thác được | Do typo vô tình | | v1.4.20 (27/01/2016) | ❌ Bị ảnh hưởng | Trước typo | | v0.8.1181 (10/04/2014) | ❌ Bị ảnh hưởng | Phiên bản đầu tiên | 👉 **Không có phiên bản cũ nào thực sự “an toàn”** --- ## Proof of Concept (PoC) * Rob Wu đã **cập nhật PoC PDF** * Hoạt động trên **tất cả các phiên bản bị ảnh hưởng**, bao gồm: * v1.4.20 * v0.8.x * Khi kiểm thử: * Cần dùng **PoC mới nhất** * Đồng thời lưu ý: * CSP * `isEvalSupported` * Môi trường nhúng (browser / Electron) --- ## Kết luận (chuẩn báo cáo CVE) > CVE-2024-4367 ảnh hưởng đến hầu hết các phiên bản PDF.js từng được phát hành. Biện pháp khắc phục hiệu quả nhất là nâng cấp lên phiên bản 4.2.67 hoặc cao hơn. Trong trường hợp chưa thể cập nhật, việc vô hiệu hóa `isEvalSupported` hoặc áp dụng Content Security Policy nghiêm ngặt có thể ngăn chặn hoàn toàn khả năng khai thác. Do các phiên bản cũ còn tồn tại nhiều lỗ hổng khác, việc tiếp tục sử dụng chúng là không an toàn. ## fix - https://github.com/mozilla/pdf.js/pull/18015 - https://github.com/mozilla/pdf.js/pull/18015/commits/551e63901c4e19450673c4832ef2235cf8f2fffa