CVE-2021-33909 Sequoia Adversary Chain

A Linux Kernel Vulnerability That Results In Privilege Escalation

I wanted to gather more ideas before tackling more challenging data science work needed to finish up the B1-66ER episodic adversarial chain. In searching for an idea to do for an adversary chain this last week, I didn’t want to hassle with Windows VMs and even though the office overall was excited for the new MacBook Pros this last week, I didn’t want to venture into the MacOS environment either. Over the last year, most of my work has been in Linux primarily as a result of NVIDIA. NVIDIA has made it difficult to do GPU pass-through on Windows and Microsoft continues to hold back WSL2 GPU pass-through with the developer build tree unavailable to switch once Windows 11 went to beta. I decided Linux would be a good target since I recently ditched Windows on my main desktop, and have been running EndeavourOS (Arch-based Linux distro) full time.

I thought it would be interesting to look back and see what vulnerabilities have been released this year on Linux, if one captivated my interest, take a chance on making an adversary chain incorporating it. I came across CVE-2021-33909, an interesting Linux kernel privilege escalation, and decided to take a more in depth look at it. Remembering back, I think I told Alex I was going to spend a day trying to get it working and if not, I was going to pivot to something else, maybe tackle some Docker TTPs.

That logic didn’t age well.

Once I spent a good amount of time fighting to get it all working, I was determined and couldn’t turn back. Many of the parameters to perform this vulnerability are online, but some are scattered around and not in a central location. On top of this, Qualys who discovered the vulnerability released a crashing PoC, but did not provide many of the parameters and even mistakenly provided the wrong directions on how to execute it properly. Luckily, they had a video running the PoC with the right instructions and you could derive how to successfully execute their crashing PoC. Looking at Twitter, several researchers had discussed trying to execute the PoC, but couldn't get it working. Much of the headaches are surrounded by the system parameter requirements.

So let's discuss the system parameters:

  • Linux kernel 3.16 through 5.13.x before 5.13.4

  • Unprivileged or a low-privileged local user

  • At least 1 million free inodes

  • At least 5GB RAM free and from testing at least 8GB of system RAM

  • net.core.bpf_jit_enable = 1

  • kernel.unprivileged_bpf_disabled = 0

  • kernel.unprivileged_userns_clone = 1

The Linux Kernel parameters are quite concrete, either your target system meets the parameter or it doesn't. It is worth investigating newer kernels or slightly divergent kernels to see if it remains working. When performing the testing for this, I specifically tested on Ubuntu 20.04 original release. For some reason, when trying to install this within virtmanager no matter what settings I selected telling it not to update anything, the kernel always updated to newest. The only way to keep the original shipped kernel version was installing without network services and then once installed, turned network services back on. If you rely on ensuring your environments are the same throughout your environment, it's worth checking your kernel to make sure its the expected version.

Unprivileged or a low-privileged local user just indicates to not be root or execute this exploit as sudo. If you execute the exploit script as sudo it will automatically make the executable root bypassing the exploit entirely.

The next parameter is inodes. This really dives quickly into files systems and how they operate. I'll be totally honest here and say I was not too familiar with inodes when I set out to do this. Inodes are index nodes that serve as a unique identifier for a specific piece of metadata on a given filesystem. Different filesystems treat inodes differently. If you are using Ubuntu, the default filesystem is etx4, inodes are allocated at the time of the partition, and (I don't believe) they can be manipulated afterwards. Each inode in etx4 is allocated by every 15kb of storage space. If you want to read more about inodes I would look at this https://www.redhat.com/sysadmin/inodes-linux-filesystem. For this exploit to work you must have at least 1 million free inodes available.

The most time spent in getting Sequoia working was tackling the issue of the exploit crashing instead of proceeding. You need at least 5GB free of RAM, but in reality, if you're just testing this exploit you should have at least 8GB of system RAM give or take. If you meet all the perimeters but don't meet the threshold of system RAM available, the OS will crash, which will likely break your agent and through my testing in virtual machines the OS will not bounce and will require a hard VM reboot. This is by far the biggest risk in attempting to do this exploit.

The last three parameters are just ensuring kernel level settings. Both “net.core.bpf_jit_enable” and “unprivileged_bpf_disabled “ are two not so well written kernel verifiers that introduce security issues when it comes to bpf (Berkeley Packet Filter). To further learn about BPF and eBPF and these specific checks here is a good reference.  The last kernel verifier “unprivileged_userns_clone” allows unprivileged user access to namespaces. If you go searching for more information on this, many forums detail why this is extremely dangerous to enable. Yet, most commercial LinuxOSs keep it enabled because it creates user inconveniences. For example, electron appimages require it enabled for their sandbox to work.  

The Sequoia exploit works by utilizing seq_files which are virtual files that contain sequences of records. Many /proc files are seq_files a feature that was introduced in Linux Kernel dating back to around the 2.x days. The seq_file is designed to make it safer to interface with /proc than other methods at the time. Seq_file uses a pull type method of pulling data into the seq_file instead of pushing data. As a result of this pulling of information records must fit into a seq_file buffer, which is enlarged as needed. This exploit takes advantage of this by expanding the size of the seq_file buffer. This exploit utilizes two size buffers one is an unsigned 64bit integer and one that is a signed 32bit integer. So while it's perfectly fine to pass a large size of data to the unsigned 64bit buffer it's then passed to the signed 32bit buffer and is called by several functions. As a result, if an unprivileged local user creates, mounts, and deletes a deep directory structure with a total path length exceeding 1GB, and if the user then open and read /proc/self/mountinfo you get privilege escalation. 

Let's discuss how the adversary is constructed.

The Sequoia adversary has (8) TTPs. First, we perform a few discovery TTPs. We check to make sure you have enough RAM, have a user-set custom variable to ensure working Linux Kernel version, perform a check to ensure the proper flags are set for each kernel setting, and display the amount of inodes on the target system . We then create a directory by default in the home directory called sequoia but this could be changed depending on if that is a suitable location with enough allocated inodes free (the home folder is safe to assume will have enough inodes). We then ensure the check has been performed using facts and if all checks pass we then execute the exploit binary. If the exploit binary is successful it will launch a new agent as root user.

I wanted to also briefly discuss some Operator features I learned while developing this adversary chain that you also might find interesting.

Variables are an important tool in developing your adversary chain. If you're not familiar with variables, they are discoverable facts that when generated, will be stored and can be utilized within future TTPs. In Operator 1.3 variables can be scoped either globally, by range, or by agent. You can locate generated facts by navigating to the top right of the Operate tab and selecting the bulleted list icon. You can generate variable facts by writing your TTP in a way that will produce valid discoverable facts such as file paths, directory paths, ssh connections, IP addresses, domain names, passwords, and JSON blobs (#{json.technique_number.givenfactname}). You can then use these generated facts on following TTPs to carry over that known data. I have used facts before in previously released chains, but specifically in Sequoia, I used facts as a way to help ensure that each parameter is met before executing the exploit payload. 

Facts can take a bit to wrap your brain around initially, but once you start using them they will open up huge amounts of possibilities in your development of chains. If you haven’t taken a look at variables yet, I highly encourage you to play around with them. Here is further information on how to use variables in Operator https://github.com/preludeorg/community/tree/master/ttps#introducing-variables

The goal with Sequoia was to make it easier for users to land the exploit and not have to necessarily think about all the parameters. Basically, do the hard work once in discovering how it all worked, and then make it super simple to execute every time afterwards. I believe we were able to accomplish that.

References