原文:《Exploiting Windows 10 in a Local Network with WPAD/PAC and JScript》
譯者: c1tas,de4dcr0w @ 360CERT
譯者公眾號:https://mp.weixin.qq.com/s/bEWXDfZE5dWtt6EyddVWJw

0x00 aPAColypse now: Exploiting Windows 10 in a Local Network with WPAD/PAC and JScript by Ivan Fratric, Thomas Dullien, James Forshaw and Steven Vittitoe

by Ivan Fratric, Thomas Dullien, James Forshaw and Steven Vittitoe

0x01 Intro

許多廣泛部署的技術在經過 20/20 hindsight(判斷方式) 之后發現都是一些奇怪的或不必要 的冒險想法

在 IT 里的工程決定往往在不完善的信息和時間壓力下完成, 一些古怪的 IT 設置可能被解 釋為" 在這時候這看起來是個好的方案", 在這篇文章的作者中, 一些人發現 WPAD("Web Proxy Auto Discovery Protocol") 更多被稱為 ("Proxy Auto-Config"), 就是一個奇怪的點

在互聯網的早期,在 1996 之前,Netscape 的工程師們決定 JavaScript 是一種很好的語言 來編寫配置文件。其結果是 PAC-配置文件格式,其工作方式如下:瀏覽器連接到預先配置的 服務器,下載 PAC 文件,并執行特定的 JavaScript 函數以確定適當的代理配置。為什么不呢? 它肯定比 XML 更具表達性和更少的細節,似乎是向許多客戶提供配置的合理方式。

PAC 本身加上一個叫做 WPAD-一個協議,使瀏覽器不必有一個預配置的服務器用來連接 協議。相反,WPAD 允許計算機查詢本地網絡來確定從哪個服務器下載的 PAC 文件。 不知怎么的,這項技術最終成為了一個 1999 到期的 IETF 的草案,現在,在 2017,每臺 Windows 機器都會問本地網絡:“嘿,我在哪里可以找到一個 JavaScript 文件來執行?“。這可 以通過多種機制來實現:DNS、WINS,但也許最有趣的是 DHCP。

近年來,瀏覽器的開發已經從最初的 DOM 轉向直接針對 JavaScript 引擎,所以僅僅提到 我們可以在沒有瀏覽器的情況下在網絡上執行 JavaScript 是一種激勵。初步調查顯示 JS 引擎 負責執行這些配置文件是 jscript.dll-遺留的 JS 引擎,也帶動了 IE7 和 IE8(和仍然可在 IE11 在 IE7 / 8 兼容模式如果使用適當的腳本屬性)。這有好有壞——一方面,它意味著不是每一個 Chakra bug 都是自動的本地網絡遠程攻擊,但另一方面,它意味著一些相當舊的代碼將負責執 行我們的 JavaScript。

早先, 安全研究人員就警告過 WPAD 的危險性, 但這貌似我們知道的第一例, 證明利用 WPAD 進攻擊是可以危害到用戶機的 。

windows 不是唯一在使用 WPAD 的軟體, 其他的操作系統和軟件同樣有在使用.Google Chrome 就同樣具有 WPAD 解釋器, 但是 Chrome 會把 PAC 文件中的 JS 代碼放在沙箱中運 行, 但其他的操作系統就沒有這么做, 這就是 windows 為何是這種攻擊最有興趣的目標。

0x02 Web Proxy Auto-Discovery

1.Intro

如上所述,WPAD 將查詢 DHCP 和 DNS(按此順序)以獲取要連接的 URL-如果沒有來 自 DNS 的響應,則顯然 LLMNR 和 Netbios 也可以使用。WPAD-over-DNS 的一些特性使得 中間人攻擊能夠出人意料地發揮作用。

2.Attack scenario: Local network via DHCP

在最常見的場景中,機器將使用 option code 252 查詢本地 DHCP 服務器。DHCP 服務器 返回一個字符串如http://server.domain/proxyconfig.pac,指定一個 URL 的配置文件應該被加 載。然后,客戶端繼續獲取該文件,并將內容作為 JavaScript 執行。 在本地網絡中,攻擊者可以簡單地模仿 DHCP 服務器 - 可以通過 ARP 欺騙或通過合法的 DHCP 進行競爭。然后,攻擊者可以提供一個惡意 JavaScript 文件所在的 URL。

