Snapd Ubuntu 提權分析 0x00 背景 做過腳本的同學都知道提權的苦楚。平時在做定向滲透,溯源反制的時候,經常遇到系統版本最新,或者低內核、高補丁,又或許可權限制的很死的情況。進得去,卻拿不下真的很氣。14號看到 Snap < 2.37.1 提權漏洞,測了下,異常好用。在 ubuntu 18.04之後版本默認安裝,只需要有文件寫入許可權和python環境,即可完美提權。幾個vps一打一個準。想了想,最末分析過的 linux 平台漏洞還是 「臟牛」。近半年多一直在搞其他方面,許久沒做漏洞分析了,正好有個提權漏洞換換腦子。 0x01 linux提權姿勢梳理 首先梳理一下 linux 提權的種類。我所知道的所有提權思路有這麼幾種: 內核漏洞利用 服務、程序漏洞利用 許可權配置不當 內核漏洞利用 內核漏洞利用是最常見的提權方式,滲透提權的時候首先想到的就是查看系統版本、內核版本。根據環境找提權 exp。內核漏洞利用的常規利用方式,有這麼幾步: 通過漏洞將 payload 打入到內核模式下 操縱內存數據,比如將用戶空間映射到內核空間 啟動新許可權的shell,獲得root許可權 這種提權方法,需要找到對應內核版本的漏洞利用工具,並且具有運行利用工具的能力。即使能夠運行工具,也不一定完全提權成功。許多公開的漏洞利用工具,都不穩定。運行提權工具有可能造成目標主機宕機重啟。 服務、程序漏洞利用 許可權具有繼承性,高許可權運行的服務、程序,他的執行能力也是高許可權。一些web服務,資料庫應用,系統服務組件往往都在高許可權下運行。例如,運維人員通常使用root許可權運行mysql。這時可以嘗試使用mysql提權漏洞,將低許可權的mysql用戶提權至mysql root 用戶。mysql 本身具有 shell 執行環境。系統root身份運行的mysql,其 mysql root 許可權接近系統 root。 許可權配置不當 這種要具體情況具體分析,常見的比如: 弱口令 suid配置錯誤 sudo許可權濫用 路徑配置不當 配置不當的Cron jobs 等 這部分,可以參照這篇 blog。本次分析的snap提權漏洞,屬於snapd 在使用api的時候,身份鑒權存在問題,允許地許可權用戶調用高許可權api,從而造成提權。 一篇完整的漏洞分析必須要包括: 漏洞背景 漏洞成因分析 漏洞上下文分析 利用方式分析 補丁分析 漏洞驗證 安全建議 下面是正文 0x02 漏洞背景 snap是一個Linux系統上的包管理軟體。在Ubuntu18.04後默認預安裝到了系統中。snapd 是負責管理本地安裝服務與在線應用商店通信的程序,隨著snap一起安裝,並且在root許可權下運行,這是提權的基本條件。 根據官方描述,服務進程snapd中提供的REST API服務對請求客戶端身份鑒別存在問題,從而導致了提權。Chris Moberly 已經公開了細節。 0x03 漏洞成因分析 漏洞相關的更改:漏洞位置在: func ucrednetGet(remoteAddr string) (pid uint32, uid uint32, socket string, err error) { pid = ucrednetNoProcess uid = ucrednetNobody for _, token := range strings.Split(remoteAddr, ";") { var v uint64 if strings.HasPrefix(token, "pid=") { if v, err = strconv.ParseUint(token[4:], 10, 32); err == nil { pid = uint32(v) } else { break } } else if strings.HasPrefix(token, "uid=") { if v, err = strconv.ParseUint(token[4:], 10, 32); err == nil { uid = uint32(v) } else { break } } if strings.HasPrefix(token, "socket=") { socket = token[7:] } } if pid == ucrednetNoProcess || uid == ucrednetNobody { err = errNoID } return pid, uid, socket, err } 該函數,是將 remoteAddr 解析出pid、uid、socket。該函數,對 remoteAddr進行分割,標誌符為 「;」 ,將分割後得到的數組,for 循環。通過 HasPrefix 判別內容,對pid、uid、socket進行賦值。這裡存在一個問題: for循環中,有可能會對變數重複賦值。Split分割後的數組,如果存在多個 uid= 開頭的值,則 uid 的值將被後者覆蓋。例如,"uid=1000;pid=1100;uid=0",通過 ; 進行分割,得到[uid=1000,pid=1100,uid=0],該數組在迭代的時候:} else if strings.HasPrefix(token, "uid=") { if v, err = strconv.ParseUint(token[4:], 10, 32); err == nil { uid = uint32(v) } else { break } } 第一次執行到這裡的時候,uid被賦值為1000,因為後面還有一個以uid為開頭的值(uid=0),所以程序還會進入這個代碼段,將uid 重置為0。這是,漏洞形成的基本邏輯。如果是發漏洞預警,分析到這裡已經可以了。但如果是做漏洞研究,還遠遠不夠,還要進行漏洞上下文和利用技術分析。 0x04 漏洞上下文分析 除了找到漏洞成因,還要知道」漏洞從哪來,到哪去」。 從哪來: 漏洞邏輯函數:func ucrednetGet(remoteAddr string) (pid int32, uid uint32, socket string, err error) { pid = ucrednetNoProcess uid = ucrednetNobody for _, token := range strings.Split(remoteAddr, ";") { var v uint64 ...... 漏洞處理函數,ucrednetGet() ,傳入變數為 remoteAddr,該變數即是Split處理對象。則查找該函數調用關係。 可以看到有n多調用,在api.go 文件中,有豐富邏輯代碼。隨進入分析。ucrednetGet() 被重命名為 postCreateUserUcrednetGet() 和 runSnapctlUcrednetGet(), 查看調用邏輯:func getUsers(c *Command, r *http.Request, user *auth.UserState) Response { _, uid, _, err := postCreateUserUcrednetGet(r.RemoteAddr) if err != nil { return BadRequest("cannot get ucrednet uid: %v", err) } if uid != 0 { return BadRequest("cannot get users as non-root") } ...... postCreateUserUcrednetGet() 傳入的參數為 r.RemoteAddr 。r 為 http.Request對象。由此可得,漏洞邏輯代碼,處理的對象來自,http.Request.RemoteAddr ,即:傳入漏洞邏輯函數 ucrednetGet() 的參數 remoteAddr 為 http.Request.RemoteAddr。 查了下,http.Request.RemoteAddr 為 go 內建結構,之後查看 go 代碼。這裡,分析了go中整個 SockaddrUnix 調用過程。這裡只簡單寫下要點: coon.go:123 聲明 RemoteAddr(),調用Conn.conn.RemoteAddr() coon.go:27 聲明結構體 Conn,其中 conn 為 net.Conn net.go:221 聲明net.conn.RemoteAddr(),返回c.fd.raddr,c 為 conn指針 net.go:164 聲明conn結構體,fd 為 netFD 指針 fd_unix.go:19 聲明 netFD 結構體。 fd_unix.go:45 聲明 setAddr 函數,對 netFD.raddr進行賦值, 此處即為漏洞傳入參數 RemoteAddr,首次聲明位置。 找到這裡還不夠,我們需要知道這個傳入的值,究竟從哪來的。 file_unix.go:66 調用 setAddr() :fd.setAddr(laddr, raddr),第二個參數,是我們需找的。 file_unix.go:60 設置raddr:addr := fd.addrFunc()(rsa) sock_posi.go:92 聲明 addrFunc(),可以看到根據套接字族設定進行不同的操作,返回sockaddrToXXX go func (fd *netFD) addrFunc() func(syscall.Sockaddr) Addr { switch fd.family { case syscall.AF_INET, syscall.AF_INET6: switch fd.sotype { case syscall.SOCK_STREAM: return sockaddrToTCP case syscall.SOCK_DGRAM: return sockaddrToUDP case syscall.SOCK_RAW: return sockaddrToIP } case syscall.AF_UNIX: switch fd.sotype { case syscall.SOCK_STREAM: return sockaddrToUnix case syscall.SOCK_DGRAM: return sockaddrToUnixgram case syscall.SOCK_SEQPACKET: return sockaddrToUnixpacket } } return func(syscall.Sockaddr) Addr { return nil } } 查閱資料,原來 AF_UNIX 用於進程間通信,綁定的文件,可以通過 sockaddrToUnix 取得。下面是說明: ```str ....... Address format A UNIX domain socket address is represented in the following structure:struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[108]; /* pathname */ }; The sun_family field always contains AF_UNIX. On Linux sun_path is 108 bytes in size; see also NOTES, below. Various systems calls (for example, bind(2), connect(2), and sendto(2)) take a sockaddr_un argument as input. Some other system calls (for example, getsockname(2), getpeername(2), recvfrom(2), and accept(2)) return an argument of this type. Three types of address are distinguished in the sockaddr_un struc‐ ture: * pathname: a UNIX domain socket can be bound to a null-terminated filesystem pathname using bind(2). When the address of a pathname socket is returned (by one of the system calls noted above), its length is offsetof(struct sockaddr_un, sun_path) + strlen(sun_path) + 1 and sun_path contains the null-terminated pathname. (On Linux, the above offsetof() expression equates to the same value as sizeof(sa_family_t), but some other implementations include other fields before sun_path, so the offsetof() expression more portably describes the size of the address structure.) For further details of pathname sockets, see below. ...... ``` unixsock_posix.go:52 定義了 sockaddrToUnix(),可以看到,是通過 syscall.SockaddrUnix獲得的綁定文件名。 分析到這裡,RemoteAddr 怎麼來的我們算整明白了:根據不同的套接字族,返回不同的地址。如果是通過 AF_UNIX 創建的套接字,將返回綁定的文件名。 到哪去 那麼,哪裡調動了存在漏洞的函數?該漏洞有多大影響呢?之前看到,漏洞函數在api.go 中進行調用: ucrednetGet 重命名為 postCreateUserUcrednetGet, 在postCreateUser有調用:...... func postCreateUser(c *Command, r *http.Request, user *auth.UserState) Response { _, uid, _, err := postCreateUserUcrednetGet(r.RemoteAddr) if err != nil { return BadRequest("cannot get ucrednet uid: %v", err) } if uid != 0 { return BadRequest("cannot use create-user as non-root") } ...... 而該函數,對應的是創建本地用戶的API:...... createUserCmd = &Command{ Path: "/v2/create-user", POST: postCreateUser, } ...... 了解下 snap API:API 文檔 功能是創建本地用戶,使用許可權是root。結合漏洞會將uid 覆蓋為 0(root)的可能,則該漏洞可以通過調用api,創建用戶,如果sudoer設置為true,則創建的為特權用戶。 0x05 漏洞利用分析 之上,將漏洞分析的明明白白。此時其實可以自己寫出exp: 創建 AF_UNIX 族套接字 綁定一個文件,文件名為;uid=0,「;」用於截取字元串,獲取覆蓋uid的能力。 調用API,且sudoer 設為true snapd在鑒權的時候會獲取遠程地址,如果是 AF_UNIX 類型套接字。將返回綁定的文件,觸發漏洞。 鑒權的到uid=0,認為是root許可權調用,執行生成本地用戶操作,且調用API,且sudoer=true,則生成的用戶具有特權。 漏洞作者給的 exp,確實是這麼寫的。 0x06 補丁分析 漏洞修補的很粗暴,之前: return fmt.Sprintf("pid=%s;uid=%s;socket=%s;%s", wa.pid, wa.uid, wa.socket, wa.Addr)現在定義了一個結構體 ucrednet ,並且現在return fmt.Sprintf("pid=%d;uid=%d;socket=%s;", un.pid, un.uid, un.socket)不再返回 wa.Addr ,即不再處理遠程連接地址。通過 AF_UNIX 套接字向RemoteAddr 注入文本已經行不通。從而修補了漏洞。 0x07 漏洞驗證 漏洞十分的好用,snap < 2.37.1 以下版本均受影響。因為在ubuntu 18.04 以後版本,默認安裝 snap ,並且測試時發現,一些vps 供應商 Ubuntu 16.04 同樣默認安裝snap。以下vps服務商的 ubuntu 安裝鏡像均存在問題: 騰訊雲 谷歌雲 亞馬遜雲 vultr 搬瓦工 ...... 測得vps,除了阿里雲外,一打一個準。阿里雲ubuntu 鏡像中,不帶有snap,是我測的主機中,唯一不受漏洞影響的雲服務商。 0x08 安全建議 修補很簡單,將 snap 升級到最新版就好了。有 ubuntu vps 的同學,建議查看一下自己主機上snap的版本。 0x09 後記 好久沒有認真做漏洞研究了,能完整分析出來漏洞,還是挺開心的。很多漏洞作者,都會公布漏洞詳情。建議做漏洞分析的同學,不要先去看漏洞詳情。成長的過程在於對漏洞的摸索。拿著分析文章,看一步調一步。沒有太大意義,沉澱不了自己的經驗。本次分析的snap漏洞,唯一卡住的地方,是套接字那裡。沒辦法,去看漏洞作者詳細分析,才知道原來還有 AF_UNIX 用於本地進程通信。這個是我知識盲區,卡在這確實沒辦法。ps:其實整篇下來,最難得部分是逆 go 的net標準庫。(笑 推薦閱讀: 相关文章 {{#data}} {{title}} {{/data}}