CTF特训营:技术详解、解题方法与竞赛技巧
上QQ阅读APP看书,第一时间看更新

6.2 代码审计的方法与技巧

代码审计是CTF比赛中非常重要的一环,不仅在线上赛占据半壁江山,在攻防赛中也是Web方向的主要考核内容。代码审计不仅需要对各种漏洞、waf绕过技巧非常熟悉,还需要有耐心在茫茫代码中找到那一个存在漏洞的考点。下面就来介绍代码审计方面的一些常用方法和技巧。

1.小型代码

线上赛题目的代码量一般都不大,所以相对来说还是比较容易找到漏洞的,通常可以按照如下所示的几个步骤来进行审计。

1)找到各个输入点。

2)找到针对输入的过滤并尝试绕过。

3)找到处理输入的函数并查看有无漏洞。

4)找到漏洞后进行最充分的利用。

下面利用ASIS 2016的一个简单审计题Binary Cloud示范一下。该题的大概思路是利用PHP7的opcache来执行代码,但关键还是在于如何上传文件。利用主页的一个文件包含漏洞我们可以读到关键源码,下面就来看一下如何上传文件。题目的源码如下:


<?php
function ew($haystack, $needle) {
    return $needle === "" || (($temp = strlen($haystack) - strlen($needle)) >= 0 && strpos($haystack, $needle, $temp) !== false);
}

function filter_directory() {
    $data = parse_url($_SERVER['REQUEST_URI']);
    $filter = ["cache", "binarycloud"];
    foreach ($filter as $f) {
        if (preg_match("/" . $f . "/i", $data['query'])) {
            die("Attack Detected");
        }
    }
}

function error($msg) {
    die("<script>alert('$msg');history.go(-1);</script>");
}

filter_directory();
if ($_SERVER['QUERY_STRING'] && $_FILES['file']['name']) {
    if (!file_exists($_SERVER['QUERY_STRING'])) error("error3");
    $name = preg_replace("/[^a-zA-Z0-9\.]/", "", basename($_FILES['file']['name']));
    if (ew($name, ".php")) error("error");
    $filename = $_SERVER['QUERY_STRING'] . "/" . $name;
    if (file_exists($filename)) error("exists");
    if (move_uploaded_file($_FILES['file']['tmp_name'], $filename)) {
        die("uploaded at <a href=$filename>$filename</a><hr><a href='javascript:history.go(-1);'>Back</a>");
    } else {
        error("error");
    }
}
?>

通过源码,我们首先可以找到可控的输入点为$_FILES、$_SERVER['QUERY_STRING']和$_SERVER['REQUEST_URI']。然后查看有无过滤,分析上传代码之后可以找到如下两个过滤函数:


ew($haystack, $needle)
//这里判断的是上传文件的后缀名是不是.php
filter_directory()
//这个函数的过滤是使parse_url($_SERVER['REQUEST_URI'])['query']的结果不能包含"cache"

可以发现,输入的$_SERVER['REQUEST_URI']经过了函数parse_url的处理,那么这个函数有没有什么特性可以利用呢?通过下面的测试可以看到,我们成功地将query这个结果去掉了,从而绕过了过滤:


/1.php?url=/home/binarycloud/www/cache
array(2) {
    ["path"]  => string(6) "/1.php"
    ["query"] => string(31) "url=/home/binarycloud/www/cache"
}
//1.php?url=/home/binarycloud/www/cache
array(2) {
    ["host"] => string(10) "1.php?url="
    ["path"] => string(27) "/home/binarycloud/www/cache"
}

这样就能成功构造输入,绕过过滤,将文件上传到我们希望的目录下了。

2.大型代码

攻防赛及实战中一般都是对CMS型的框架进行审计,漏洞触发条件一般都不算太难,主要问题还是需要从大量代码中快速定位到这些漏洞,同样也可以按照下面这几个步骤来进行审计:

1)找到危险函数;

2)向上回溯寻找有无可用输入点;

3)尝试绕过针对输入点的过滤;

4)寻找触发漏洞的方法。

这里用的是phpok之前存在的一个注入的例子。首先寻找危险函数,可以找到一个通用的插入数据库的函数,代码如下:


public function save_log($data)
{
    return $this->db->insert_array($data,'wealth_log');
}