3.Attack scenario: Remote over the internet via privileged position and DNS

除了當地的網絡攻擊的情況下,事實上,查找 WPAD 這一請求也可能通過 DNS 查詢發生 的這一情況, 也就產生了更多攻擊場景。許多用戶配置他們的計算機執行 DNS 查找某個公共 DNS,全局可見的 DNS 服務器(如 8.8.8.8、8.8.4.4,地址 208.67.222.222 和 208.67.220.220)。 在這樣的情況下,機器會發出 DNS 查詢(如 wpad.local),位于本地網絡服務器之外。攻擊者 在網絡上的特權位置(例如網關或任何其他上行主機)可以監視 DNS 查詢并欺騙應答,指示 客戶端下載并執行惡意 JavaScript 文件。 像這樣的設置似乎很常見-根據這一維基百科條目,DNS 根服務器所看到的流量中的一大 部分是本地請求。

4.Attack scenario: Remote over the internet via malicious wpad_tld

WPAD 的特殊之處在于遞歸地遍歷本地機器名稱以查找要查詢的域。如果一臺機器被稱為 “laptop01.us.division.company.com”,則按照以下方式查詢以下域名:

  • wpad.us.division.company.com

  • wpad.division.company.com

  • wpad.company.com

  • wpad.com

這(根據這個維基百科條目)過去導致人們注冊 wpad.co.uk 并將流量重定向到在線拍賣 網站。進一步引用該條目: 通過 WPAD 文件,攻擊者可以將用戶的瀏覽器指向自己的代理,攔截并修改所有的 WWW 流量。盡管在 2005 年對 Windows WPAD 處理進行了簡單的修復,但它只解決了.com 域的問題。在 Kiwicon 的一個演示顯示,世界其他地區仍然嚴重受到這個安全漏洞的威脅, 在 新西蘭注冊了一個樣本域名,用于測試目的,接收來自全國各地的代理請求,速度為幾秒鐘。 幾個 wpad.tld 域名(包括 COM,NET,ORG 和 US)現在指向客戶端回送環回地址,以幫助 防止此漏洞,盡管某些名稱仍然被注冊(wpad.co.uk)。

因此,管理員應該確保用戶可以信任組織中的所有 DHCP 服務器,并且保證組織的所有 可能的 WPAD 域都受到控制。此外,如果沒有為組織配置 wpad 域,用戶將轉到域分層結構 中具有下一個 wpad 站點的任何外部位置,并將其用于其配置。這允許只要注冊了特定國家的 wpad 子域名,通過設置自己作為所有流量或感興趣的站點的代理,就可以對該國的大部分互 聯網流量進行中間人攻擊。

另一方面,IETF 草案明確要求客戶只允許“規范”(例如非頂級域名)。我們還沒有調查是 否有人在什么程度上實施這個攻擊,或者二級域名(如.co.uk)是否是流量重定向的歷史案例 的罪魁禍首。 無論哪種方式, 如果一個管理員注冊 wpad.$TLD 給定組織的 TLD, 前提是該 TLD 沒有被 客戶端實施明確列入黑名單, 那么在 JavaScript 引擎的漏洞就可以被通過互聯網遠程利用. 鑒于 1999 年的 IETF 草案提到了 1994 年的 TLD 列表(RFC1591),客戶不太可能已經更新以反映 新 TLD 的擴散。

我們嘗試為各種 TLD 注冊 wpad.co.$TLD 尚未成功。

0x03 Bugs

