Alpine Linux APK包管理器遠端程式碼執行漏洞分析
概述
Alpine 是一款 面向安全應用的輕量級Linux發行版本,常作為Docker映象使用。在研究過程中,我們發現了Alpine Linux 預設包管理器APK的一些漏洞。該漏洞允許網路中間人(或惡意包映象)在使用者的機器上實現任意程式碼執行。這一漏洞的後果尤為嚴重,因為在使用預設倉庫( Repositories)的過程中,並不會通過TLS的方式提供包。目前,這一漏洞已經得到修復,並且Alpine的基礎映象已經更新。受漏洞影響的使用者需要及時對映象進行更新。
在獲得程式碼執行漏洞利用後,我想出了一個很酷的方法,通過寫入 /proc/<pid>/mem,使原始apk程序退出,並返回狀態碼(Exit Code)0,且該方法不需要SYS_PTRACE功能。經過嘗試,結果證明安裝帶有apk的包的Dockerfile後,漏洞仍然能夠成功利用,並且能夠建立成功。
以下是我在Alpine環境下,通過中間人攻擊的方式,對Docker容器進行漏洞利用的過程視訊:
https://justi.cz/assets/apkpoc.mp4
漏洞詳情:任意檔案建立導致遠端程式碼執行
Alpine軟體包是以.apk檔案的形式釋出,實際上它只是用gzip壓縮的tar檔案。當apk正在提取包時,它會在檢查雜湊值之前就把這些檔案提取出來。在提取壓縮包的時候,每個檔名和硬連結目標都以. apk-new為字尾名。隨後,當apk檢查到某個下載的包雜湊值不正確時,會嘗試刪除對所有提取的檔案和目錄的連結。
由於apk的“提交掛鉤”(Commit Hooks)特性,就很容易將已經存在的任意檔案寫入漏洞變成程式碼執行漏洞。如果我們能找到某種方法,將檔案解壓縮到/etc/apk/commit_hooks.d/,並讓它在清理過程之後還能保留在原有位置,那麼,該檔案就會在apk退出之前執行。
通過控制正在下載的tar檔案,我們可以建立一個具有永續性的“提交掛鉤”,如下所示:
1、在/etc/apk/commit_hooks.d/建立一個資料夾,預設情況下該資料夾不存在。提取的資料夾不能以.apk-new為字尾。
2、建立一個符號連結,指向/etc/apk/commit_hooks.d/x,其中x可以是任何名稱,比如link。它在加上字尾後,檔名為link.apk-new,但仍然會指向/etc/apk/commit_hooks.d/x。
3、建立一個名稱link的常規檔案(也將新增字尾,變為link.apk-new)。它將通過符號連結寫入,並在/etc/apk/commit_hooks.d/x處建立一個檔案。
4、當apk發現包的雜湊與簽名索引不匹配時,它將首先取消連結link.apk-new。然而在此時,/etc/apk/commit_hooks.d/x將不會發生變化。原因在於,這個目錄中已經包含我們的Payload,所以取消/etc/apk/commit_hooks.d/的連結時將會失敗,出現ENOTEMPTY的錯誤。
修改狀態碼
現在,在apk退出之前,我們能夠在客戶端上執行任意程式碼。需要解決的問題就是,如何找到一種方法能讓apk程序完美的退出。如果在Dockerfile建立的步驟使用apk,那麼apk將會返回非0的狀態碼(Exit Code),這一步驟也會失敗。
如果我們什麼都不做,那麼apk會返回一個等於它無法安裝的軟體包數量的狀態碼。目前為止,無法安裝的軟體包至少有一個。但有趣的是,這個值存在溢位漏洞,如果錯誤數量%256==0,那麼這個過程就會返回0,也就是我們想要的狀態碼返回值。目前,該漏洞已經修復,詳見:https://github.com/alpinelinux/apk-tools/commit/7b654e125461b00bc26e52b25e6a7be3a32c11b9 。
我首先嚐試使用gdb附加到程序,並且只調用exit(0)。但不幸的是,Docker容器預設沒有SYS_PTRACE功能,所以我們不能這樣做。但是,由於我們是root使用者,所以我們可以為apk程序讀取和寫入/proc/<pid>/mem。
import subprocess import re pid = int(subprocess.check_output(["pidof", "apk"])) print("\033[92mapk pid is {}\033[0m".format(pid)) maps_file = open("/proc/{}/maps".format(pid), 'r') mem_file = open("/proc/{}/mem".format(pid), 'w', 0) print("\033[92mEverything is fine! Please move along...\033[0m") NOP = "90".decode("hex") # xor rdi, rdi ; mov eax, 0x3c ; syscall shellcode = "4831ffb83c0000000f05".decode("hex") # based on https://unix.stackexchange.com/a/6302 for line in maps_file.readlines(): m = re.match(r'([0-9A-Fa-f]+)-([0-9A-Fa-f]+) ([-r])', line) start = int(m.group(1), 16) end = int(m.group(2), 16) if "apk" in line and "r-xp" in line: mem_file.seek(start) nops_len = end - start - len(shellcode) mem_file.write(NOP * nops_len) mem_file.write(shellcode) maps_file.close() mem_file.close()
因此,我們的方案如下:
1、使用pidof找到apk程序的pid;
2、使用/proc/<pid>/maps查詢程序的可執行記憶體;
3、編寫Shellcode/">Shellcode,最終將exit(0)直接寫入記憶體。這一步能夠成功我覺得非常驚訝,我本來以為會寫入失敗。
當我們的提交掛鉤退出後,apk恢復執行,就會執行我們的Shellcode。
總結
如果你在生產環境中使用Alpine Linux,你應該首先重建映象,然後考慮向該專案組捐款表示支援(https://wiki.alpinelinux.org/wiki/Alpine_Linux:Developers )。原因在於,apk專案的一個主要開發人員只花費了不到一週時間就修復了這一漏洞(https://github.com/fabled ),並且隨後不久,Alpine的維護團隊就釋出了新的版本(https://github.com/ncopa )。考慮到目前可能有數百個組織在生產環境中使用了Alpine Linux,因此這些組織可能已經受到了該漏洞的威脅,我們建議儘快開展自查,並及時更新。