1721 字
9 分钟
Loading... Page Views
【赛博保安】某些 PDF 加密分享平台获取源文件

本文章没有针对任何厂商和平台, 我只是平等地看不起所有把开源代码修改几下, 自以为是地加个所谓的”加密”, 就敢把产品拿出来祸害用户的垃圾厂商.

本文章的所有内容纯属虚构, 如有雷同纯属巧合.

0 写在前面#

本文中以虚构的笔X分享为例, 介绍一种通过 HTTP 流量抓包/重放实现的简单的获取声称”被加密”的 PDF 源文件的方法.

首先来细数这类分享平台的罪恶:

  • 滥用开源代码, 不遵守开源协议. 不仅代码闭源, 甚至在不标注代码来源的情况下修改代码后商用.
  • 以文件”加密”平台自居, 实际上并未对文件进行任何意义上的加密, 仅使用开源的前端 JS 代码对 web 端页面权限进行限制. 文件传输\读取过程暴露在完全未加密的状态下, 属于是害人来的.
  • 文件水印功能实际上只显示在前端, 并未对文件进行加水印操作, 属于是想省钱想疯了.

1 工具准备#

  • 任意抓包工具, 只要能抓 HTTP 流量就行

本文以 Burp Suite Community 为例.

2 XX, 启动!#

启动 Burp, 在 Proxy 页上开启拦截.

在 Burp 内置的谷歌浏览器中访问得到的分享链接.

如果你使用的是 2025 年之后版本的 Burp 的话, 会看到 Burp 将页面的所有 web 请求都整齐地列了出来, 这点比老版本的 Burp 会舒服很多. 而且还可以多选 Forward/Drop.

作为例子的这个网站接入了腾讯的统计系统, 会频繁向 otheve.beacon.qq.com 发送请求. 并且每隔 1s 就会发起一次 heartbeat 请求. 比较烦人, 可以右键停止拦截到这些目录的请求.

放一些包, 直到看到一个明摆着加载了文件的包时:

好家伙, 文件直接存到 CDN 里面, 访问的时候前端限制一下, 然后 CDN 里面直接丢源文件过来.

把这个请求传到 Repeater 里面.

就拿到了源文件.

Response 栏上面三条杠, 保存到文件, 再用二进制编辑器把文件内容之外的 HTTP 响应体删掉. 如果你的 PDF 阅读器够现代化, 可以自动识别到文件头的位置, 甚至可以直接打开.

3 所以… 问题出在哪里#

首先大概梳理以下这个网站的”加密”逻辑:

用户上传文件#

存储至 CDN 并为文件生成一个 ID (疑似为 Base64 编码后的数字字符串)-> (猜测)将文件在 CDN 中的存储位置与随机生成的文件 ID 存储至数据库

这里就已经出现了比较严重的问题了.

可以看到, 当文件上传时, 会跳转至 /#/pdf/result/<digital id> 页面, 会将访问文件时使用的链接(https://pdfxx.cn/api/pdf/pdf?id=MTIyxxxx)显示出来. 神奇的事情发生了, 链接中的 id 参数在 base64 解码之后正是 url 中的<digital id>. 因此猜测存在 id 爆破问题. 尝试访问几个其他的文件:

确实和猜测的一样. 多试了几个, 没有加密的文件是可以直接访问的:

访问文件时会从 CDN 上获取对应的文件

用户访问文件#

根据用户访问的 id 查询文件在 CDN 上的路径 -> 用 pdfjs 加载 CDN 上的 pdf 文件.

也就是说, 全程实际上没有涉及到加密传输之类的技术, 仅在前端对按键\开发者工具等进行了限制.

在看 js 代码时, 发现了一个接口:

联系上下文, 应该是获取 pdf 文件在 CDN 中存储地址的接口, 浅试一下:

至此已经可以在获得文件分享地址的情况下提取到 pdf 源文件了.

import requests
from base64 import b64decode
host = "pdfxx.cn"
id = "xxxxxx"
url_api = f"https://{host}/api/pdf/uurl?id={id}"
headers = {
"Host": host,
"Sec-Ch-Ua": '"Chromium";v="133", "Not(A:Brand";v="99"',
"Sec-Ch-Ua-Mobile": "?0",
"Sec-Ch-Ua-Platform": '"Windows"',
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
"Accept": "*/*",
"Origin": f"https://{host}",
"Sec-Fetch-Site": "same-site",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Dest": "empty",
"Referer": f"https://{host}/",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9",
"Priority": "u=1, i",
"Connection": "keep-alive"
}
api_cdn = requests.get(url_api, headers=headers, timeout=10)
url_cdn = b64decode(api_cdn.json.get("data").encode())
response = requests.get(url_cdn, headers=headers, timeout=100)
response.raise_for_status()
with open(f"{url_cdn.split('/')[-1]}", "wb") as f:
f.write(response.content)