我們花了一些時間在尋找jscript.dll, 采用手工分析和 fuzzing,JScript最初的挑戰是許許多 多的"feature" 能在 javascript engines 觸發 bugs 的 tips 無法在其工作, 應為它太陳舊而不支持 它們. 舉個例子

  • 沒有多種的 array tpyes(int array, float array etc.). 因此混淆一個數組類型是不可能的

  • 沒有更多的優化例如 ("fast paths"), 在更新的更快的 Javascript engines 中這些 fast path 的優化常常是 bug 的來源

  • 不能對一個通常的 JavaScript 對象定義 getter/setter. 只在 dom 對象中可以調用的定義 的屬性, 但在 WPAD 進程中沒有 DOM. 并且 DOM 對象中大量的 JScript 函數將 fail 并伴隨一個消息"JScript object expected"

  • 只要在一個對象被創建后, 不允許修改他的屬性 (i.e. 這里沒有proto屬性)

然而,JScript 會遭受更多"old school" 的漏洞, 比如 use-after-free. JScript 的垃圾收集器 (a post).Jscript 使用了非世代 (no-generational) 的標記和清理垃圾收集器. 從本質上講, 無論 何時垃圾收集被觸發, 它都會標記所有的 JScript 對象. 然后從一組"root" 對象 (有時候被稱 為"scavengers"(清道夫)) 開始掃描他們, 并從所有的對象中清除標記. 所有仍被標記的對象都將 被刪除. 一個經常出現的問題是, 默認情況下, 堆棧上的本地變量不會添加到根對象列表中, 這 意味著程序員需要記住將他們添加到垃圾收集器的根列表, 特別是這些變量引用對象, 在函數的 生命周期中可以被刪除的時候.

另一種可能的漏洞類型包括 buffer overflows(緩沖區溢出),uninitialized variables 等等 Fuzzing, 我們使用了基于語法的Domato fuzzing 引擎, 并且寫了一套新的 JScript 語法. 我 們把發現的有趣的內置的屬性和函數添加到這個語法中, 通過觀察不同 JScript 對象的EnsureB uildin方法. 被添加的 JScript 語法在 Doamto repository(here)

在 fuzzing 和手工測試中我們發現了 7 個安全漏洞, 整理如下表

在發布這篇文章的時候, 所有的 bugs 已經被修復了 這個表格通過漏洞觸發類別, 和所需要的兼容模式來區分.JScript 在 WPAD 中相當于在 IE 兼容模式運行腳本, 這意味我們雖然發現了 7 個漏洞, 但實際只有 5 個引發 WPAD, 然而其 他漏洞任然可以被用在 IE(包括 IE11) 投入 IE8 兼容模式的惡意網頁。

0x04 Exploit

1、 Understanding JScript VARs and Strings

在這篇文章中的接下來的部分,我們將討論JScript VARs和Strings, 在討論漏洞工作方式之前,先講述這些內容是十分有用的

JScript VAR是一個24-byte(64位)結構,這表示一個JavaScript變量,本質上是和VARIANT的數據結構相同的(MSDN described).

在多數情況下(足以跟蹤漏洞)內存布局如下所示

例如,我們可以用一個VAR表示一個雙精度數字,它在前2個字節中是5(表示double類型),后跟一個實際的雙值,在偏移量為8。最后8個字節將沒有使用,但如果從這個VAR.復制另一個VAR的值,它們將被復制

一個JScript的String 是VAR的一種類型在標志位為8,并且指針偏移量是8.這個指針指向一個BSTR結構體(described here).在64位的情況下BSTR的結構如下

一個String VAR 指針直接指向字符數組,這意味著,獲取一個字符串的長度,指針需要-4從那個位置讀取長度.注意,BSTRs被OleAut32.dll的處理,被分配在獨特的堆(i.e. 意思就是其他的Jscript的對象被分配在其他的堆)

釋放BSTR也不同于大多數對象,在釋放的時候,不再是直接釋放一個BSTR,當SysFreeSting被調用的時候,它首先會想一個字符串放在被OleAut32.dll控制的cache中.這個機制在Javascript Heap 風水

2、Stage 1:Infoleak

infoleak 的目的在于獲取一個完全由我們控制的string在內存的地址,我們在這個點上沒獲得任何可執行模塊的地址,但接下來會.然而,我們的目標是擊破hig-entropy heap randomization(高熵堆隨機化)并且讓第二階段的exploit可靠,而不需要是用堆噴

