白帽故事 · 2025年8月12日 0

FileJacking – 如何让浏览器文件API成为初始入侵跳板

摘要

  • 通过文件系统 API 的‘走私’技巧
  • 从浏览器直接植入后门文件
  • 直接从浏览器读取、创建、删除文件和文件夹
  • 无 MotW(标记为可能存在危险的内容) 绕过

介绍

文件系统 API 是一种浏览器 API,允许网络应用执行一些本地文件系统操作,比如直接编辑文件、保存及访问目录。

该 API 于 2020 年由谷歌推出,目前,基于 Chromium 的浏览器(如 Chrome、Edge、Opera 和 Brave)支持此功能,由于安全和隐私问题,Firefox 和 Safari 不支持该 API。在 MDN 上,该 API 标记为 “实验性”,其行为可能在未来发生变化。

免责声明: 本研究是在 2025 年 7 月的 Windows 11 环境下,使用 Chrome 和 Edge 浏览器进行的,并没有在其它基于 Chromium 的浏览器上进行测试。

文件系统 API 实现了四个可用于初始访问的有趣特性:

  • window.showOpenFilePicker()
  • window.showSaveFilePicker()
  • window.showDirectoryPicker()
  • DataTransferItem.getAsFileSystemHandle()

在继续阅读之前,请注意以下几点:

  • 在研究期间,没有观察到 MotW 绕过,在第一次修改过程中,所有允许写入文件的操作都会立即添加 MotW。

  • 文件系统 API 可用于:

    • 含 TLS 的页面(如 https://***
    • 本地 HTML 文件(例如 file:///Z:/index.html),通过电子邮件附件发送实现 FileJacking 的 .html 文件是可行的
  • 所有文件系统 API 函数都无法发现用户选择的路径,你只能靠猜测

  • 本文不会讨论使用 FileJacking 的社交工程技巧

  • 一旦网站获得对目录 / 文件的访问权限,就可以在 “后台” 与其交互,即使用户当前不在页面上

本文不展示所有可能的文件操作,这些操作非常简单且文档齐全:文件写入、文件读取、目录操作。

showOpenFilePicker()

该函数可以打开文件资源管理器,允许你选择一个或多个文件,然后可以请求访问权限来编辑这些文件(弹出窗口),并在用户不知情的情况下继续编辑。

示例代码:

// 1. 打开文件资源管理器并选择文件
const [fileHandle] = await showOpenFilePicker({
    startIn: "desktop"
});

// 2. 授予写入权限 [弹出窗口]
const writable = await fileHandle.createWritable();
await writable.write("lorem ipsum");
await writable.close();

可以读取和编辑任何文件(例如 DLL、EXE、MSI),但有以下限制:

  • 文件不能来自 RESTRICTED-PATHS-2(见下文)。
  • LNK 文件不能指向 RESTRICTED-PATHS-2 中的二进制文件,如果 LNK 文件指向受限路径,则该 LNK 文件会被解析,浏览器会返回限制错误,这个行为很奇怪

注意: 创建后的文件无法重命名或删除。

受限路径演示:

该函数的一个有趣特性是 types 属性,它允许你在文件资源管理器的弹出窗口(右下角)中写入内容,强迫用户只选择我们感兴趣的文件,通过 excludeAcceptAllOption: true,可以隐藏下拉菜单中的 “所有类型” 选项。该标签可以在社交工程环境中作为可信度因素。

showOpenFilePicker({
    types: [
        {
            description: "这是受控的", // <--- 在这里
            accept: { "*/*": [".dll", ".chm"] },
        },
    ],
    excludeAcceptAllOption: true,
});

image.png

另一个有趣的特性是 startIn 属性,该属性允许你定义文件资源管理器默认打开的文件夹,可能的值仅限于以下几项:desktopdocumentsdownloadsmusicpictures 和 videos,这些是当前用户上下文中的默认目录,这允许你更好地引导用户到他们应该查找的地方。

滥用想法: 欺骗用户 “上传” 或 “选择” 某个文件,并在用户不知情的情况下编辑该文件,使用某些容器文件(如 .7z.zip)重写文件,使用 EXE、DLL 或任何应用程序配置文件并进行后门植入,这个操作看起来像是上传,但实际上允许我们修改文件。

showSaveFilePicker()

该函数打开文件资源管理器并允许你保存文件,改变其名称、扩展名和位置,它在磁盘上创建该文件,然后你可以在用户不知情的情况下编辑文件。

示例代码:

// 1. 打开文件资源管理器并选择保存文件的位置
//    (名称在资源管理器中已经建议)
const fileHandle = await window.showSaveFilePicker({
    suggestedName: "test.dll",
    startIn: "desktop"
});

// 2. 写入文件 [无弹出窗口]
const writable = await fileHandle.createWritable();
await writable.write("lorem ipsum");
await writable.close();

可以创建任何文件(例如 DLL、EXE、LNK、MSI),但有以下两个限制:

  • 如果文件的扩展名在 WARNING-EXTENSIONS 列表中,会触发弹出警告窗口,但仍能创建该文件
  • 文件不能在 RESTRICTED-PATHS-2 中创建(见下文)

注意: 无法通过 JS 重命名或删除创建后的文件。

这个函数实现了与 showOpenFilePicker() 中描述的相同 types 和 startIn 属性,它还实现了 suggestedName: string 属性,允许我们控制将要创建的文件的默认名称(包括扩展名),该值可以在文件资源管理器窗口中由用户更改。

值得注意的是,这种方式创建的文件不会出现在下载历史中!在 URL 栏的左侧会出现一个附加图标,指示当前页面正在访问的项,并允许你撤销访问,打开目录时也会以相同的方式工作。

标准下载看起来像下面这样:

image.png

文件系统 API 的"下载"(弹窗不会自动显示,你需要点击图标):

image.png

滥用想法: 这就像是经典 “下载” 机制的加强版,它不会在 “下载历史” 菜单中显示任何痕迹,它允许你在创建文件后进行修改,利用这个机制作为一种巧妙的‘走私’技术,也许能够绕过某些检测?

showDirectoryPicker()

该函数打开文件资源管理器,允许你选择一个以 readwrite 模式打开的目录,选择文件夹后,浏览器中会出现确认弹出窗口,然后你可以在所选目录中创建、编辑和删除文件和子目录,用户却对此一无所知。

示例代码:

// 1. 打开文件资源管理器并选择目录
// 2. 授予“readwrite”权限 [弹出窗口]
const dirHandle = await window.showDirectoryPicker({
    mode: "readwrite",
    startIn: "desktop"
});

// 3. 创建新文件 [无弹出窗口]
const fileHandle = await dirHandle.getFileHandle("test.chm", { create: true });

// 4. 写入文件 [无弹出窗口]
const writable = await fileHandle.createWritable();
await writable.write("lorem ipsum");
await writable.close();

使用该函数有以下限制:

  • 我们可以用此功能打开的路径相当有限,所有允许和不允许的路径在 RESTRICTED-PATHS-1 中描述(见下文)
  • 创建文件和子文件夹也不会在用户界面或 “下载历史” 中生成任何可见的痕迹

滥用想法: 

  • 只需两次点击,即可获取对整个目录及其所有子目录的访问权限,或许你知道组织中有一个自定义的文件夹不存在于黑名单上,并希望访问它,欺骗用户让他们给你访问权限,阅读和修改文件,并寻找持久化的方法。

  • 以新奇的 “下载” 机制来欺骗用户,并利用它作为巧妙的‘走私’技术,记住,你拥有整个目录的所有权,这有点像一个容器(如 .zip.7z),但不需要使用容器本身。

DataTransferItem.getAsFileSystemHandle()

有些浏览器 API 会创建一个 DataTransferItem 对象,如果 DataTransferItem 是一个文件或目录(不一定是文件或目录,你总是需要检查 DataTransferItem.kind),你可以调用 .getAsFileSystemHandle() 方法来获取该项目在文件系统中的句柄。

然后,我们请求对该对象的 readwrite 访问权限,这会在浏览器中打开一个确认弹出窗口。

截止目前有三种已知的浏览器 API 使用 DataTransferItem

这些 API 列表未来可能会扩展,我们将重点关注拖放和剪贴板 API,因为没能找到在 InputEvent API 中使用文件系统项的方法。

拖放 API 的示例用法(通过将文件或目录拖放到页面上工作):

addEventListener("drop", async (e) => {
// 1. 通过拖放文件或目录触发“drop”事件 [无弹出窗口]
const [item] = e.dataTransfer.items;
const fileHandle = await item.getAsFileSystemHandle();

if (handle.kind === "directory") {
    // 与 showDirectoryPicker() 示例相同的操作
} else {
    // 2. 授予“readwrite”权限 [弹出窗口]
    await fileHandle.requestPermission({ mode: "readwrite" });

    // 3. 写入文件 [无弹出窗口]
    const writable = await fileHandle.createWritable();
    await writable.write(content);
    await writable.close();
}
});

剪贴板 API 的示例用法(通过 “Ctrl + V” 粘贴文件或目录):

addEventListener("paste", async (e) => {
// 1. 通过“ctrl + v”触发“paste”事件 [无弹出窗口]
const [item] = e.clipboardData.items;
const fileHandle = await item.getAsFileSystemHandle();

// 其余部分与“drop”事件相同...
});

拖放 / 粘贴目录的行为与 showDirectoryPicker() 完全相同,具有完全相同的限制和行为。

拖放 / 粘贴文件具有独特性,没有任何限制。什么都没有。你可以从任何想要的位置(当然,前提是你的 Windows 账户对该位置有权限)拖放或粘贴 LNK、DLL、EXE 文件,读取和修改它们。

滥用想法:

  • 与 showDirectoryPicker() 相同,但你有另一种与用户交互的方式,拖放或粘贴目录可能会引发其它社交工程场景
  • 显而易见,用它来偷偷植入一些 LNK 文件…

一个小型的概念验证,展示如何执行基于浏览器的后门攻击:FileJacking-PoC

限制

FileJacking备忘录

注意: 这里呈现的列表为原作者的研究成果,原作者并没有找到任何文档解释为什么这些扩展名或路径被阻止,而其它的一些则不被阻止。

受限扩展(RESTRICTED-EXTENSION)

  • Chrome: .dll.lnk.scf.url
  • Edge: .dll.drv.gadget.grp.hta.lnk.ocx.scf.sys.url.xbap
  • 其它浏览器:未测试

针对这份列表中的 141 个扩展名进行测试,测试过程中,并没有观察到同一浏览器内部的差异。为什么这些扩展名在不同浏览器之间有所不同?原因无从得知。你可以使用以下代码在你的浏览器中进行测试:

for (const ext of extensions) {
    try {
        await dirHandle.getFileHandle(name, {
            create: true,
        });
    } catch (err) {
        if (err instanceof TypeError) {
            console.error("禁止的扩展名:", ext);
        } else {
            console.error(`错误: [${ext}]`, err);
        }
    }
}

警告扩展(WARNING-EXTENSIONS)

这些扩展名会导致警告弹出窗口出现,用户可以选择接受或拒绝,由于不知道这些扩展名的完整列表,因此还没有找到其他方法可以通过手动保存文件到文件资源管理器来检查它们。

原作者没有逐个检查,但这个列表肯定比 RESTRICTED-EXTENSIONS 更广泛,因为 .EXE 和 .CHM 也会触发弹窗。

可以肯定的是,如果你正在保存一些可疑的东西,可能会出现这个弹出窗口,但需要检查一下才能确定。

受限路径1(RESTRICTED-PATHS-1)

假设以下列表并不完整,以下是手动检查每个目录的结果,主要在 Chrome 上进行测试

不允许访问:

  • 当前 \desktop 文件夹(无论是默认还是 OneDrive)
  • 当前 \documents 文件夹(无论是默认还是 OneDrive)
  • 当前 \downloads 文件夹(无论是默认还是 OneDrive)
  • C:\
  • C:\Program Files\*
  • C:\Program Files (x86)\*
  • C:\ProgramData\*
  • %APPDATA%\*
  • %LOCALAPPDATA%\*
  • %TEMP%\*
  • %CommonProgramFiles%\*
  • C:\Users\<user>\

允许访问:

  • 当前 \desktop\documents 或 \downloads 的子目录

如果未设置为当前活动目录(例如,如果默认的 \desktop 文件夹已切换为 OneDrive,则可以访问旧的文件夹):

  • C:\Users\<user>\Desktop
  • C:\Users\<user>\Documents
  • C:\Users\<user>\Downloads
  • C:\inetpub\*
  • C:\PerfLogs\*
  • 在 C:\* 中的自定义目录
  • C:\Windows\*
  • %PUBLIC%\*
  • C:\Users\Default\*
  • C:\Users\<user>\Contacts
  • C:\Users\<user>\Favorites
  • C:\Users\<user>\Links
  • C:\Users\<user>\Pictures
  • C:\Users\<user>\Music
  • C:\Users\<user>\Searches
  • C:\Users\<user>\Videos
  • 除了C:\之外的其它根目录

受限路径2(RESTRICTED-PATHS-2)

不允许访问:

  • C:\
  • C:\Program Files\*
  • C:\Program Files (x86)\*
  • C:\ProgramData\*
  • C:\Users\<end of the code>
  • C:\Windows\<end of the code>
  • %CommonProgramFiles%\*
  • %TEMP%\*
  • %LOCALAPPDATA%\*
  • %APPDATA%\*

允许访问:

  • 所有在 RESTRICTED-PATHS-1 中允许的路径
  • desktop\*
  • documents\*
  • downloads\*
  • C:\Users\<user>\

参考资料

原文:https://print3m.github.io/blog/filejacking-initial-access-with-file-system-api