用户浏览器加载 pdf 文件#

前端 js 对用户行为进行限制, 包括:

  • 监听并劫持键盘事件
  • 定时发送 HeartBeat, 以实现限制单次访问时长的功能
  • 检测到 Debugger 之后跳转到 ConsoleBan 页面
  • 鼠标焦点离开页面后显示遮罩层

祭出我的 Tamper Monkey 脚本, 稍作适配:

// ==UserScript==
// @name PDFXX Unlocker
// @namespace http://noahbox.github.io
// @version 0.1
// @description 解除PDFXX的控制台检测/右键限制/遮罩层
// @author NoahTie
// @match *://pdfxx.cn/*
// @run-at document-start
// @grant unsafeWindow
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 禁用ConsoleBan
// ========================================
unsafeWindow.ConsoleBan = class {
constructor() {
console.log("[Bypass] ConsoleBan constructor neutralized");
}
init() {}
ban() {}
fire() {
console.warn("[Bypass] Blocked redirect attempt");
return false;
}
};
Object.defineProperty(unsafeWindow, 'ConsoleBan', {
configurable: false,
writable: false
});
// 事件系统劫持
// ========================================
const nativeAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
const blockedEvents = [
'contextmenu', 'selectstart', // 右键/选择限制
'visibilitychange', 'blur', // 标签页切换检测
'mouseleave', 'mouseout' // 鼠标离开检测
];
if (blockedEvents.includes(type)) {
console.log(`[Bypass] Blocked ${type} event binding`);
return;
}
nativeAddEventListener.call(this, type, listener, options);
};
// 动态遮罩层清除
// ========================================
GM_addStyle(`
#mask-layer, .anti-shade, [class*="mask"] {
display: none !important;
opacity: 0 !important;
pointer-events: none !important;
}
body > div:not(#viewerContainer) {
filter: none !important;
}
`);
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1 && (
node.id.includes('mask') ||
node.className.match(/shade|modal|overlay/)
)) {
node.remove();
console.log("[Bypass] Removed dynamic mask layer");
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// 定时器清理
// ========================================
const timerStore = new Set();
const hijackedSetInterval = window.setInterval;
window.setInterval = (fn, delay) => {
const id = hijackedSetInterval(() => {
try {
if(!fn.toString().includes('debugger')) fn();
} catch(e) { console.error(e); }
}, delay);
timerStore.add(id);
return id;
};
setInterval(() => {
timerStore.forEach(id => {
clearInterval(id);
clearTimeout(id);
});
timerStore.clear();
}, 1000);
// 网络请求拦截
// ========================================
const nativeFetch = window.fetch;
window.fetch = (url, options) => {
if (/consoleBan|lunxun/.test(url)) {
console.log(`[Bypass] Blocked request to ${url}`);
return Promise.reject(new Error('Blocked by script'));
}
return nativeFetch(url, options);
};
XMLHttpRequest.prototype.open = function(method, url) {
if (/consoleBan|lunxun/.test(url)) {
this._blocked = true;
console.log(`[Bypass] Blocked XHR to ${url}`);
}
nativeOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function(data) {
if (this._blocked) return;
nativeSend.call(this, data);
};
// 右键恢复
// ========================================
document.addEventListener('DOMContentLoaded', () => {
document.body.oncontextmenu = null;
document.querySelectorAll('*').forEach(el => {
el.oncontextmenu = null;
el.style.pointerEvents = 'auto';
});
});
// 反反调试
// ========================================
Object.defineProperty(unsafeWindow, 'console', {
value: console,
writable: false,
configurable: false
});
Object.defineProperty(unsafeWindow.console, 'log', {
value: console.log,
writable: false
});
})();

至此, 除了需要密码访问和需要微信鉴权的文件之外, 已经完全结束了.

4 And more … ?#

在尝试了密码加密和微信鉴权之后, 我发现了一些有趣的东西.

不过这些不打算写出来. 毕竟已经涉及到用户隐私了.

还是做一个提醒, 不要相信这些加密和微信鉴权.

另外, 这个平台的文件访问密码一定是 6 位纯数字的, 没有任何 captcha 机制, api 接口暴露且没有来源检测.

评论

【赛博保安】某些 PDF 加密分享平台获取源文件
https://noahbox.github.io/posts/某些pdf加密分享平台获取源文件/
作者
MasterNoah
发布于
2025-02-23
许可协议
CC BY-NC-SA 4.0