做過腳本的同學都知道提權的苦楚。
平時在做定向滲透,溯源反制的時候,經常遇到系統版本最新,或者低內核、高補丁,又或許可權限制的很死的情況。進得去,卻拿不下真的很氣。
14號看到 Snap < 2.37.1 提權漏洞,測了下,異常好用。在 ubuntu 18.04之後版本默認安裝,只需要有文件寫入許可權和python環境,即可完美提權。幾個vps一打一個準。
想了想,最末分析過的 linux 平台漏洞還是 「臟牛」。近半年多一直在搞其他方面,許久沒做漏洞分析了,正好有個提權漏洞換換腦子。
首先梳理一下 linux 提權的種類。我所知道的所有提權思路有這麼幾種:
內核漏洞利用是最常見的提權方式,滲透提權的時候首先想到的就是查看系統版本、內核版本。根據環境找提權 exp。
內核漏洞利用的常規利用方式,有這麼幾步:
這種提權方法,需要找到對應內核版本的漏洞利用工具,並且具有運行利用工具的能力。
即使能夠運行工具,也不一定完全提權成功。許多公開的漏洞利用工具,都不穩定。運行提權工具有可能造成目標主機宕機重啟。
許可權具有繼承性,高許可權運行的服務、程序,他的執行能力也是高許可權。一些web服務,資料庫應用,系統服務組件往往都在高許可權下運行。
例如,運維人員通常使用root許可權運行mysql。這時可以嘗試使用mysql提權漏洞,將低許可權的mysql用戶提權至mysql root 用戶。
mysql 本身具有 shell 執行環境。系統root身份運行的mysql,其 mysql root 許可權接近系統 root。
這種要具體情況具體分析,常見的比如:
這部分,可以參照這篇 blog。
本次分析的snap提權漏洞,屬於snapd 在使用api的時候,身份鑒權存在問題,允許地許可權用戶調用高許可權api,從而造成提權。
一篇完整的漏洞分析必須要包括:
下面是正文
snap是一個Linux系統上的包管理軟體。在Ubuntu18.04後默認預安裝到了系統中。
snapd 是負責管理本地安裝服務與在線應用商店通信的程序,隨著snap一起安裝,並且在root許可權下運行,這是提權的基本條件。
根據官方描述,服務進程snapd中提供的REST API服務對請求客戶端身份鑒別存在問題,從而導致了提權。Chris Moberly 已經公開了細節。
漏洞相關的更改:
漏洞位置在:
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
該函數,對 remoteAddr進行分割,標誌符為 「;」 ,將分割後得到的數組,for 循環。通過 HasPrefix 判別內容,對pid、uid、socket進行賦值。
;
HasPrefix
這裡存在一個問題: for循環中,有可能會對變數重複賦值。Split分割後的數組,如果存在多個 uid= 開頭的值,則 uid 的值將被後者覆蓋。
uid=
uid
例如,"uid=1000;pid=1100;uid=0",通過 ; 進行分割,得到[uid=1000,pid=1100,uid=0],該數組在迭代的時候:
"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。這是,漏洞形成的基本邏輯。
uid=0
如果是發漏洞預警,分析到這裡已經可以了。但如果是做漏洞研究,還遠遠不夠,還要進行漏洞上下文和利用技術分析。
除了找到漏洞成因,還要知道」漏洞從哪來,到哪去」。
漏洞邏輯函數:
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處理對象。則查找該函數調用關係。
ucrednetGet()
可以看到有n多調用,在api.go 文件中,有豐富邏輯代碼。隨進入分析。
api.go
ucrednetGet() 被重命名為 postCreateUserUcrednetGet() 和 runSnapctlUcrednetGet(), 查看調用邏輯:
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 ,即:
r.RemoteAddr
http.Request.RemoteAddr
傳入漏洞邏輯函數 ucrednetGet() 的參數 remoteAddr 為 http.Request.RemoteAddr。
查了下,http.Request.RemoteAddr 為 go 內建結構,之後查看 go 代碼。
這裡,分析了go中整個 SockaddrUnix 調用過程。這裡只簡單寫下要點:
fd.setAddr(laddr, raddr)
addr := fd.addrFunc()(rsa)
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 } }
```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. ......
```
分析到這裡,RemoteAddr 怎麼來的我們算整明白了:根據不同的套接字族,返回不同的地址。如果是通過 AF_UNIX 創建的套接字,將返回綁定的文件名。
RemoteAddr
那麼,哪裡調動了存在漏洞的函數?該漏洞有多大影響呢?
之前看到,漏洞函數在api.go 中進行調用:
ucrednetGet 重命名為 postCreateUserUcrednetGet, 在postCreateUser有調用:
ucrednetGet
postCreateUserUcrednetGet
...... 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,則創建的為特權用戶。
sudoer
之上,將漏洞分析的明明白白。此時其實可以自己寫出exp:
漏洞作者給的 exp,確實是這麼寫的。
漏洞修補的很粗暴,之前:
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 注入文本已經行不通。從而修補了漏洞。
漏洞十分的好用,snap < 2.37.1 以下版本均受影響。
因為在ubuntu 18.04 以後版本,默認安裝 snap ,並且測試時發現,一些vps 供應商 Ubuntu 16.04 同樣默認安裝snap。
以下vps服務商的 ubuntu 安裝鏡像均存在問題:
測得vps,除了阿里雲外,一打一個準。阿里雲ubuntu 鏡像中,不帶有snap,是我測的主機中,唯一不受漏洞影響的雲服務商。
修補很簡單,將 snap 升級到最新版就好了。
有 ubuntu vps 的同學,建議查看一下自己主機上snap的版本。
好久沒有認真做漏洞研究了,能完整分析出來漏洞,還是挺開心的。
很多漏洞作者,都會公布漏洞詳情。建議做漏洞分析的同學,不要先去看漏洞詳情。成長的過程在於對漏洞的摸索。
拿著分析文章,看一步調一步。沒有太大意義,沉澱不了自己的經驗。
本次分析的snap漏洞,唯一卡住的地方,是套接字那裡。沒辦法,去看漏洞作者詳細分析,才知道原來還有 AF_UNIX 用於本地進程通信。這個是我知識盲區,卡在這確實沒辦法。
ps:其實整篇下來,最難得部分是逆 go 的net標準庫。(笑
推薦閱讀: