得益于初赛的人品,我们有幸入围了决赛。但是对于决赛的比赛方式:攻防赛,我们并不了解,事先也不知道需要准备哪些东西,只是配了几个扫描器,然后就是之前用的 IDA 之类的工具和各种语言环境。经过两天的比赛,最终成绩是第 12 名(如果没有赛后的名次变动的话)。这次比赛确实让我学到了一些东西,下面我就来说一说吧~
各组选手维护相同的一系列服务,每五分钟(第二天改为了三分钟)为一轮,有一个flag 文件是 /home/flag/flag
,你需要努力获取其它队伍的 flag 文件,也要尽量保证自己的 flag 文件不会被获取。每一轮这个文件的内容都会变,每一轮每个队伍只能提交获取到的其它各个队伍的 flag 各一次。也就是说,如果你不把漏洞修好,那么每一轮都可以被所有发现该漏洞的队伍攻击一次;每一轮会有一次服务存活检测,如果服务 down 掉了,丢失的分数会更多。
由于这次的题目类型大多是 PWN 的,而我是一只 WEB 狗,所以大部分的分数并不是我拿的,对于 PWN 的题目我也没法做什么分析。这次比赛的 WEB 题是这样的:你要维护的是一个简单的博客系统,使用的框架是 PHP Slim,支持最简单的注册、登录、发博文(标题、纯文本内容、模板名称)的功能。flag 文件是 /home/flag/flag
,属于 www-data
,权限为 511(每一轮自动换)。我们需要获取其它队伍的 flag 文件中的内容。
与之同时发布的还有一道 PWN 题,@沈园 同学果断接下了这个锅(事实上我们几乎所有有成绩的 PWN 题,修补漏洞和编写 EXP 都是他负责的,在此先膜拜一下 ),我和@SummerZhang 同学开始看 WEB。
首先先用 tar
命令将整个 web 目录打包,放到 /tmp
下,然后通过 scp
命令将其复制到本地。
1 | scp ctf@10.250.111.11:/tmp/www.tar.gz ./www.tar.gz |
解压缩之后对里面的文件进行逐一查看:
1 | web |
其中 html
文件夹中主要是 PHP 文件,config.php
是一些配置项,包括数据库的账号和密码,由于每一队维护的服务代码都是相同的,而且我们也没权限修改数据库的登录密码,因此这些无需修改。但是上面有这么一句:
1 | $config['displayErrorDetails'] = true; |
为了保险起见还是改成 false
吧。接下来是 db.php
,是自己写的一个库文件,我们大概能感觉到这里面会有 SQL 注入的风险。
1 | public function where($key = '', $operate = '', $value = '') { |
看到这个函数的时候我还诧异:居然写了过滤?然而找到这个函数之后才发现:
1 | public function filter($value) { |
坑爹呢!于是赶紧在 return 的值外面包了层 addslashes
。继续往下看有个 select
函数:
1 | public function select($value = '*') { |
不用想了,$where
这儿也有问题。在 else
一段为它添加 addslashes
吧:
1 | else { |
下面是一个 insert
函数,不定参数。其中有一句似乎是调用了 $this->filter
:
1 | $args_list = array_map(array($this, 'filter'), func_get_args()); |
这段应该是没问题的,所以不改了。下面的 sess
类和 hello
函数也没发现什么问题。至此,db.php
已经没有什么明显的 BUG 了。接下来看 index.php
,其中对路由 /list
的处理中,有一段看起来可能有问题:获取到文章的信息存放到 $result
之后,执行渲染的函数:
1 | return $this->view->render($response, "/list.tpl", array( |
其中 /list.tpl
文件中有这样一段:
1 | {% for note in notes %} |
note.3
是我们发这篇文章时选择的模板文件,它是在路由 /post
中被这样生成的:
1 | $title = $parsedBody['title']; |
当然,如果 filter
函数没有修改的话,可以这样直接注入:
1 | POST /post HTTP/1.1 |
这样拼接出来的 SQL 是:
1 | INSERT INTO notes VALUES ('[logged_username]', '1', 'title', '\/note_tpl\/..\/..\/..\/..\/home\/flag\/flag') # ', 'xxx', '0') |
于是在 render 的时候会触发文件包含的漏洞。如果数据库防了注入,这招就失灵了。但是我们可以这样:发现数据库的 template
字段类型是 varchar
,有长度限制,我们只需要用空格填满剩余的空间即可:
1 | title=0&content=xxx&temp=..%2F..%2F..%2F..%2Fhome%2Fflag%2Fflag++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
于是存到数据库中的就相当于 /home/flag/flag
了。要说这个也属于逻辑设计不合理,应该是在数据库存放文件名,然后渲染之前现场拼接路径。其实 Slim 本身也做了一层过滤,在 vendor/twig/twig/lib/Twig/Loader/Filesystem.php
中有过滤的函数,把注释去掉就可以限定包含的文件只能在当前目录下:
1 | protected function validateName($name) { |
但是这么修改太麻烦了,我们看了一下,只有三个模板,文件名分别是 1、2、3……于是果断加了一句:
1 | $parsedBody['temp'] = intval($parsedBody['temp']); |
这下管你什么文件包含呢,通通没办法了吧?于是第一天我们的 WEB 题没有丢分(有一段时间许多队伍的 WEB 被 DoS 了,而 DoS 是被规则禁止的攻击方式,不知道主办方会怎么处理,这段时间的丢分我就认为不算丢分吧),反倒还拿了其它队伍不少分。
但是第二天就奇怪了,一大片队伍的 WEB 题都 down 掉了,我们不光 down 了,还被拿到了 flag。这怎么能忍?我们一遍遍排查代码,确认没有什么逻辑上的漏洞。然后突然发现服务器操作特别慢,于是 ps aux
了一下,发现了一大堆这样的命令:
1 | sh -c echo 123;x() { x|x& };x |
卧槽,居然连 fork 炸弹都上了!这也是被规则禁止的,于是我们通知了主办方,主办方把所有队伍的 WEB 服务都重启了一遍,但是我们的 WEB 题还是既 down 又被 flag,简直神奇。看了一下 /tmp
目录下被上传了一堆 shell,但是我们的 ctf 用户没权限删除 www-data 用户创建的内容。后来我们直接给自己的服务器上了一个 webshell(这就是俗话说的:我急了连自己的机器都上 shell!),因为 webshell 就是以 www-data 用户身份运行的。我们没发现有什么可以上传文件的地方,但是确实是被 get shell 了,于是在找出上传方法之前,先将所有的 shell 文件 kill 掉,然后对其执行 chmod 000
;刚才 ps 的时候还发现了一个定时发送 flag 的 crontab,于是也果断将其清空。
就算这样还是被 flag 了,而且还在 down 着。我们在改完代码测试流程的时候偶然发现无法注册无法登录,于是猜想是不是数据库挂掉了,于是连进数据库一看,发现整个 database 全部被 drop 掉了……我们之前没有备份数据库,但是凭借着一点点记忆力以及@SummerZhang 同学根据代码推断数据库结构的能力,直接手动建起了数据库,恢复了服务的运行:
1 | create database 0ops; |
但是文件是怎么传上来的呢?我们在 index.php
文件的最一开始加了一段代码,可以将全部的 HTTP 请求包记录到 /tmp/log.txt
中,然后我们就在命令行中 tail -f /tmp/log.txt
,开始分析所有的请求,最终锁定了两个奇怪的请求:
1 | GET /index.php?59b620d4=6cd13eb6assert41a2e1&edfd2=50cbin1d3&208a8e=74fe6cdupload89f&25411bcd=cdde9uploadf814ff266a&cecc789=9ce4c38feeval1de&2e84e621f=368c9e9baa918e&7657=b4a6uploadb339c1b1a&d54c1=1925cinto4aa&28d5bd999f=e7fselect3c37&b5fee3356a=c27ceeval2038&43a7c6bb4=4b3b74assert7a51&e9f6642fc=27b7into244&10fd41aefe=44e18a89a6into2a2f&08a3c97=ee6into3a909a4&c565ef5=6ec68upload2224e453&4df26=1fd254select4caaf&3c743ef7=a69bbfaassertbfa HTTP/1.1 |
1 | POST /post HTTP/1.1 |
我们自己尝试了一下,第二个请求之后会直接报 Slim application error
,但是保险起见我们将所有 UA 带有 -kali-
字样的请求全部 die 掉,第一个请求貌似是主办方的服务存活检测,因为将其 die 掉之后我们的网站虽然还能正常运行,但是被判定为 down(但是没有被 flag,这一点我没有及时注意到,这是我的锅),取消 die 之后又变回了只被 flag 的状态。后来惊觉:这是主办方留的后门被人利用了!在 vendor/autoload.php
下面发现了后门:
1 | require_once __DIR__ . '/composer' . '/autoload_real.php'; |
在 vendor/composer/autoload_real.php
中发现了这样一段代码:
1 | /** |
上面那一串乱码一样的东西其实是个混淆,只要稍微改一改,顺着解析一遍就可以了。把最后的 $v();
去掉(一看就是用来执行解析出来的函数的),然后输出 $y
、$L
:
1 | 【$y】 |
所以重点就是 $L
了。稍微美化一下,在不修改逻辑的情况下简化一些语句,可得:
1 | function xor_encode($text, $key) { |
可以看出这就是个 webshell,内容是通过 Referer 传进来的。除了好多加密解密以绕过过滤的函数以外,核心代码在这儿:
1 | ob_start(); |
所以只要以同样的方式传进来数据,那么显然可以直接 get shell!似乎这个文件由于权限问题没法直接修改,所以解决问题的最简单的方法就是在 index.php
中加入一行代码:
1 | $_SERVER['HTTP_REFERER'] = 'Hello friend'; |
改完之后又过了一轮,我们的 WEB 完全正常了。虽然这个时候已经被打的很惨了……
至于那些 PWN 的题,@沈园 同学负责分析、补漏洞(直接手工修改二进制文件也是 666)、写 exp,@SummerZhang 同学来跑 exp,因为 exp 不是很稳定所以他还顺便当了一次人肉守护进程。
当然,也多亏 @SummerZhang 同学连夜搞出了那道 400 分的静态分析题,现学现卖的能力果然好强。
总之,还是我们的水平不够啊……不过这次比赛对我们以后为校内赛出题提供了很多思路,说不定以后的 NUAACTF 就不光会有 CTF,还会有渗透和攻防的赛程了呢!