復盤)
前一陣做內網(wǎng)口令安全評估我寫了個批量口令噴灑腳本對著三十幾臺新到的網(wǎng)絡設備跑。跑到半程結果讓我一下子清醒A設備的admin賬號密碼明明是對的系統(tǒng)卻返回認證失敗B設備根本沒測過幾次guest日志里卻顯示guest因為連續(xù)錯誤被鎖定。我第一反應是腳本的線程把結果寫串了于是翻代碼、看日志、加鎖、重跑折騰到凌晨發(fā)現(xiàn)每條輸出對應的IP都對得上。腳本是好的問題出在測試鏈路本身。那段時間我滿腦子就四個字共享狀態(tài)。后面發(fā)生的事情讓我把隔離這件事從網(wǎng)絡到容器再到電路完整走了一遍最后還揭開了一個黑盒設備的一角。這篇文章就是完整的過程復盤對剛入行做安全測試、負責環(huán)境搭建或者寫并發(fā)腳本的工程師應該都能找到點有用的東西。1. 口令實驗撞上的串號事故測試結果為什么會互相污染1.1 實驗場景35臺設備一套集中認證這次任務是給公司新購入的一批網(wǎng)絡設備做口令安全評估。設備共35臺都是同一個廠商同一個型號管理端統(tǒng)一接入一臺用了很多年的認證服務器走RADIUS做AAA認證。所謂AAA就是認證、授權、計費簡單說就是設備自己不做密碼判斷把用戶名和密碼轉發(fā)給認證服務器認證服務器說通過設備才放行。為了不干擾正常業(yè)務網(wǎng)段管理員要求所有測試流量經(jīng)過跳板機進出。跳板機做了源NAT所有從測試機發(fā)過去的請求源IP都會被改寫成同一個地址。這一步本來是為了管理方便誰也想不到它成了后續(xù)所有怪象的伏筆。測試腳本用Python寫的線程池開8個worker每個worker負責若干臺設備的Web登錄頁往登錄接口提交用戶名和口令組合再根據(jù)HTTP響應判斷是否成功。腳本本身很普通扔到任何一臺機器上都能跑。問題也正出在這個普通上它太依賴一個假設——每個請求都是獨立的。1.2 三種詭異現(xiàn)象誤報、虛放、誤鎖事故的現(xiàn)象有三類我很清楚地列在文檔里。第一是誤報。A設備用正確的admin口令登錄Web頁面明確提示用戶名或密碼錯誤。手動用瀏覽器登錄同一臺設備同樣的口令一登就進。設備和口令都沒問題。第二是虛放。B設備用錯誤的guest口令登錄居然偶爾能跳轉到管理主界面。這意味著有些錯誤的嘗試被認證服務器判定為通過這是比誤報更嚴重的事。第三是誤鎖。C設備的admin賬號腳本只提交過兩次錯誤口令但設備日志顯示它已經(jīng)觸發(fā)了連續(xù)失敗鎖定策略賬號直接被鎖。奇怪的是三種現(xiàn)象不是同時出現(xiàn)在每臺設備上而是隨機分布在不同設備和不同時間點。這讓我最初完全沒有規(guī)律可循。1.3 三條排查路線程、單臺、并發(fā)驗證排查第一步審查腳本。我把所有并發(fā)線程改成串行執(zhí)行一次只跑一臺設備。結果很干凈所有正確口令全通過錯誤口令全被拒鎖定策略不再誤觸發(fā)。這一步說明設備和口令庫本身沒有毛病。第二步恢復并發(fā)但只保留一臺設備。結果同樣正常。說明單純的高并發(fā)也不會導致問題。第三步恢復并發(fā)且保留全部35臺設備同時把線程數(shù)提升到16個。問題立刻回來了。于是我的懷疑范圍縮小到一個很具體的條件并發(fā)加多臺設備同時向同一臺認證服務器發(fā)起認證。為了確認網(wǎng)絡層的因素我在跳板機上抓包。抓包結果很有趣RADIUS請求正常發(fā)出響應也正常返回但對不上的不是報文本身而是請求和響應之間的事務對應關系。簡單說A設備發(fā)出的認證請求拿到的是B設備認證請求的響應。到這里我已經(jīng)非常確定問題出在某種共享狀態(tài)上只是不知道它藏在哪一層。2. 共享狀態(tài)的三種藏身之處連接池、全局變量與設備固件2.1 共享狀態(tài)到底是什么共享狀態(tài)說得直白一點就是多個執(zhí)行單元共同讀寫同一份數(shù)據(jù)。寫單線程程序時這份數(shù)據(jù)是你的私有變量一旦開了多線程、多進程或者多臺設備同時往同一個服務發(fā)請求這份數(shù)據(jù)就變成了公共資源誰都能碰誰都會改它。我習慣拿公寓水電表打比方如果每家每戶有獨立分表誰用了多少電一清二楚但整棟樓只有一塊總表某一戶開了個大功率空調整棟樓的賬單都會跳。認證服務器里那張會話表就是那塊總表。2.2 藏身點之一客戶端連接池和會話對象第一類常見的共享狀態(tài)在客戶端。很多剛寫并發(fā)腳本的工程師第一反應是共享requests.Session因為Session里帶著Cookie和連接池多線程共用同一個Session時如果服務端按Keep-Alive連接緩存認證上下文線程A的請求就可能被線程B繼承了狀態(tài)。這種問題在單純訪問公開接口時不容易暴露因為無狀態(tài)接口不依賴上下文。但在口令實驗里登錄接口天然依賴會話一旦服務端針對連接而不是針對用戶維護狀態(tài)串號就來了。我早年被這種問題坑過所以這次一開始也懷疑過它但很快被排除了就算把腳本改成每個線程獨立Session問題依然在。2.3 藏身點之二服務端全局注冊表第二類在服務端。很多老系統(tǒng)為了節(jié)省內存會把會話注冊表做成一個全局的單例對象用哈希表保存所有在線會話。如果Key設計不合理比如只按源IP區(qū)分那么來自同一源IP的并發(fā)請求就會共享同一個會話槽。偽代碼大致是這樣# 錯誤示范會話Key只按源IP區(qū)分 session_table[source_ip] session_context更合理的Key應該組合源IP、目標設備IP、用戶名甚至隨機序列號。這也是我在排查過程中最懷疑的地方因為我們的場景里所有請求都來自同一個源IP和這種錯誤Key的特征完全吻合。2.4 藏身點之三設備固件里看不見的全局會話表最后就是這次真正的主角。那臺認證服務器是閉源的文檔只寫了支持并發(fā)認證請求但沒有說明內部如何管理會話。它的固件里極大概率維護了一個當前活動會話的全局變量或者一張以源IP為索引的會話表。新請求到達時如果源IP已經(jīng)有一個活動會話新請求會直接把舊會話頂?shù)簟UJ證響應生成時固件讀取當前會話槽自然把對應另一個請求的結果返回回去。這樣一來任何來自同一IP的并發(fā)請求響應都可能被裝到別的請求頭上。用一句話概括它把一個源IP當成了一個客戶端并且默認一個客戶端不會有并發(fā)認證。2.5 為什么口令實驗里共享狀態(tài)的殺傷力被放大如果只是普通接口的并發(fā)測試這種串號頂多讓返回數(shù)據(jù)對不上重試兩次就完事。但口令實驗里共享狀態(tài)帶來的后果嚴重得多正確口令被記錄為失敗你會誤判設備存在弱口令問題。錯誤口令被判定為通過弱口令問題反而被掩蓋。多次錯配會觸發(fā)服務端的賬號鎖定策略直接把合法賬號鎖死影響業(yè)務。如果共享會話中偶然殘留其他用戶的口令信息還可能在日志里留下敏感數(shù)據(jù)。所以當確認問題的性質后我?guī)缀鯖]有猶豫就確定了解決辦法的方向隔離。把不該共享的狀態(tài)拆開把測試流量從一口鍋里分到獨立灶臺上。3. 隔離方案的四個工程層次從VLAN到光耦的取舍那晚之后我把隔離手段按層次梳理了一遍發(fā)現(xiàn)一個很清晰的分界線從網(wǎng)絡域做隔離到容器資源隔離再到內核驅動清理最后是電路級的光耦隔離。每多走一層隔離更徹底成本也更高。下面把這四層詳細說一下。3.1 網(wǎng)絡域隔離VLAN劃分與ACL配置最便宜的隔離是網(wǎng)絡域隔離。把測試網(wǎng)段從生產(chǎn)網(wǎng)段中剝離開讓測試流量只可能到達目標設備。在實驗開始之前我就應該先把測試區(qū)劃到獨立VLAN可惜當時跳板機直接接入設備管理網(wǎng)省了這一步也就漏掉了第一層防護。配置VLAN和ACL的思路很簡單示例命令如下不同廠商語法有差異思路一致# 創(chuàng)建測試VLAN 100網(wǎng)段10.10.100.0/24 vlan 100 name test-net # 三層接口 interface vlanif 100 ip address 10.10.100.1 255.255.255.0 # ACL只允許測試網(wǎng)段訪問目標設備網(wǎng)段10.10.20.0/24 acl number 3001 rule 5 permit ip source 10.10.100.0 0.0.0.255 destination 10.10.20.0 0.0.0.255 rule 10 deny ip # 接入端口 interface g0/0/1 port link-type access port default vlan 100 traffic-filter inbound acl 3001這樣配置之后生產(chǎn)網(wǎng)段的流量進不到測試區(qū)測試區(qū)也訪問不到生產(chǎn)網(wǎng)段。代價是增加了網(wǎng)絡規(guī)劃工作量好處是萬一測試腳本失控爆炸半徑被限制在測試VLAN內。但這里必須說清楚VLAN隔離解決的是域的問題它可解決不了認證服務器內部的會話表問題。兩臺設備哪怕不在一個VLAN只要它們把認證請求發(fā)給同一個服務器且源IP相同會話照樣串。所以網(wǎng)絡域隔離只是第一步不是全部。3.2 容器資源隔離給每個測試進程一個獨立網(wǎng)絡空間我們的核心需求從認證服務器的視角看是讓35臺設備的并發(fā)請求看起來來自35個不同客戶端。最直接的實現(xiàn)方式就是用容器給每個測試進程分配獨立的網(wǎng)絡命名空間和獨立IP。當時我用的方案是Docker配合macvlan網(wǎng)絡# 創(chuàng)建基于VLAN 100的macvlan網(wǎng)絡 docker network create -d macvlan \ --subnet10.10.100.0/24 \ --gateway10.10.100.1 \ -o parenteth0.100 test-macvlan # 每臺設備一個容器分配獨立IP for i in $(seq 1 35); do docker run -d --name spray-$i \ --network test-macvlan \ --ip 10.10.100.$((i10)) \ spray-image --target 10.10.20.$i done每個容器擁有自己獨立的協(xié)議棧、路由表、ARP表發(fā)出的請求都會帶上自己的源IP。認證服務器看到的就不再是同一個IP下的高并發(fā)而是35個獨立IP的登錄請求。做完這一步之后串號現(xiàn)象立刻消失。容器隔離本質上是Linux內核的namespace機制在起作用。network namespace隔離了網(wǎng)絡棧PID namespace隔離了進程視圖。它比虛擬機輕量但也有一個隱含代價容器共享宿主內核。宿主機內核一旦被某個不兼容驅動污染容器網(wǎng)絡就可能集體出問題。這就到了下一層。3.3 內核與驅動隔離當隔離工具本身不干活我在搭容器網(wǎng)絡的時候正好撞上了這個問題。宿主機之前裝過一版第三方網(wǎng)絡準入客戶端它在內核里注冊了網(wǎng)絡過濾鉤子。macvlan給容器分配虛擬網(wǎng)卡時這個過濾鉤子把所有非本地流量都給攔了導致容器能啟動但網(wǎng)絡完全不通。排查過程比較痛苦因為現(xiàn)象是幾乎所有網(wǎng)絡命令都正常但容器一個包都發(fā)不出去。最終定位到驅動層處理方式如下# 查看已加載的沖突模塊 lsmod | grep access # 停掉依賴該驅動的服務 systemctl stop access-client # 從內核卸載模塊 rmmod access_driver # 永久禁用把模塊加入黑名單 echo blacklist access_driver /etc/modprobe.d/blacklist-test.conf update-initramfs -u reboot注意一點rmmod的時候如果模塊正被進程引用內核會拒絕卸載并提示Module in use。這時候強來不行得先停服務再卸載模塊。生產(chǎn)環(huán)境的服務器千萬別隨手執(zhí)行這些命令最好先確認模塊用途再在維護窗口操作。內核對驅動的不兼容很難從日志里直接看出來搭建實驗環(huán)境時最好從一開始就用干凈的宿主機尤其是做網(wǎng)絡實驗時。所謂內核隔離在實驗場景里其實就是要保證宿主內核沒有因為某個驅動被額外共享到影響所有容器。這是很多人容易忽略的一層。3.4 電路級隔離9600波特率下的光耦隔離選型隔離的最后一層在物理電路上。這次實驗里有一臺老設備網(wǎng)絡管理口已經(jīng)失效只能靠串口調試。我把USB轉串口接到筆記本上數(shù)據(jù)時斷時續(xù)偶爾還冒出亂碼。一測設備電源地和筆記本地之間存在將近2伏的壓差。地環(huán)路引起的漏電問題靠的就是電氣隔離來解決。光耦隔離的原理很直接輸入端把電平信號轉換成紅外光輸出端檢測到光之后再轉換成電平信號。輸入和輸出之間沒有電氣連接地環(huán)路被直接切斷。需要注意光耦隔離通信中波特率與器件延遲的關系至關重要選型時一定要算清楚。9600波特率是串口最常用的低速檔。按8N1格式一個bit的時長是1除以9600秒約104.17微秒。普通光耦PC817的傳播延遲典型值在4到10微秒加上上升下降時間一個bit周期內還有足夠的裕量完成翻轉所以隔離9600波特率的串口信號PC817勉強能用但邊沿質量比較差。我當時用的是PC817把光耦做成了一個很小的轉接板插在串口線上實驗數(shù)據(jù)湊合能用但如果要求信號波形更干凈我會換高速光耦。如果波特率提到115200一個bit只剩8.68微秒PC817的延遲就明顯不夠了必須上6N137這類高速光耦。6N137的傳播延遲小于100納秒適合幾Mbps到10Mbps的速率。我整理了一個簡單的選型表型號傳播延遲推薦速率典型用途PC8174-10微秒不超過9600波特率低速IO隔離、簡單串口隔離6N137小于100納秒最高10Mbps高速串口、RS232/RS485隔離TLP2361小于100納秒數(shù)十Mbps緊湊型高速隔離順帶提一句如果去看電源設計會發(fā)現(xiàn)還有非隔離高效率LCC拓撲、非隔離式Buck-Boost電路這類方案。它們的優(yōu)點是效率高、體積小、成本低壞處就是沒有電氣隔離調試時人和設備都直接暴露在危險電位下。隔離型方案加了一個變壓器或光耦成本高一點但故障時能切斷危險傳導路徑。這個取舍邏輯和軟件層隔離是一樣的多一層隔離多一點安全邊際代價是更多一點成本和復雜度。4. 黑盒一角會話串號背后的固件會話管理機制4.1 控制變量法鎖定元兇隔離方案上線之后我把完整測試流程重新跑了一遍。結果非常干凈所有正確口令全部一次通過錯誤口令全部被拒鎖定策略只在賬號真正連續(xù)多次失敗后才觸發(fā)。與之前唯一的差別就是源IP從1個變成了35個。我還沒敢直接下結論又做了一組對照實驗。這次故意把全部容器的源IP統(tǒng)一改成一個地址跑5分鐘后串號現(xiàn)象立刻復活。改回獨立IP現(xiàn)象消失。來來回回三次每次結果一致。到這里元兇被徹底鎖定認證服務器內部維護著一份以源IP為索引的共享會話表。4.2 抓包還原請求與響應的錯位過程為了把機制還原得更精確我在認證服務器的RADIUS端口和其中兩臺設備的網(wǎng)口上同時抓包。時間線是這樣的T1時刻源IP 10.10.100.11上的管理員用戶A發(fā)起認證請求認證服務器為它創(chuàng)建會話S1。 T2時刻源IP 10.10.100.11上的管理員用戶B發(fā)起認證請求。固件發(fā)現(xiàn)該源IP已有活動會話用新會話S2覆蓋了S1。 T3時刻認證結果返回。響應頭里帶的會話標識是S2對應的用戶B的認證結果。于是用戶A拿到的回執(zhí)其實是用戶B的結果。用文字表達就是這樣請求A正確口令 → 會話S1 → 被請求B覆蓋 請求B錯誤口令 → 會話S2 → 響應返回到請求A的位置S1的結果被S2覆蓋丟失S2的結果被A設備讀取。于是A設備的正確口令失敗了B設備的錯誤口令卻通過了。鎖定策略的誤觸發(fā)也是一樣的邏輯某次認證請求明明沒失敗但被錯誤響應標記為失敗計數(shù)累加最終鎖賬號。4.3 為什么說這只是黑盒的一角說它是黑盒是因為這臺AAA服務器是閉源的老設備官方文檔只寫了支持并發(fā)認證請求完全沒提內部的會話管理方式。從外部只能看到請求和響應的報文內部固件怎么組織狀態(tài)是看不見的。我所有排查本質上都是在黑盒外部做行為推斷如果不是隔離實驗把變量砍干凈了我可能到現(xiàn)在還在懷疑腳本?,F(xiàn)在我能確切知道一個內部行為它把一個源IP當成了一個活動客戶端同一源IP的活動會話只有一份。但我不知道它內部是用全局變量、單向鏈表還是某種哈希表來實現(xiàn)的也不知道它還藏著多少類似限制。盒子里只露出了一條縫看到了一根線但這一根線足以解釋所有奇怪現(xiàn)象。4.4 發(fā)現(xiàn)帶來的直接改變這個黑盒行為顛覆了我對這類老認證服務器并發(fā)支持的信任。之后的測試環(huán)境規(guī)范里我加了一條硬性要求任何涉及AAA認證的口令實驗都必須為并發(fā)請求分配多個源IP禁止用單一跳板源IP做高并發(fā)噴灑。能走獨立管理口的目標設備盡量分開管理不能分開的就用容器或者虛擬網(wǎng)絡把源IP拆散。我也給廠商提了問題單附上了對照實驗記錄和抓包時間線。對方后來回復產(chǎn)品固件的確為簡化會話管理做了這個設計同一來源IP只維護一個活動會話且官網(wǎng)文檔沒有明確寫出該限制。這個回復讓我挺感慨你以為在測試一個黑盒最后發(fā)現(xiàn)黑盒里裝的是人家當初一拍腦袋做的簡化方案。5. 可復現(xiàn)的口令隔離實驗三步走與環(huán)境清單5.1 第零步畫一張共享點拓撲圖動手之前先把拓撲畫清楚。別嫌麻煩排查時這張圖能幫你快速鎖定變量。拓撲圖上至少要標出三類信息流量路徑測試機到跳板、跳板到設備網(wǎng)段、設備到認證服務器的完整路徑。NAT點哪里做了源地址轉換改成了什么地址。狀態(tài)存儲點認證服務器、會話管理組件、連接池等可能保存狀態(tài)的位置。我當時就是因為跳板機上那個源NAT沒標出來排查時多繞了一大圈。如果一開始就知道所有請求從認證服務器視角看都同源可能第一輪就能想到共享會話表。5.2 環(huán)境搭建VLAN、容器與測試工具按下面的清單準備環(huán)境基本可以覆蓋大多數(shù)口令評估實驗準備項配置說明測試機獨立Linux服務器建議有2個以上網(wǎng)口網(wǎng)絡隔離獨立VLAN 100測試網(wǎng)段10.10.100.0/24目標接入設備網(wǎng)段10.10.20.0/24與測試VLAN可互通容器網(wǎng)絡Docker加macvlan每個容器獨立IP測試腳本Python并發(fā)腳本每個線程或進程綁定獨立源IP抓包工具tcpdump或Wireshark客戶端與服務端同時抓容器網(wǎng)絡創(chuàng)建方式在3.2節(jié)給過直接抄作業(yè)即可。5.3 分階段執(zhí)行冒煙、基線、并發(fā)、驗證執(zhí)行流程我建議分成五個階段順序不要亂冒煙單線程、單容器只測一臺設備確認工具本身沒問題?;€串行跑完所有設備記下每臺設備的正確口令和弱口令基線。這一步是后續(xù)判斷并發(fā)是否串號的參照物。并發(fā)測試所有容器同時啟動每個容器獨立源IP按計劃做口令噴灑。復現(xiàn)實驗有意把所有容器源IP改成一個復跑一小段確認黑盒行為可復現(xiàn)為問題單留證據(jù)?;貧w驗證恢復獨立源IP再跑一遍確認修復有效。每一步做完把結果文件按設備IP命名歸檔避免后面混淆。5.4 踩坑清單這次實驗最大的五個坑癥狀根因解決辦法正確口令被拒絕認證服務器共享會話表被并發(fā)請求覆蓋多源IP并發(fā)不做單源高并發(fā)錯誤口令偶爾通過響應錯配到另一個請求抓包比對事務ID或會話ID賬號莫名鎖定鎖定策略被并發(fā)誤判觸發(fā)評估鎖定閾值控制測試頻率容器網(wǎng)卡反復掉線內核驅動與macvlan沖突卸載沖突模塊并加入黑名單串口數(shù)據(jù)亂碼設備地與筆記本地存在壓差串口線加光耦隔離這張表里的前四條在普通業(yè)務系統(tǒng)測試里也可能遇到。尤其是第五條看似是硬件問題本質還是共享地導致的隔離缺失。5.5 善后數(shù)據(jù)歸檔與問題反饋實驗結束后抓包文件、腳本版本、容器配置、結果數(shù)據(jù)全部歸檔命名按日期和實驗批次來??诹钤u估的結果數(shù)據(jù)非常敏感不要散落在臨時目錄測試結束后一并將容器銷毀、VLAN清理。如果需要向廠商反饋黑盒行為附上對照實驗時間線和抓包會話比任何文字描述都有說服力。那次實驗之后我把隔離狀態(tài)寫進了自己的測試環(huán)境設計清單。共享狀態(tài)本身不是壞事很多并發(fā)框架恰恰靠它才獲得高性能但在口令實驗里一旦狀態(tài)串了輕則測試結論報廢重則賬號被鎖、業(yè)務受損。我現(xiàn)在動手做任何測試之前第一件事都是盯著拓撲圖問自己哪些狀態(tài)是共享的哪些是可以在開始之前就先隔開的把源IP拆開、網(wǎng)絡域分開、驅動清理干凈、串口做物理隔離很多坑其實是可以在踩進去之前就繞開的。這比事后抓包省心太多了也順便把黑盒的一角看得更清楚了。