前言:
PDF文件是一种复杂的文档格式,由一系列对象组成,包括字体、图像、页面内容等。PDF文件支持嵌入JavaScript代码,这使得PDF文件不仅可以显示静态内容,还可以执行动态操作。这种特性被攻击者利用来嵌入恶意脚本代码。
攻击者通过在PDF文件中嵌入恶意JavaScript代码,使得当用户打开该PDF文件时,恶意代码会在用户的浏览器或PDF阅读器中执行。
pdf-xss制作:
python代码如下:
from PyPDF2 import PdfReader, PdfWriter
# 创建一个新的 PDF 文档
output_pdf = PdfWriter()
# 添加一个新页面
page = output_pdf.add_blank_page(width=72, height=72)
# 添加js代码
output_pdf.add_js("app.alert('xss');")
# 将新页面写入到新 PDF 文档中
with open("alert.pdf", "wb") as f:output_pdf.write(f)
执行后会将脚本植入到pdf中,对应的pdf分析
当我们打开对应的pdf文件,可以看到弹出了对应的xss对话框
修复:
代码修复也很简单,就是要处理JavaScript方法,对应的处理的点如下
- 识别并移除
/Names/JavaScript
结构 - 处理嵌套字典中的JavaScript
- 处理数组中的JavaScript动作
- 递归检查所有可能的位置
对应的java代码如下
package org.example;import org.apache.pdfbox.Loader;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;import java.io.File;
import java.io.IOException;
import java.util.List;public class Main {public static void main(String[] args) {String inputFile = "alert.pdf";String outputFile = "safe-output.pdf";try {removeJavaScript(inputFile, outputFile);System.out.println("PDF中的JavaScript已彻底移除!");} catch (IOException e) {System.err.println("处理失败: " + e.getMessage());}}public static void removeJavaScript(String inputPath, String outputPath) throws IOException {PDDocument document = Loader.loadPDF(new File(inputPath));try {// 1. 处理文档级JavaScriptprocessDocumentLevelJS(document);// 2. 处理页面级JavaScriptprocessPagesJS(document);// 3. 保存处理后的文档document.save(outputPath);} finally {document.close();}}// 处理文档级的JavaScriptprivate static void processDocumentLevelJS(PDDocument document) {// 获取文档目录PDDocumentCatalog catalog = document.getDocumentCatalog();COSDictionary catalogDict = catalog.getCOSObject();// 处理文档开放动作catalog.setOpenAction(null);// 专门处理Names字典中的JavaScriptprocessNamesDictionaryJS(catalogDict);// 移除标准JS和AAremoveDictJavaScript(catalogDict);}// 专门处理Names字典中的JavaScriptprivate static void processNamesDictionaryJS(COSDictionary dict) {if (dict.containsKey(COSName.NAMES)) {COSDictionary namesDict = dict.getCOSDictionary(COSName.NAMES);if (namesDict != null) {// 检查JavaScript字典if (namesDict.containsKey(COSName.JAVA_SCRIPT)) {namesDict.removeItem(COSName.JAVA_SCRIPT);System.out.println("已移除文档级的Named JavaScript字典");// 如果Names字典变空(即没有其他条目了),则移除整个Names字典if (namesDict.size() == 0) { // 修改这里:使用size()==0代替isEmpty()dict.removeItem(COSName.NAMES);}}}}}// 处理页面级JavaScriptprivate static void processPagesJS(PDDocument document) throws IOException {for (PDPage page : document.getPages()) {// 处理页面级JavaScriptCOSDictionary pageDict = page.getCOSObject();removeDictJavaScript(pageDict);// 处理页面注释List<PDAnnotation> annotations = page.getAnnotations();for (PDAnnotation annotation : annotations) {removeAnnotationJavaScript(annotation);}}}// 移除注释中的JavaScriptprivate static void removeAnnotationJavaScript(PDAnnotation annotation) {COSDictionary dict = annotation.getCOSObject();removeDictJavaScript(dict);}// 核心方法:移除COSDictionary中的JavaScriptprivate static void removeDictJavaScript(COSDictionary dict) {// 1. 直接移除所有JavaScript项dict.removeItem(COSName.JS);// 2. 检查并移除标准动作中的JavaScriptif (dict.containsKey(COSName.A)) {COSDictionary actionDict = dict.getCOSDictionary(COSName.A);if (isJavaScriptAction(actionDict)) {dict.removeItem(COSName.A);}}// 3. 检查并移除附加动作中的JavaScriptif (dict.containsKey(COSName.AA)) {COSDictionary aaDict = dict.getCOSDictionary(COSName.AA);removeAllJavaScriptFromDict(aaDict);}// 4. 处理Names字典(针对嵌套的Names字典)processNamesDictionaryJS(dict);}// 检查是否为JavaScript动作private static boolean isJavaScriptAction(COSDictionary actionDict) {// JavaScript动作的标识:/S 为 /JavaScriptreturn COSName.JAVA_SCRIPT.equals(actionDict.getCOSName(COSName.S)) ||actionDict.containsKey(COSName.JS);}// 彻底清除字典中的所有JavaScriptprivate static void removeAllJavaScriptFromDict(COSDictionary dict) {// 遍历所有条目for (COSName key : dict.keySet().toArray(new COSName[0])) {COSBase value = dict.getItem(key);// 1. 直接移除JavaScript项if (value == COSName.JS || key == COSName.JS) {dict.removeItem(key);}// 2. 处理嵌套的JavaScript字典else if (value instanceof COSDictionary) {COSDictionary subDict = (COSDictionary) value;if (isJavaScriptAction(subDict)) {dict.removeItem(key);} else {// 递归处理嵌套字典removeAllJavaScriptFromDict(subDict);}}// 3. 处理数组中的JavaScriptelse if (value instanceof COSArray) {processArrayForJavaScript((COSArray) value);}}}// 处理数组中的JavaScriptprivate static void processArrayForJavaScript(COSArray array) {for (int i = 0; i < array.size(); i++) {COSBase element = array.get(i);if (element instanceof COSDictionary) {COSDictionary dict = (COSDictionary) element;if (isJavaScriptAction(dict)) {array.remove(i); // 从数组中移除JavaScript字典i--; // 调整索引}}}}
}
上述代码具体的执行逻辑如下:
-
完整遍历:
- 文档级 (1次)
- 页面级 (每页1次)
- 注释级 (每个注释1次)
- 潜在嵌套结构 (无限递归深度)
-
脚本检测:
- 检测到JS项的PDF对象:直接移除
- 检测到A/AA项中的JS动作:移除整个动作项
- 检测到Names中的JS:移除整个JS子字典
-
资源清理:
- 移除后的空容器会被清理
- 只移除脚本相关项,保留其他内容
对应的执行完成的和开始的对比
统一为1.6版本的pdf格式
如此便可以去除pdf文件中的js脚本代码,后续最好再head中添加
CSP和X-Content-Type-Options: nosniff
可以进一步防御xss攻击