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