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標準庫。(笑

推薦閱讀:

相关文章