因此我們主要在RegExp.lastParen中使用這個漏洞.為了理解這個bug,我們先來仔細看看jscript!RegExpFncObj的內存布局,以及他對應的 JScript RegExp 對象.在偏移OxAC處, RegExpFncObj包含20個整數的緩沖區.實際上這是10對整數:每一對的第一個元素的輸入字符串的start index(開始索引),第二個元素是end index(結束索引).只要RegExp.test,RegExp.exec或帶有RegExp參數的String.search在匹配正則語法-組(group)的時候(RegExp中的圓括號),匹配的開始和結束索引就會存儲在這里.顯然的緩沖區中只有10對空間,所以只有前10個匹配項會被存在這個緩沖區中

但是,如果RegExp.lastParen被調用,并且有超過10個的捕獲組的時候,RegExpFncObj::LastParen會很高興的使用捕獲組的數量作為緩沖區的索引,從而導致越界讀取

這是一個PoC:

var r= new RegExp ( Array (100) .join ('() ') );
''.search (r);
alert ( RegExp.lastParen );

這兩項指標(我們稱之為start_index, end_index)讀取緩沖區邊界之外,并且可以達到任意大的區域.假設這第一個越界訪問不會造成崩潰,如果這些索引中的值大于輸入字符串的長度,那將發生第二次越界讀取,這將允許我們讀取輸入字符串邊界之外的內容.像這樣的字符串越界讀取,將字符串內容返回到字符串變量中的可以被檢測到的調用處.

這第二次越界讀將會是我們所使用的,但首先我們需要去明白如何獲得start_index, end_index的控制權.幸運的是,在RegExpFncObj的布局中,在index緩沖區后的有一段數據我們可以控制:RegExp.input 的值.通過設置RegExp.input為一個整數值,并且使用一個由41組空括號組成的RegExp(正則表達式),當RegExp.lastParen被調用的時候,start_index將會變成0,然后end_indx的值將會是我們寫到RegExp.input的值

如果我們將一個輸入字符串和一個釋放字符串相鄰,然后通過越界讀取輸入字符串的邊界外內容,我們就可以獲得堆的metadata,比如指向其他空閑堆的指針(堆塊的紅黑樹的左,右和父節點參閱window10堆內部信息獲得更多的信息),Image1 展示了相應對象在infoleak時的情況。

Image 1: Heap infoleak layout

我們將使用20000 bytes-long的字符串作輸入,為了不被分配在低碎片堆(Low Fragmentation Heap)(LFH只能被用來16k字節或更小的分配).因為LFH的堆metadata是不同的,在win10的Segment堆中不包含有用的指針.此外,LFH還引入了隨機性,這會影響到我們把輸入字符串被放在釋放字符串旁邊的構想

通過從返回的字符串中讀取堆的metadata,我們可以獲得一個釋放字符串的地址,那么我們分配一個與釋放字符串相同大小的字符串,它可能被分配在這個地址,我們實現了我們的目標,那就是我們知道了一個由我們控制的字符串的地址

這整個infoleak的過程大概是:

  1. 分配 1000 10000-character的String(提示: 10000 characters == 20000 bytes)

  2. 釋放每第二個(Free every second one)

  3. 觸發infoleak bug,使用剩余的字符串之一作為輸入字符串并越界讀取20080個bytes(字節)

  4. 分析泄露的字符串并獲得指向其中一個釋放字符串的指針

  5. 使用特定內容分配與釋放的字符串(10000個 characters)長度相同的500個字符串

特定內容的字符串在這個步驟還不重要,但在下一階段將變得十分重要,因此會在下一個階段描述它.還注意,通過檢查堆metadata,我們可以很容易的確定進程正在使用的堆(segment 堆和NT堆)

Image2和Image3展示了使用Heap History Viewer 在infoleak時生成的堆可視化的圖像

綠色的條紋表示分配塊(被字符串占用),灰色的條紋標識分配后,但是被釋放的后再分配的塊,(指向的是我們釋放后,又觸發infoleak后被重新分配到(the stings we free and then reallocate after triggering the infoleak bug)),白色的條紋是代表從未被分配的數據(保護頁).你可以看到隨著時間的推移字符串是如何被分配的,然后一半被釋放的(灰色的),一段時間后又被分配(條紋變成綠色)

我們可以看到,每3個這樣大小的分配后就會有一個保護頁.我們的exploit是從來沒有想過去接觸這些保護頁的(這發生在讀取字符串尾少量的數據(it reads too little data past the end of the string for that to occur))但有1/3的幾率,infoleak輸入的字符串后面講不會有空字符串,所以預期的堆metadata將會丟失.但是我們可以很容易的檢測到這種情況,并且使用另一個輸入字符串觸發infoleak錯誤,或者終止exploit(注意:到目前為止我們沒有觸發任何內存損壞)

Image 2: Heap Diagram: Showing the evolution of the heap over time

Image 3: Step-by-step illustration of leaking a pointer to a string.

3、Stage2: overflow

在exploit的第二個階段,我們使用this heap overflow bug 在Array.sort中.如果駛入的數組中元素數量大于Array.length/2,JsArrayStringHeapSort(如果未指定比較函數則被Array.sort調用)就會分配一個與當前數組元素數量相同大小的臨時的緩沖區(注意:可比array.length小).然后嘗試從0到Array.length的每一個數組引索檢索相應的元素,如果該元素存在,則將其添加到緩沖區,并轉化為字符串.如果數組在JSArrayStringHeapSort的生命周期中沒有改變,這將工作正常.但是,JsArrayStringHeapSort將數組元素轉化為Strings將觸發toString()回調.如果在其中一個toString()回調元素被添加到之前未定義的數組中時,將發生溢出.

為了更好的理解這個bug以及其可利用性,我們來仔細看看我們將會溢出的緩沖區結構.已經提到,數組的大小和當前正在輸入數組中的元素數量相同(確切的說,這將是元素數量+1)數組的每個元素都是48字節(64位)結構如下

在JsArrayStringHeapSort期間,檢索索引為 <array.length> 的數組的每個元素,并且如果元素被定義,這會發生以下情況

  1. 這個數組元素將被讀到偏移16的VAR中

  2. 原始的VAR將被轉換為一個String VAR.指向這個String VAR的指針被寫到偏移0

  3. 在偏移8,這個元素的index將被寫入

  4. 取決于原始VAR的類型,0或1將被寫入偏移40

看到臨時緩存區的buffer,我們不能直接控制大部分.如果一個數組成員是一個字符串,那么在它偏移量為0和24時,我們將會有一個指針,當他被解引用的時候,我們將在偏移8處包含另一個指向我們控制數據的指針.然而,這是一個間接的在大多數情況下對我們有用的大層次

但是,如果數組的成員是雙精度,則在偏移量24(對應原始VAR中的偏移量8)中,該數值將被寫入,并直接在我們的控制之下.

如果我們使用與階段1中獲得指針相同的雙重表示形式創建一個數字,那么可以使我們的溢出在緩存區結束后的某個地方使用指向我們直接控制的內存的指針來覆蓋指針

現在問題變成了,我們可以使用這個方法改寫這個漏洞,如果我們仔細研究一下對象如何在JScript中工作,那么答案可能就是其中一個

每個對象(具體的說,一個NameList的JScript對象)將有一個指向hash表的指針.這個hash表只是一個指針數組.當一個對象的成員元素被訪問的時候,元素名稱的hash被計算.然后取消對應于hash表最低位的偏移處指針.這個指針指向一個對象元素的鏈表,并且這個鏈表被遍歷,直到我們到達一個與請求元素具有相同名字的元素.這將在Image4中顯示

Image 4: JScript Object element internals

請注意,當元素名稱少于4個字節時,它存儲在與VAR(元素值)相同的結構中,否則將會有個指向元素名稱的指針.名稱長度<=4對于我們來說已經足夠,所以我們不需要深入了解這個細節

一個對象的hash表是覆蓋的很好的候選者,因為

  1. 我們能通過訪問相應的成員對象來控制它的那些元素被解引用.我們用我們不控制的數據覆蓋的元素將永遠不會被訪問

  2. 通過控制相應對象有多少個成員,我們對hash表的控制有限.例如,hash表以1024個字節開始,但如果我們向對象添加多于512個元素,hash表將被重新分配為8192個字節

  3. 通過用指向我們控制的數據的指針,覆蓋hash表指針,我們可以在我們控制的數據中創建假的JScript變量,并通過訪問相應對象成員來訪問它們

要更可靠的覆蓋,我們需要執行以下操作:

  1. 分配和釋放大小為8192的大量內存塊.這就將代開LFH以分配8192大小的堆.這將確保我們溢出的緩沖區,以及我們正在溢出的hash表被分配在LFH上.這一點很重要,因為這意味著附近不會有其他大小的分配來破壞漏洞攻擊(因為LFH bucket只能包含特定大小的分配),這反過來將確保我們將高度可靠地完全覆蓋我們想要的東西

  2. 創建2000個對象,每個對象包含512個成員.在這種狀態下,每個對象都有一個1024字節的hash表,但是向其中一個對象添加一個元素將使其增長到8192個字節

  3. 添加第513個元素到前1000個對象中,這導致1000個8192-byte的hash表被分配

  4. 使用長度為300包含170個元素的數組觸發Array.sort.這將分配一個(170 + 1)*48 = 8208bytes.由于LFH的規則,這個對象將被分配在與8192字節hash表相同的LFH bucket中.

  5. 立即在(第一個數組元素的toString()方法中)向第二個1000個對象添加第513個元素.這使得我們非常確定,現在排序緩沖區是相鄰hash表之一.在相同的toString()方法中,還會向數組中添加更多的元素,這將使它超出邊界.

Image5 顯示了排序緩存區地址周圍的堆可視化(紅線).你可以看到排序緩存區被大小相近的分配所包圍,這些分配都與hash表對應.你也可以觀測到LFH隨機性,因為隨后分配的不一定在隨后的地址上,然而這對我們的利用沒有任何影響

Image 5: Heap visualization around the overflow buffer

正如前面提到的,我們以這樣一種方式制作了我們的溢出:一個不幸的JScript對象的hash表指針將會被指向我們控制的數據指針覆蓋.現在我們把這些數據放在什么地方:我們制作了一個包含5個(假)JavaScript變量的方法:

  1. 變量1 只包含數字1337.

  2. 變量2 是特殊類型的0x400c.這個類型基本上告訴Javascript,實際的VAR是有偏移量為8的指針指向的,在讀取或寫入這個變量之前,這個指針應該被解引用.在我們的例子中,這個指針指向變量1之前的16個字節,這基本上意味著變量2的最后8字節的qword和變量1的第一個8字節的qword重疊

  3. 變量3, 變量4和變量5是簡單的整數.它們的特殊之處在于它們分別在最后8字節包含數字5,8,0x400c.

Image6顯示了溢出之后被破壞的對象狀態

Image 6: State of objects after the overflow. Red areas indicate where the overflow occurred. Each box in the bottom row (except those marked as ‘...’) corresponds to 8 bytes. Data contained in ‘...’ boxes is omitted for clarity

我們可以訪問變量1, 只需要訪問正確引索處的已破壞對象(我們之前稱為Index1),對于變量2-5也是如此.實際上,我們可以通過訪問所有對象的Index1來檢測哪個對象被破壞,并查看哪個對象有現在的特征值1337

重疊變量1,變量2的作用是可以將變量1的類型(第一個word)改為5(double),8(字符串),或0x400c(指針).我們通過讀取變量2,3或4然后將讀取的值寫入變量2來實現.例如語句:

corruptedobject [ index 2] = corruptedobject [ index 4];

具有將變量1的類型更改為字符串(8)的效果,而變量1的所有其他字段將保持不變

