Local privilege escalation via snapd socket
Affects | Status | Importance | Assigned to | Milestone | |
---|---|---|---|---|---|
snapd |
Fix Released
|
High
|
Zygmunt Krynicki | ||
snapd (Ubuntu) |
Fix Released
|
High
|
Unassigned | ||
Trusty |
Fix Released
|
High
|
Unassigned | ||
Xenial |
Fix Released
|
High
|
Unassigned | ||
Bionic |
Fix Released
|
High
|
Unassigned | ||
Cosmic |
Fix Released
|
High
|
Unassigned | ||
Disco |
Fix Released
|
High
|
Unassigned |
Bug Description
NOTE: Hello, snap team! The below is my full 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.
An exploit is attached that works 100% of the time on fresh, default installations of Ubuntu Server and Desktop.
Researcher: Chris Moberly @ The Missing Link Security
# 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:/
Management of locally installed snaps and communication with this online store are partially handled by a systemd service called "snapd" (https:/
# Vulnerability Overview
## Interesting Linux OS Information
The snapd service is described in a systemd service unit file located at /lib/systemd/
Here are the first few lines:
```
[Unit]
Description=Snappy daemon
Requires=
```
This leads us to a systemd socket unit file, located at /lib/systemd/
The following lines provide some interesting information:
```
[Socket]
ListenStream=
ListenStream=
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-
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.
## 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:/
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:/
Let's look at this line:
```
ucred, err := getUcred(
```
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.
...
109: ucred, err := getUcred(
=> 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{
}
```
...and then a bit more in this one:
```
func (wa *ucrednetAddr) String() string {
return fmt.Sprintf(
}
```
..and is finally parse by this function:
```
func ucrednetGet(
...
for _, token := range strings.
var v uint64
...
} else if strings.
if v, err = strconv.
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.
...
=> 41: for _, token := range strings.
...
(dlv) print remoteAddr
"pid=5127;
```
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=
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)
...
=> 210: func (c *conn) LocalAddr() Addr {
...
(dlv) print c.fd
...
laddr: net.Addr(
Name: "/run/snapd.
Net: "unix",},
raddr: net.Addr(
```
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.
client_
# Connect to the snap daemon
client_
```
Now watch what happens in the debugger when we look at the remoteAddr variable again:
```
> github.
...
=> 41: for _, token := range strings.
...
(dlv) print remoteAddr
"pid=5275;
```
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.
...
=> 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:/
*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:/
**IMPORTANT: YOU MUST CREATE AN UBUNTU ACCOUNT ONLINE AND UPLOAD AN SSH KEY TO EXPLOIT**
dirty_sock.py -u "<email address hidden>" -k "id_rsa"
The exploit will do the following:
- Generate a random name for the sock file, including the "dirty sock" of ";uid=0;"
- Create a socket bound to this file
- Initiate a connection from the dirty sock to the snapd API
- POST the correct specs to /v2/create-user
- snapd queries the Ubuntu developer portal to gather the username and SSH public key associated with the account your provide
- snapd creates a local Linux user based on those details
- Verify a successful response
- SSH to localhost using the new account and your SSH private key
From this new account, you can execute "sudo" commands with no password.
# Protection / Remediation
As far as fixing this, the snap folks will know better than I what the best route is. I would advise sticking with the variables output from the golang standard libary - get rid of all that concatinating and re-parsing stuff in ucrednet.go.
Thanks for reading!!!
CVE References
description: | updated |
description: | updated |
description: | updated |
Changed in snapd: | |
importance: | Undecided → Critical |
no longer affects: | ubuntu |
Changed in snapd: | |
status: | New → Triaged |
assignee: | nobody → Zygmunt Krynicki (zyga) |
Hello Chris, thank you for contacting us. This is absolutely beautiful work, well done. I'll get the snapd team working on this.
Thanks