這一切始於一篇新聞報導。一家公司剛在我的城市投放了一批電動滑板車。大多數人看到的是一種新的城市交通方式。我則看到了一群連網裝置,運行著一個尚未被任何人深入研究的後端系統。
首先,我需要兩樣東西:這是哪家公司,以及該服務對一般使用者是如何運作的。公司名稱就在新聞報導中,快速的 Google 搜尋讓我找到了他們的網站,網站上清楚地說明了流程:
這對任何使用者來說都是「快樂路徑」。我想看看背後到底發生了什麼事。
我開始繪製與 electricscootercompany.com.br 相關的一切。子網域列舉(Subdomain enumeration)發現了不少,包括:
並非所有這些都是實際的應用程式。app、membro、vouchers 和 planos 都基本上顯示相同的頁面,只是將你導向應用程式商店。名稱很多,但沒有什麼新東西值得關注。
www.electricscootercompany.com.br:WordPress 行銷網站;
api.electricscootercompany.com.br:客戶端使用的 REST API;
painel.electricscootercompany.com.br:營運人員使用的 Angular 管理面板。
網站上沒有任何連結指向該管理面板。我僅僅是透過列舉才找到它。僅僅因為某樣東西沒有被連結,並不意味著它被鎖定了。
該管理面板對任何人都是開放的:一個生產環境的 Angular 應用程式。我下載了所有 32 個 JavaScript 程式碼塊,並深入研究了這些捆綁包,在那裡我發現了 83 個用於以下功能的端點:
憑證、交易和財務模組。
這告訴我目標有多麼誘人,但卻讓我一無所獲。我嘗試了約 38 個受保護的路由,都沒有有效的會話,每一個都給了我相同的回應:HTTP 401。
我嘗試的許多方法都無效:
直接路由存取:被驗證阻止。
未簽名的管理員 JWT:被後端拒絕。
篡改令牌聲明:沒有產生有效的會話。
登入時的 SQL 注入:對驗證欄位運行了 SQLMap,但沒有發現可注入的參數。
報表端點:php/report.php 返回了空的 500 錯誤。
經典的暴露檔案:.git、.env 和原始碼映射無法存取。
驗證在直接嘗試時似乎沒有問題。而應用程式已經為我指明了一條更容易的路:找到一個真實的使用者,然後去攻擊他們的密碼。
公開的 WordPress REST API 愉快地允許我列出作者:
回應給出了使用者 ID 1、公開名稱 admin、別名 electricscootercompany。這通常只是例行公事的 WordPress 列舉。在這裡,我可以將相同的識別符號用於營運面板。
登入根據我輸入的內容給出了不同的回應:
因此,我無需猜測 electricscootercompany 僅僅是一個部落格作者。後端直接告訴我,相同的身份也存在於營運系統中。這將一次通用的列舉轉變為一個具有極高價值的目標名單。
在確認使用者後,我對登入進行了暴力破解。沒有任何節流措施來限制重複的嘗試,最終找到了一個有效的密碼,於是成功登入了管理面板。
這部分真正解釋了根本原因。由於沒有真正的速率限制,一個已知的身份就足以將一個猜測循環變成一個有效的會話。
之後的一切都基於我在 Burp 中捕獲到的真實會話。JWT 解碼後是一個帳戶,具有:
等級 1000 是管理員角色。該令牌有效期約為 950 天,因此一旦獲取到一個會話,它將在數年內保持有效,除非有人刻意去終止它。
有了有效的會話,一切都瞬間改變了。過去給我 401 錯誤的路由現在返回了真實的營運數據。在捕獲中,我在 118 個獨特的宿主/方法/路徑組合中記錄了 168 個第一方請求。
車庫和營運基礎設施;
地理圍欄多邊形位於 /fronteiras/__coordenadas;
物聯網設備清單,包含識別符號和狀態;
單獨的車輛和整個車隊;
應用程式使用者,介面顯示超過 408,000 筆記錄;
公司、權限、交易、憑證批次和財務數據。
地圖將數位端與實際的地面營運連接起來:車庫的位置、停靠點的位置、散佈在城市中的車輛,以及每個車輛屬於哪個物聯網設備。
點擊標記會打開車輛的詳細資訊:代碼、類型以及它所在的位置(例如,停靠點):
一些回應足夠大,顯示了這有多大的存取權限:
Burp 會將較大的回應主體截斷在約 3 KB,所以我沒有保存每一個完整的響應,但我記錄的狀態、路徑和大小都是可靠的。
接下來我想知道管理面板是否只讀取數據,還是也能寫入。該會話顯示了針對以下目標的 PUT 請求:
我還可以為測試帳戶啟用免費乘車標誌。所以這不僅僅是讀取數據。我還可以更改業務規則和記錄。
車輛註冊表允許我查找車隊中的任何滑板車(介面顯示了數千筆記錄)並打開其編輯表單:
這個表單就是讀取轉化為控制的地方。除了保存更改之外,它還有解鎖、鎖定和重啟物聯網按鈕,這些按鈕直接與實體設備通信。
最令人擔憂的部分是物聯網模組。管理面板發送的命令如下所示:
我發送了第二個命令來關閉同一輛車:
相同的確認。我將整個測試限制在一輛車上,pk_veiculo: 699:一個被盜的管理員帳戶可以從任何地方解鎖滑板車,然後立即將其鎖定。
管理面板甚至彈出了一個成功訊息:
但 API 回應和螢幕上的綠色訊息本身並不能證明太多。我需要知道命令是否真的到達了實體滑板車。我當時不在任何一輛車附近,所以我請朋友走到一輛滑板車旁,在我發送命令的瞬間錄製影片。結果奏效了:滑板車解鎖了,燈亮了,它準備好騎行了,沒有人付費、掃描二維碼,甚至沒有觸碰它。
我沒有測試的部分與我測試的部分同樣重要。我從未對大量車輛進行過自動化測試,也從未觸碰過任何可能導致移動、剎車或任何現實世界危險的操作。對一輛滑板車發送一個命令就足以證明網頁面板可以觸及實際硬體。
並非所有線索都得到了證實。死胡同也值得分享,因為排除它們引導我走向了成功的道路。
Android 應用程式是 Flutter 開發的,並將其大部分邏輯編譯到 libapp.so 中。我嘗試了許多方法(模擬器、證書、重新打包的 APK、流量捕獲),才終於能夠攔截到一個會話。許多代理和證書組合都未能給我帶來我想要的流量。
當捕獲最終成功時,33 個流程顯示 validateApp 保護依賴於靜態標頭和 Bearer 令牌,而不是每個請求都有一個新的簽名:
這有助於我繪製 API 圖,但我仍然需要一個有效的令牌。這個繞過並不是我進入管理面板的方式。
寬泛的查詢觸發了一些防禦措施並終止了會話,可能是粗略的異常過濾器,或者在我變得貪婪時觸發了令牌吊銷。這是後端實際反擊的少數幾個地方之一。
我將車輛參考資料發送到歷史記錄端點,但收到了空集或 400 錯誤,因此無法跨越存取他人的行程。回應確實洩漏了內部類別名稱和 PHP/ORM 訊息,例如 T_app_usuario_viagem 和 Undefined array key 6。
我在找到憑證路由之前就追蹤了這兩者,但都沒有成功。登入欄位無法注入,未簽名或被篡改的令牌被拒絕。這些失敗值得記錄下來:我最終獲得的存取權並非來自 SQLi 或 JWT 漏洞,而是來自列舉加上弱密碼加上過多的權限。
這一切都不需要什麼複雜的漏洞。它都來自於身份識別、驗證和權限問題的堆疊:
最大的問題是沒有任何東西能夠相互備份。密碼是連接開放網際網路和一個可以移動實體硬體的按鈕之間的唯一屏障。在這個過程中,沒有任何地方需要第二次登入或對物聯網命令進行額外的確認。
憑藉一個被盜的管理員憑證,我可以存取:
車輛、車庫和停靠點的地圖;
營運拓撲和地理圍欄;
廣泛的使用者數據和財務模組;
編輯測試帳戶上的記錄和優惠;
向測試滑板車發送遠端開鎖和鎖定命令。
在真實的攻擊中,這可能會轉化為未經授權的車隊使用、詐騙、營運損失、暴露的使用者數據以及間接的實體風險,所有這些都可以通過單一憑證大規模完成。
遠端解鎖滑板車的道路並非始於花哨的逆向工程或罕見的加密漏洞。它始於一個被遺忘的子網域、一個公開的使用者和兩個不同的錯誤訊息。
那些花哨的嘗試都失敗了:偽造的 JWT 被拒絕,SQL 注入從未出現,路由一直返回 401。真正起作用的是最明顯的攻擊。一個已知的身份、一個可猜測的密碼,以及沒有第二道防線,就足以將網頁存取轉化為對實體設備的控制。