這種布局給了我們幾個非常強大的開發原語:

  • 如果我們將一個包含指針的變量寫入變量1,我們可以通過將變量1的類型改為double(5)并將其讀取出來公開該指針的值

  • 我們可以通過在改地址上偽造一個字符串來在任意地址上公開(讀取)內存.我們可以通過首先將與我們想要讀取的地址對應double的值寫入變量1,然后將變量1的類型更改為Stirng(8)來完成此操作

  • 首先將對應于地址的數值寫入變量1,然后將變量1的類型更改為0x400c(指針),最后將一些數據寫入變量1,從而寫入任意地址.

有了這些利用源語,通常獲得代碼執行將非常簡單,但由于我們正在利用win10,我們首先要繞過Control Flow Guard(CFG).

4、 Stage 3: bypass CFG

我們可以使用一些已知的繞過方式,但是如果攻擊者有讀寫的權限就可以針對jscript.dll進行方便繞過。我們想要達到以下的效果:

  1. 返回地址沒有受CFG保護

  2. 一些Jscript對象已經指向原始的棧

特別地,每一個NameTbl 對象(在Jscript中,所有的JavaScript對象都從NameTbl繼承),在偏移24的位置保存一個指向CSession對象的指針。CSession對象偏移80的位置保存著指向靠近原始棧頂的指針。

所以,如果可以任意讀,通過任何Jscript對象的一連串指針,有可能獲得指向原始棧的指針。如果可以任意寫,就有可能繞過CFG來覆蓋返回地址。

5、Stage 4: Getting code execution as Local Service

準備好所有攻擊的條件,我們現在可以實現代碼執行。具體步驟如下:

  1. 從任一JScript對象的虛表中讀取jscript.dll的地址

  2. 通過讀取jscript.dll的導入表獲取kernel32.dll的地址

  3. 通過讀取kernel32.dll的導入表獲取kernelbase.dll的地址

  4. 從kernel32.dll中搜索我們需要的rop gadget

  5. 從kernel32.dll的導出表中獲取WinExec函數的地址

  6. 根據上文的描述泄露棧地址

  7. 準備好ROP鏈并寫入棧中,從最接近泄露的棧地址的一個返回地址開始執行rop

我們需要的rop鏈大致如下:

[ address of RET ] // needed to align the stack to 16 bytes
[ address of POP RCX ; RET ] // loads the first parameter into rcx
[ address of command to execute ]
[ address of POP RDX ; RET ] // loads the second parameter into rdx
1
[ address of WinExec ]

通過執行rop鏈我們可以調用WinExec函數調用指定的命令。例如:如果我們運行‘cmd’命令,將產生一個命令提示符,和WPAD服務運行用戶相同的權限。

不幸的是,在子進程起的本地服務無法連接網絡,但是我們可以將提權代碼從內存落地到可寫可執行的本地磁盤中。

6、Stage 5: Privilege escalation

當本地服務用戶是一個service用戶時,并沒有管理員權限。這意味著exploit在進行系統的訪問和修改操作時有很大的局限性,特別是攻擊過后或者系統重啟后的持久化。但是Windows上總是有可能找到未修補的提權漏洞,所以我們不需要找一個新的提權漏洞。我們可以濫用一個built-in功能實現從本地服務權限到SYSTEM權限。下圖是WPAD中service用戶被賦予的權限:

Image 7: Service Access Token’s Privileges showing Impersonate Privilege

我們只能得到以上三個權限,但是圖中高亮的SeImpersonatePrivilege很重要。該權限允許此服務獲得本地系統其他用戶的權限。Impersonate權限表明該服務接受本地系統其他用戶的請求,可能要代表這些用戶執行一些操作。然而只要我們獲得這些用戶的訪問令牌,就可以獲得這些用戶的全部權限,包括SYSTEM用戶,這使得我們在本地系統上擁有了管理員權限。

濫用Impersonate權限是Windows安全模式(你可以通過查找Token Kidnapping獲取更多細節)下一個已知的問題。微軟已經嘗試讓它更難獲得其他用戶的訪問令牌,但是不可能避免所有可能的情況。例如,James發現了一個Windows DCOM實現的一個漏洞。該漏洞允許任意用戶獲得SYSTEM用戶訪問令牌。微軟修復直接提權的漏洞,但是沒有或者很難修復token kidnapping問題。我們可以濫用這個功能來獲取SYSTEM權限,然后完全控制該系統,比如安裝一個提權服務。

