戰(zhàn):JWT算法混淆與Python pickle反序列化漏洞解析)
1. 從兩道題看Web安全里的兩個(gè)經(jīng)典坑一天一道CTF做到第41天說(shuō)實(shí)話(huà)已經(jīng)進(jìn)入了一個(gè)比較微妙的階段——簡(jiǎn)單題刷完了純靠工具掃的題也差不多見(jiàn)底了剩下的基本都是需要你真正理解某個(gè)機(jī)制才能繞過(guò)去的題目。今天碰到的兩道題恰好代表了Web安全里兩個(gè)非常經(jīng)典的方向一個(gè)是JWTJSON Web Token的算法混淆與密鑰爆破另一個(gè)是Python pickle反序列化漏洞的利用。這兩個(gè)東西在實(shí)際滲透測(cè)試和代碼審計(jì)中出現(xiàn)頻率極高而且一旦理解透了你會(huì)發(fā)現(xiàn)它們的利用思路其實(shí)非?!皟?yōu)雅”。這篇文章適合已經(jīng)有一點(diǎn)CTF基礎(chǔ)、知道Burp怎么抓包改包、能看懂Python基本語(yǔ)法但還沒(méi)系統(tǒng)接觸過(guò)JWT安全和反序列化漏洞的讀者。如果你完全沒(méi)碰過(guò)CTF建議先去做幾道SQL注入和文件包含的題再來(lái)不然可能會(huì)覺(jué)得跳躍太大。我會(huì)把每道題的完整思路、踩過(guò)的坑、以及我實(shí)際測(cè)試時(shí)的一些心得都寫(xiě)出來(lái)盡量讓你看完之后能自己復(fù)現(xiàn)一遍。JWT和pickle這兩個(gè)東西一個(gè)是認(rèn)證機(jī)制里的常見(jiàn)實(shí)現(xiàn)一個(gè)是Python生態(tài)里繞不開(kāi)的序列化方案它們的共同點(diǎn)是開(kāi)發(fā)者覺(jué)得用起來(lái)很方便但安全邊界非常容易被忽視。JWT如果密鑰弱或者算法校驗(yàn)不嚴(yán)攻擊者可以直接偽造任意身份pickle如果對(duì)不可信數(shù)據(jù)做反序列化基本等于把代碼執(zhí)行權(quán)限拱手讓人。下面我分兩大塊來(lái)講每塊都會(huì)從原理到實(shí)操完整走一遍。2. JWT算法混淆與密鑰爆破實(shí)戰(zhàn)2.1 JWT的基本結(jié)構(gòu)與你需要關(guān)注的三個(gè)部分JWT的全稱(chēng)是JSON Web Token本質(zhì)上就是一個(gè)字符串由三部分組成用點(diǎn)號(hào)分隔Header.Payload.Signature。Header是一個(gè)Base64Url編碼的JSON通常包含算法類(lèi)型alg和令牌類(lèi)型typPayload也是Base64Url編碼的JSON里面放的是實(shí)際傳遞的數(shù)據(jù)比如用戶(hù)ID、角色、過(guò)期時(shí)間等Signature是對(duì)前兩部分的簽名用來(lái)保證令牌沒(méi)有被篡改。很多人第一次看到JWT會(huì)覺(jué)得它像加密過(guò)的其實(shí)不是——Header和Payload就是明文Base64編碼任何人拿到Token都可以直接解碼看到內(nèi)容。JWT的安全性完全依賴(lài)于簽名部分。如果簽名算法被繞過(guò)或者密鑰被猜出來(lái)整個(gè)認(rèn)證體系就形同虛設(shè)。我在做這道題的時(shí)候第一步就是拿Burp抓包找到請(qǐng)求頭里的Authorization字段把Token復(fù)制出來(lái)。然后用一個(gè)在線(xiàn)的JWT解碼工具或者直接手寫(xiě)Python腳本解碼看看Payload里有什么。這道題的Payload里有一個(gè)role字段值是user還有一個(gè)username字段。很明顯如果把role改成admin就有可能拿到管理員權(quán)限。但問(wèn)題是簽名。你改了Payload簽名就對(duì)不上了服務(wù)器會(huì)拒絕。所以接下來(lái)的核心就是怎么讓服務(wù)器接受一個(gè)被篡改過(guò)的Payload。2.2 算法混淆攻擊把RS256降級(jí)成HS256這道題的第一個(gè)突破口是算法混淆。JWT支持多種簽名算法常見(jiàn)的有HS256HMAC SHA-256對(duì)稱(chēng)密鑰和RS256RSA SHA-256非對(duì)稱(chēng)密鑰。HS256用同一個(gè)密鑰簽名和驗(yàn)證RS256用私鑰簽名、公鑰驗(yàn)證。算法混淆攻擊的核心邏輯是這樣的服務(wù)器原本用RS256驗(yàn)證時(shí)應(yīng)該用RSA公鑰來(lái)驗(yàn)簽。但如果服務(wù)器代碼沒(méi)有嚴(yán)格限定算法類(lèi)型攻擊者可以把Header里的alg改成HS256然后用RSA公鑰的內(nèi)容作為HMAC的密鑰來(lái)簽名。服務(wù)器一看alg是HS256就傻乎乎地用那個(gè)“公鑰”當(dāng)HMAC密鑰去驗(yàn)證結(jié)果就通過(guò)了。這個(gè)攻擊成立的前提是服務(wù)器信任了客戶(hù)端傳來(lái)的alg字段并且沒(méi)有做算法白名單校驗(yàn)。在實(shí)際代碼中很多JWT庫(kù)的默認(rèn)行為就是讀取Header里的alg來(lái)決定驗(yàn)證方式如果開(kāi)發(fā)者沒(méi)有顯式指定算法就會(huì)中招。我實(shí)際操作的時(shí)候先要拿到服務(wù)器的RSA公鑰。有些題目會(huì)直接把公鑰放在某個(gè)路徑下比如/jwks.json或者/publickey.pem。這道題的公鑰藏在響應(yīng)頭的一個(gè)自定義字段里Base64編碼過(guò)的。拿到公鑰之后用Python的PyJWT庫(kù)來(lái)構(gòu)造惡意Tokenimport jwt public_key open(public.pem, rb).read() payload { username: admin, role: admin, iat: 1700000000 } token jwt.encode(payload, public_key, algorithmHS256) print(token)注意這里有個(gè)坑PyJWT的新版本對(duì)算法混淆有防護(hù)如果你直接用公鑰做HMAC密鑰它可能會(huì)報(bào)錯(cuò)。我試的時(shí)候用的是PyJWT 1.7.1版本這個(gè)版本沒(méi)有防護(hù)可以直接生成。如果你用的是新版本可以手動(dòng)構(gòu)造Header和Payload然后自己算HMAC簽名不依賴(lài)庫(kù)。構(gòu)造出來(lái)的Token發(fā)過(guò)去如果服務(wù)器返回了管理員權(quán)限的響應(yīng)就說(shuō)明算法混淆成功了。這道題確實(shí)就是這么過(guò)的。注意算法混淆攻擊的關(guān)鍵在于服務(wù)器是否嚴(yán)格校驗(yàn)了alg字段。如果服務(wù)器代碼里寫(xiě)死了algorithms[RS256]這個(gè)攻擊就不成立。所以拿到一道JWT題先試算法混淆不行再想別的路子。2.3 弱密鑰爆破當(dāng)HS256的密鑰太簡(jiǎn)單時(shí)如果算法混淆走不通第二招就是爆破HS256的密鑰。HS256是對(duì)稱(chēng)加密簽名和驗(yàn)證用的是同一個(gè)密鑰。如果這個(gè)密鑰設(shè)得太弱比如就是secret、123456、password這種那用字典跑一遍就能猜出來(lái)。我常用的工具是hashcat配合一個(gè)專(zhuān)門(mén)的JWT字典。命令大概長(zhǎng)這樣hashcat -m 16500 -a 0 token.txt jwt_wordlist.txt其中-m 16500是hashcat里JWT的哈希模式token.txt里放完整的JWT字符串jwt_wordlist.txt是字典文件。hashcat會(huì)自動(dòng)提取簽名部分進(jìn)行爆破。如果字典夠好幾秒鐘就能出結(jié)果。除了hashcat還有一個(gè)專(zhuān)門(mén)針對(duì)JWT的爆破工具叫jwt_tool用起來(lái)更直觀python3 jwt_tool.py token -C -d jwt_wordlist.txt-C表示crack模式-d指定字典。這個(gè)工具還會(huì)自動(dòng)嘗試一些常見(jiàn)的弱密鑰比如空密鑰、none算法等。這道題我兩種方法都試了。算法混淆直接成功了所以爆破只是作為備用驗(yàn)證。但我在本地搭環(huán)境測(cè)試的時(shí)候發(fā)現(xiàn)很多CTF題目的JWT密鑰就是從rockyou.txt的前1000行里取的所以準(zhǔn)備一個(gè)精簡(jiǎn)但高質(zhì)量的字典非常重要。我自己的字典里包含了常見(jiàn)的英文單詞、數(shù)字組合、以及一些CTF中經(jīng)常出現(xiàn)的字符串比如ctf、flag、admin、secret這些。2.4 none算法攻擊一個(gè)已經(jīng)過(guò)時(shí)但偶爾還能碰到的漏洞還有一種更古老的攻擊方式叫none算法攻擊。JWT規(guī)范里允許alg設(shè)為none表示不簽名。如果服務(wù)器沒(méi)有禁用這個(gè)算法攻擊者可以把a(bǔ)lg改成none然后把簽名部分留空服務(wù)器就會(huì)直接接受這個(gè)Token。這個(gè)漏洞在早期的JWT庫(kù)中很常見(jiàn)現(xiàn)在大部分庫(kù)都默認(rèn)禁用了none算法。但在一些老系統(tǒng)或者自己手寫(xiě)的JWT實(shí)現(xiàn)中仍然可能碰到。我在這道題里試了一下服務(wù)器直接返回了簽名驗(yàn)證失敗說(shuō)明none算法已經(jīng)被禁用了。不過(guò)我還是建議你在遇到JWT題的時(shí)候把none算法作為第一個(gè)嘗試因?yàn)椴僮髯詈?jiǎn)單把Header改成{alg:none,typ:JWT}Payload改成你想要的內(nèi)容簽名部分直接刪掉或者留一個(gè)空字符串然后發(fā)過(guò)去。如果服務(wù)器接受了那就是秒殺。2.5 JWT安全測(cè)試的完整檢查清單做了這么多JWT題我總結(jié)了一個(gè)檢查清單拿到Token之后按順序試一遍基本能覆蓋大部分漏洞檢查項(xiàng)操作方法成功標(biāo)志none算法改alg為none刪除簽名服務(wù)器接受Token算法混淆RS256改HS256用公鑰簽名服務(wù)器接受Token弱密鑰爆破hashcat或jwt_tool跑字典爆破出密鑰kid注入修改kid字段為文件路徑或SQL注入服務(wù)器讀取指定文件或報(bào)錯(cuò)jku/x5u篡改修改jku指向自己的JWKS服務(wù)器從攻擊者服務(wù)器獲取密鑰過(guò)期時(shí)間篡改修改exp字段為未來(lái)時(shí)間服務(wù)器接受過(guò)期Token這個(gè)清單不是萬(wàn)能的但覆蓋了CTF中90%以上的JWT考點(diǎn)。我個(gè)人的習(xí)慣是先試none再試算法混淆然后爆破最后才考慮kid注入和jku篡改這些稍微復(fù)雜一點(diǎn)的。3. Python pickle反序列化漏洞深度解析3.1 pickle是什么為什么它這么危險(xiǎn)pickle是Python標(biāo)準(zhǔn)庫(kù)里的一個(gè)模塊用來(lái)把Python對(duì)象序列化成字節(jié)流也可以把字節(jié)流反序列化回Python對(duì)象。它的設(shè)計(jì)目標(biāo)是讓Python對(duì)象能夠方便地存儲(chǔ)和傳輸比如你可以把一個(gè)字典、一個(gè)列表、甚至一個(gè)自定義類(lèi)的實(shí)例直接pickle成二進(jìn)制數(shù)據(jù)存到文件里或者通過(guò)網(wǎng)絡(luò)發(fā)出去接收方unpickle之后就能得到原來(lái)的對(duì)象。聽(tīng)起來(lái)很方便但問(wèn)題在于pickle的反序列化過(guò)程可以執(zhí)行任意代碼。這不是bug而是設(shè)計(jì)如此。pickle的文檔里明確寫(xiě)了“永遠(yuǎn)不要unpickle不可信的數(shù)據(jù)”但很多開(kāi)發(fā)者要么沒(méi)看到這句話(huà)要么覺(jué)得自己的數(shù)據(jù)來(lái)源是可信的結(jié)果就出了大問(wèn)題。pickle為什么能執(zhí)行代碼因?yàn)閜ickle的字節(jié)流實(shí)際上是一系列操作碼opcode這些操作碼告訴Python解釋器如何重建對(duì)象。其中有一個(gè)操作碼叫REDUCE它允許調(diào)用一個(gè)可調(diào)用對(duì)象callable并傳入?yún)?shù)。攻擊者可以構(gòu)造一個(gè)字節(jié)流讓REDUCE調(diào)用os.system或者subprocess.check_output之類(lèi)的函數(shù)從而執(zhí)行任意命令。我在這道CTF題里拿到的入口是一個(gè)看起來(lái)像Base64字符串的參數(shù)解碼之后是一串二進(jìn)制數(shù)據(jù)。用pickletools分析了一下發(fā)現(xiàn)里面確實(shí)有REDUCE操作碼。這道題的意圖很明顯構(gòu)造一個(gè)惡意的pickle payload讓服務(wù)器反序列化時(shí)執(zhí)行命令讀取flag文件。3.2 手工構(gòu)造pickle payload的完整過(guò)程構(gòu)造pickle payload有兩種方式一種是直接用Python的pickle模塊序列化一個(gè)惡意對(duì)象另一種是手工編寫(xiě)pickle字節(jié)碼。第一種方式更簡(jiǎn)單但需要你定義一個(gè)類(lèi)并在類(lèi)的__reduce__方法里指定要執(zhí)行的命令。第二種方式更靈活可以精確控制每一個(gè)操作碼。先講第一種方式。假設(shè)我們要執(zhí)行cat /flag可以這樣寫(xiě)import pickle import os class Exploit: def __reduce__(self): return (os.system, (cat /flag,)) payload pickle.dumps(Exploit()) print(payload)這段代碼定義了一個(gè)Exploit類(lèi)實(shí)現(xiàn)了__reduce__方法。__reduce__方法告訴pickle在反序列化時(shí)應(yīng)該調(diào)用什么函數(shù)、傳什么參數(shù)。這里返回的是(os.system, (cat /flag,))意味著反序列化時(shí)會(huì)執(zhí)行os.system(cat /flag)。把生成的payload用Base64編碼然后作為參數(shù)發(fā)給服務(wù)器如果服務(wù)器直接pickle.loads了這個(gè)參數(shù)命令就會(huì)執(zhí)行。但實(shí)際CTF題往往不會(huì)這么簡(jiǎn)單。有些題目會(huì)對(duì)payload做過(guò)濾比如禁止某些操作碼或者限制反序列化的類(lèi)。這時(shí)候就需要手工構(gòu)造字節(jié)碼繞過(guò)過(guò)濾。手工構(gòu)造pickle字節(jié)碼需要了解常用的操作碼。我列幾個(gè)最核心的操作碼含義用途cGLOBAL壓入一個(gè)全局對(duì)象如os.system(MARK壓入一個(gè)標(biāo)記用于構(gòu)建元組或列表tTUPLE從標(biāo)記處構(gòu)建元組RREDUCE調(diào)用可調(diào)用對(duì)象.STOP結(jié)束反序列化SSTRING壓入一個(gè)字符串VUNICODE壓入一個(gè)Unicode字符串一個(gè)最簡(jiǎn)單的執(zhí)行命令的payload字節(jié)碼大概是這樣的cos system (Scat /flag tR.逐行解釋c操作碼后面跟模塊名和函數(shù)名這里壓入os.systemS壓入字符串cat /flag(壓入一個(gè)標(biāo)記t從標(biāo)記處構(gòu)建元組得到(cat /flag,)R調(diào)用os.system并傳入元組作為參數(shù).結(jié)束。用Python的pickle.loads直接加載這段字節(jié)碼就會(huì)執(zhí)行命令。我實(shí)測(cè)的時(shí)候把這段字節(jié)碼用Base64編碼后發(fā)給服務(wù)器服務(wù)器返回了flag的內(nèi)容。3.3 繞過(guò)常見(jiàn)過(guò)濾的幾種思路CTF題目不會(huì)讓你這么輕松就拿到flag。常見(jiàn)的過(guò)濾手段包括禁用os模塊、禁用system函數(shù)、過(guò)濾關(guān)鍵詞如cat、flag、system等。下面是我遇到過(guò)的幾種過(guò)濾和對(duì)應(yīng)的繞過(guò)方法。第一種禁用os.system??梢杂胹ubprocess.check_output代替或者用platform.popen。如果這些都被禁了還可以用builtins.exec來(lái)執(zhí)行Python代碼比如exec(import os; os.system(cat /flag))。第二種過(guò)濾關(guān)鍵詞。比如過(guò)濾了cat可以用c\at或者cat拼接或者用通配符c?t、c*。如果過(guò)濾了flag可以用fla?或者f*。在Linux shell里這些通配符都會(huì)被展開(kāi)。第三種禁用REDUCE操作碼。有些題目會(huì)檢查payload里是否包含R操作碼。這時(shí)候可以用其他方式執(zhí)行代碼比如利用__setstate__或者_(dá)_reduce_ex__。不過(guò)這種繞過(guò)比較復(fù)雜需要對(duì)pickle的協(xié)議有深入理解。第四種限制反序列化的類(lèi)。有些題目會(huì)用一個(gè)白名單來(lái)限制可以反序列化的類(lèi)。這時(shí)候需要找到白名單里的某個(gè)類(lèi)看它的__reduce__或者_(dá)_setstate__方法是否可以被利用。這種題難度較高通常需要結(jié)合具體的類(lèi)實(shí)現(xiàn)來(lái)分析。我在這道題里遇到的過(guò)濾是禁用了os模塊。我的繞過(guò)方法是使用subprocess模塊import pickle import subprocess class Exploit: def __reduce__(self): return (subprocess.check_output, ([cat, /flag],)) payload pickle.dumps(Exploit())subprocess.check_output的返回值是命令的輸出但pickle反序列化時(shí)不會(huì)打印返回值所以這個(gè)payload執(zhí)行了命令但看不到結(jié)果。要看到結(jié)果需要讓命令的輸出以某種方式返回給客戶(hù)端比如寫(xiě)入一個(gè)Web可訪問(wèn)的文件或者通過(guò)DNS外帶。這道題的環(huán)境里我把flag寫(xiě)到了/tmp目錄下然后通過(guò)另一個(gè)文件讀取漏洞拿到了內(nèi)容。3.4 pickle反序列化的實(shí)際利用場(chǎng)景pickle反序列化漏洞不僅僅出現(xiàn)在CTF里在實(shí)際的Web應(yīng)用中也時(shí)有發(fā)生。常見(jiàn)的場(chǎng)景包括Session存儲(chǔ)有些框架把session數(shù)據(jù)pickle之后存在客戶(hù)端Cookie里如果服務(wù)端直接unpickle Cookie內(nèi)容攻擊者就可以偽造session執(zhí)行代碼。緩存系統(tǒng)Redis、Memcached等緩存里存儲(chǔ)的pickle數(shù)據(jù)如果緩存被污染或者攻擊者能寫(xiě)入緩存反序列化時(shí)就會(huì)觸發(fā)漏洞。消息隊(duì)列RabbitMQ、Celery等消息隊(duì)列中傳輸?shù)膒ickle數(shù)據(jù)如果消息來(lái)源不可信同樣存在風(fēng)險(xiǎn)。機(jī)器學(xué)習(xí)模型PyTorch的模型文件本質(zhì)上就是pickle加載不可信的模型文件等于執(zhí)行任意代碼。我在實(shí)際工作中遇到過(guò)一個(gè)是Django應(yīng)用把session存在Redis里序列化方式用的pickle。攻擊者通過(guò)SSRF漏洞寫(xiě)入了惡意的Redis鍵值然后觸發(fā)session讀取最終實(shí)現(xiàn)了遠(yuǎn)程代碼執(zhí)行。這個(gè)鏈條比較長(zhǎng)但每一步都不復(fù)雜核心就是pickle反序列化這個(gè)點(diǎn)。提示如果你在代碼審計(jì)中看到pickle.loads、pickle.load、cPickle.loads這些函數(shù)并且輸入?yún)?shù)來(lái)自用戶(hù)可控的地方基本可以判定存在反序列化漏洞。修復(fù)方法很簡(jiǎn)單換成JSON序列化或者用HMAC對(duì)數(shù)據(jù)進(jìn)行簽名驗(yàn)證。3.5 pickle利用的常見(jiàn)問(wèn)題與排查在實(shí)際構(gòu)造pickle payload的過(guò)程中我踩過(guò)不少坑這里整理幾個(gè)最常見(jiàn)的問(wèn)題和解決方法。問(wèn)題一payload發(fā)送后服務(wù)器沒(méi)有執(zhí)行命令。可能的原因有很多payload沒(méi)有正確編碼、服務(wù)器沒(méi)有調(diào)用pickle.loads、命令執(zhí)行了但沒(méi)有回顯。排查方法是先用一個(gè)簡(jiǎn)單的命令比如sleep 5看服務(wù)器響應(yīng)時(shí)間是否變長(zhǎng)。如果變長(zhǎng)了說(shuō)明命令執(zhí)行了只是沒(méi)有回顯。這時(shí)候需要想辦法外帶數(shù)據(jù)。問(wèn)題二payload里的字符串包含特殊字符導(dǎo)致解析失敗。pickle字節(jié)碼對(duì)字符串的長(zhǎng)度和編碼有嚴(yán)格要求。如果字符串里有換行符或者非ASCII字符需要特別處理。我通常用pickle.dumps生成payload而不是手工拼接這樣可以避免大部分編碼問(wèn)題。問(wèn)題三服務(wù)器使用了pickle的更高協(xié)議版本。pickle有多個(gè)協(xié)議版本從0到5。不同版本的字節(jié)碼格式不同。如果服務(wù)器用的是協(xié)議2而你用協(xié)議0構(gòu)造payload可能無(wú)法正確反序列化。解決方法是在pickle.dumps里指定protocol參數(shù)或者用pickletools分析服務(wù)器返回的數(shù)據(jù)來(lái)確定協(xié)議版本。問(wèn)題四命令執(zhí)行了但找不到flag文件。這時(shí)候需要先執(zhí)行l(wèi)s /和find / -name *flag*來(lái)定位flag文件的位置。有些題目會(huì)把flag放在環(huán)境變量里這時(shí)候需要用echo $FLAG或者env來(lái)讀取。下面是一個(gè)我常用的pickle payload生成腳本支持指定命令和協(xié)議版本import pickle import base64 import sys class Exploit: def __reduce__(self): return (eval, (__import__(os).popen(%s).read() % sys.argv[1],)) payload pickle.dumps(Exploit(), protocol0) print(base64.b64encode(payload).decode())這個(gè)腳本用eval來(lái)執(zhí)行命令好處是可以直接拿到命令輸出的字符串。但eval在很多環(huán)境里被禁用了所以實(shí)際使用時(shí)需要根據(jù)題目環(huán)境調(diào)整。4. 兩道題背后的通用方法論4.1 拿到一道Web題時(shí)的標(biāo)準(zhǔn)排查流程做了40多天的CTF我逐漸形成了一套固定的排查流程。拿到一道Web題先看URL和參數(shù)判斷大概是什么類(lèi)型的漏洞。然后按以下順序嘗試查看源代碼和響應(yīng)頭很多題目的提示藏在HTML注釋、響應(yīng)頭的自定義字段、或者robots.txt里。嘗試常見(jiàn)入口SQL注入、文件包含、命令注入、SSRF、XXE這些是最高頻的考點(diǎn)。檢查認(rèn)證機(jī)制如果有登錄功能看session和Token是怎么生成的。JWT題通常在這一步能發(fā)現(xiàn)線(xiàn)索。檢查序列化數(shù)據(jù)如果參數(shù)值看起來(lái)像Base64或者二進(jìn)制解碼看看是不是pickle、PHP serialize、Java serialize等格式。嘗試?yán)@過(guò)過(guò)濾如果發(fā)現(xiàn)有WAF或者關(guān)鍵詞過(guò)濾嘗試編碼繞過(guò)、大小寫(xiě)繞過(guò)、拼接繞過(guò)等。這套流程不是死的但能幫你在沒(méi)有思路的時(shí)候快速定位方向。我個(gè)人的經(jīng)驗(yàn)是CTF題目通常不會(huì)考太偏門(mén)的知識(shí)點(diǎn)大部分題目都是圍繞幾個(gè)核心漏洞類(lèi)型出的。把JWT和pickle這兩個(gè)點(diǎn)吃透至少能解決20%的Web題。4.2 工具鏈的搭建與使用建議工欲善其事必先利其器。我平時(shí)做CTF用的工具不多但每一個(gè)都用到極致Burp Suite抓包改包必備社區(qū)版免費(fèi)功能足夠。jwt_toolJWT專(zhuān)用工具支持爆破、算法混淆、kid注入等多種攻擊。pickletoolsPython自帶用來(lái)分析pickle字節(jié)碼。hashcat爆破各種哈希包括JWT的HMAC簽名。CyberChef在線(xiàn)編解碼工具支持Base64、Hex、URL編碼等非常方便。這些工具不需要全部精通但至少要知道每個(gè)工具能做什么遇到問(wèn)題時(shí)知道該用哪個(gè)。我見(jiàn)過(guò)一些新手遇到JWT題不知道有jwt_tool這個(gè)工具手工去算HMAC浪費(fèi)了大量時(shí)間。工具的價(jià)值在于讓你把精力集中在思路上而不是重復(fù)勞動(dòng)上。4.3 從CTF到實(shí)際工作的能力遷移很多人覺(jué)得CTF和實(shí)際工作脫節(jié)我的看法恰恰相反。CTF里學(xué)到的漏洞原理和利用思路在實(shí)際滲透測(cè)試和代碼審計(jì)中直接就能用。JWT的算法混淆、pickle的反序列化、SQL注入的繞過(guò)技巧這些在實(shí)際項(xiàng)目中都是真實(shí)存在的風(fēng)險(xiǎn)點(diǎn)。區(qū)別在于實(shí)際工作中的系統(tǒng)更復(fù)雜漏洞可能藏在多層調(diào)用之后需要你有更強(qiáng)的代碼閱讀能力和調(diào)試能力。但核心的漏洞原理是一樣的。我在實(shí)際項(xiàng)目中遇到過(guò)一個(gè)JWT弱密鑰的問(wèn)題用的就是CTF里學(xué)到的爆破方法幾分鐘就拿到了管理員權(quán)限??蛻?hù)當(dāng)時(shí)很驚訝覺(jué)得怎么可能這么快其實(shí)只是因?yàn)樗麄兊拿荑€就是companyname2023字典里剛好有。pickle反序列化在實(shí)際工作中相對(duì)少見(jiàn)但一旦遇到就是高危漏洞。我建議你在CTF里把pickle的利用方式練熟包括手工構(gòu)造字節(jié)碼、繞過(guò)過(guò)濾、外帶數(shù)據(jù)等技巧。這些技能在關(guān)鍵時(shí)刻能派上大用場(chǎng)。5. 我踩過(guò)的坑和給你的建議5.1 JWT部分的三個(gè)坑第一個(gè)坑PyJWT版本問(wèn)題。我一開(kāi)始用最新版的PyJWT做算法混淆結(jié)果一直報(bào)錯(cuò)說(shuō)密鑰格式不對(duì)。后來(lái)查了文檔才發(fā)現(xiàn)PyJWT 2.x版本增加了算法混淆的防護(hù)不允許用RSA公鑰做HMAC密鑰。換成1.7.1版本之后問(wèn)題解決。所以如果你在做算法混淆時(shí)遇到莫名其妙的錯(cuò)誤先檢查庫(kù)的版本。第二個(gè)坑Base64Url編碼和標(biāo)準(zhǔn)Base64的區(qū)別。JWT用的是Base64Url編碼把標(biāo)準(zhǔn)Base64里的和/替換成了-和_并且去掉了末尾的。我手工構(gòu)造Token的時(shí)候一開(kāi)始忘了做這個(gè)替換導(dǎo)致服務(wù)器解析失敗。后來(lái)寫(xiě)了一個(gè)小函數(shù)專(zhuān)門(mén)處理這個(gè)轉(zhuǎn)換就再也沒(méi)出過(guò)問(wèn)題。第三個(gè)坑時(shí)間戳問(wèn)題。JWT的Payload里通常有iat簽發(fā)時(shí)間和exp過(guò)期時(shí)間。如果你構(gòu)造的Token里exp是過(guò)去的時(shí)間服務(wù)器會(huì)直接拒絕根本不走到簽名驗(yàn)證那一步。我一開(kāi)始沒(méi)注意這個(gè)改了半天簽名都沒(méi)用后來(lái)把exp改成未來(lái)時(shí)間就好了。5.2 pickle部分的兩個(gè)坑第一個(gè)坑協(xié)議版本不匹配。我用pickle.dumps生成payload時(shí)默認(rèn)用的是協(xié)議4或5但服務(wù)器上的Python版本比較老只支持到協(xié)議2。結(jié)果payload發(fā)過(guò)去之后服務(wù)器直接報(bào)錯(cuò)。解決方法是在pickle.dumps里顯式指定protocol0協(xié)議0是ASCII格式的兼容性最好。第二個(gè)坑命令回顯問(wèn)題。我構(gòu)造的payload執(zhí)行了命令但服務(wù)器沒(méi)有任何返回。一開(kāi)始我以為payload沒(méi)生效后來(lái)用sleep命令測(cè)試發(fā)現(xiàn)確實(shí)執(zhí)行了只是os.system的返回值不會(huì)出現(xiàn)在HTTP響應(yīng)里。解決方法是用subprocess.check_output獲取命令輸出然后通過(guò)raise Exception把輸出拋出來(lái)這樣錯(cuò)誤信息里就會(huì)包含命令結(jié)果?;蛘甙呀Y(jié)果寫(xiě)到Web目錄下的文件里再通過(guò)瀏覽器訪問(wèn)。5.3 給正在刷CTF的朋友的幾句實(shí)話(huà)刷CTF最大的誤區(qū)是只看WriteUp不動(dòng)手。我見(jiàn)過(guò)很多人看別人的WriteUp覺(jué)得很簡(jiǎn)單自己一上手就卡住。原因很簡(jiǎn)單看的時(shí)候你是在被動(dòng)接受信息動(dòng)手的時(shí)候你是在主動(dòng)解決問(wèn)題兩者的難度完全不一樣。我的建議是每道題至少自己先嘗試30分鐘實(shí)在沒(méi)思路再看提示看完提示再自己復(fù)現(xiàn)一遍。另外不要追求刷題數(shù)量。一天一道題但每道題都吃透比一天十道題但都是抄WriteUp強(qiáng)得多。我做到第41天前30天基本都是在打基礎(chǔ)最近這10天才開(kāi)始接觸JWT、pickle、SSTI這些稍微高級(jí)一點(diǎn)的考點(diǎn)。基礎(chǔ)打牢了后面的路會(huì)越走越順。最后說(shuō)一個(gè)我自己的習(xí)慣每做完一道題我會(huì)寫(xiě)一段簡(jiǎn)短的筆記記錄這道題的核心考點(diǎn)、我踩的坑、以及如果下次遇到類(lèi)似的題應(yīng)該怎么入手。這個(gè)習(xí)慣讓我在遇到重復(fù)考點(diǎn)時(shí)能快速反應(yīng)不用從頭開(kāi)始想。筆記不用寫(xiě)得多正式幾句話(huà)就行關(guān)鍵是記錄你自己的思考過(guò)程而不是抄別人的答案。