然后,查找是否有调用此函数并且有输入点的地方,在framework/model/wealth.php中发现wealth_autosave满足条件,其中存在$_SERVER['QUERY_STRING']可以让我们输入。$_SERVER['QUERY_STRING']是直接取出来的,没有经过过滤,代码如下:


<?php
public function wealth_autosave($uid = 0, $note = '', $main_id = '', $ext = '') {
    ...
    if ($_SERVER['QUERY_STRING']) {
        $url.= '?' . $_SERVER['QUERY_STRING'];
    }
    $data['url'] = substr($url, 0, 255);
    ...
    foreach ($wealth_list as $key => $value) {
        if (!$value['rule']) {
            unset($wealth_list[$key]);
            continue;
        }
        $log = $data;
        $log['wid'] = $value['id'];
        $log['status'] = $value['ifcheck'] ? 0 : 1;
        $log['mid'] = $main_id;
        foreach ($value['rule'] as $k => $v) {
            ...
            if ($ext && is_array($ext) && count($ext) > 0) {
                foreach ($ext as $kk => $vv) {
                    $val = str_replace($kk, $vv, $val);
                }
            }
            $val = round($val, $value['dnum']);
            $log['val'] = $val;
            $get_val = $this->get_val($log['goal_id'], $log['wid']);
            $this->save_log($log);

最后看看如何触发这个漏洞就可以了,发现在framework/api/register_control.php中存在调用,代码如下:


if($uid){
    //保存用户与用户的关系
    if($_SESSION['introducer']){
    $this->model('user')->save_relation($uid,$_SESSION['introducer']);
    }
    $this->model('wealth')->wealth_autosave($uid,P_Lang('会员注册'));
}

即注册成功就能触发漏洞,如图6-7所示。

图6-7 触发漏洞

3.审计工具

目前还没有比较完美的自动化代码审计工具,代码审计工具的结果仍然需要人工处理。这里为大家推荐两款国内比较流行的代码审计工具,分别是收费的“RIPS”和免费的“Seay源代码审计系统”。

RIPS是一款非常优秀的源码审计工具,现在已经是一款收费工具了,并且报价不菲。虽然RIPS的免费版只是一个雏形,但依然可以从中窥探其优秀的算法,并根据我们的需要对其进行修改。

RIPS的主要思路就是利用PHP的函数token_get_all来分析代码的语法,通过一定的规则识别出每个漏洞,再通过回溯追踪输入点来查看是否能够构成漏洞。

当然,开源版RIPS的缺点也很明显,一是漏洞规则不够准确,可能出现误报、漏报的情况;二是输入追踪比较潦草,尤其是包含文件时几乎没有处理,所以导致了准确度较低的问题。

我们可以将它当作一个源码审计系统框架,基于其算法思路来编写我们自己的系统,关键代码位于lib/scanner.php中。

它的使用方法很简单,直接在路径上填入待扫描的源码文件夹就可以了,如图6-8所示。

图6-8 RIPS设置界面

扫描完成后,结果将直接显示出来,并且指出你可以输入的地方,这样你就可以有针对性地寻找漏洞了,如图6-9所示。

图6-9 RIPS扫描结果

接下来介绍的是“Seay源代码审计系统”,该源码审计系统的设计思路比较简单,自动审计的过程主要是根据各种正则表达式匹配的结果来判断是否存在漏洞,主要还是简化了审计人员的一些重复工作,是一款比较实用的工具。

下面列出其中几个比较常用的功能。

(1)自动审计功能

这个功能就是与预先设置好的正则匹配表达式相匹配,如果匹配成功则会打印出来,图6-10为一个检测一句话后门的结果。

图6-10 检测结果

当然,你也可以选择使用自定义的规则,在系统配置的规则管理选项中进行设置就可以了,如图6-11所示。

图6-11 规则管理

(2)全局搜索功能

通过这个功能,你可以在所选的目录下搜索你输入的字符串,通常可以查找关键函数、变量或关键字符串,如图6-12所示。

图6-12 全局搜索

除了上面介绍的两款代码审计工具之外,我们还可以借助安全狗、D盾、护卫神等扫描Webshell的工具来扫描一下代码,检查是否有被预留Webshell或是明显危险的函数调用,以防出现问题。