這里有一個通過DCOM實現的token kidnapping(RottenPotato),但是這個實現是被設計用于Metasploit框架中 getsystem 命令的,我們無法使用。所以我們用C++實現了更簡易的版本,通過 CreateProcessWithToken API直接產生帶有SYSTEM 令牌的隨機進程。我們將它編譯成11KB大小的可執行程序,比RottenPotato小的多,方便落地到本地,執行rop鏈。

7、Tying it all together

當WPAD服務請求PAC文件時,我們將運行惡意文件來攻擊WPAD服務,運行WinExec來落地和運行提權程序。該程序會在SYSTEM權限下執行命令(我們例子中是‘cmd’的硬編碼)。

該exploit在我們的實驗環境中運行得十分穩定,但是100%的穩定性并不需要,因為如果該exploit使WPAD服務崩潰了,一旦用戶發起另一個WPAD服務請求,就會產生新的進程,所以攻擊者可以一直嘗試。如果用戶沒有關閉Window 錯誤報告,將不會有圖形化的頁面顯示WPAD服務崩潰了。如果開啟,將會捕捉到這個崩潰,然后報告給微軟。

事實上,我們的exploit一旦運行它的payload就會使WPAD服務崩潰,所以如果我們在服務被攻擊后繼續保持惡意的PAC文件,將會再次攻擊服務。你可以在圖片7中看到該效果,在攻擊服務后的幾分鐘里,在受害者機器上產生了大量的HTTP請求。

Image 8: Did we leave the exploit running for too long?

我們將很快在 issue tracker 上公布我們的exploit源碼

0x05 Conclusion

執行不信任的JavaScript 代碼是危險的,在一個沒有沙箱的進程里執行更是如此。即使是在像jscript.dll這樣相對安全的JavaScript引擎中也是這樣。我們披露了jscript.dll 7個安全問題,成功證明了可以從本地網絡(或者之外)達到穩定地代碼執行。即使是在Windows 10 64-bit with Fall Creators Update版本的全補丁(撰寫本文之前)也可以實現。

盡管現在bug已經被修復了,是否意味我們就可以完成任務,安枕無憂了?事實不然,盡管我們花了相當多的時間,精力和電力成本來發現jscript.dll漏洞,但是我們并沒有表明我們找到了所有漏洞。事實上,發現了7個漏洞,就很有很可能發現第8個。所以一些事情不改變,早晚有一天我們很可能會在野外看到這些利用。(這還是樂觀地假設攻擊者沒有這種能力。)

那么,微軟能做什么來使未來像這類攻擊變的艱難:

  1. 默認關閉WPAD,事實上,雖然其他的操作系統也支持WPAD,但是Windows是唯一默認開啟的。

  2. 沙箱化在WPAD服務中的Jscript解釋器。當解釋器需要執行一個定義好輸入輸出的JavaScript函數時,應該直接沙箱化它。考慮到輸入輸出模型的簡易性,如果微軟推出一個類似于seccomp-strict,有限制性的沙箱:一些進程不需要高于“接收一些數據”、“執行一些計算”、“返回一些數據”的權限。

如果你想要自己解決這些問題,當前防止這類攻擊(除了未知的漏洞)的唯一方法似乎就是完全關閉WinHttpAutoProxySvc服務。由于有其他服務依賴于WPAD,所以在Services 界面里有時不能關閉WinHttpAutoProxySvc服務(“Startup type”可能變灰失效),但是可以通過修改相應的注冊表項關閉:將“HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WinHttpAutoProxySvc”中的“Start”值從3(手動)改成4(關閉)。

當搜索“disabling WPAD”時,在網上發現一些常見的建議在我們的實驗中并不能防范攻擊:

  1. 在控制面板關閉“Automatically detect settings”

  2. 設置“WpadOverride”注冊表值

  3. hosts文件中寫入“255.255.255.255 wpad”(這將會關閉DNS變量,但是不能關閉DHCP變量)


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/541/