Catch is a very interesting medium-rated linux box on HackTheBox. It starts with some light reversing of an android application to obtain an access token for the Let’s Chat API running on the host. This token will lead you to some credentials in the chat logs, which are reused in the Cachet instance running on the host, which you can exploit to leak the application config and obtain a password that was reused for SSH login. Once inside the box, you will be exploiting a custom cron job that process user controlled files in an unsafe way, leading to code execution as root
.
Info
Recon
NMAP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# Nmap 7.70 scan initiated Mon Mar 14 06:44:58 2022 as: nmap -sC -sV -oN nmap.txt -v 10.10.11.150
Nmap scan report for catch.htb (10.10.11.150)
Host is up (0.36s latency).
Not shown: 995 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.4 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Catch Global Systems
3000/tcp open ppp?
| fingerprint-strings:
| GenericLines, Help, RTSPRequest:
| HTTP/1.1 400 Bad Request
| Content-Type: text/plain; charset=utf-8
| Connection: close
| Request
| GetRequest:
| HTTP/1.0 200 OK
| Content-Type: text/html; charset=UTF-8
| Set-Cookie: i_like_gitea=f9ae0e9b88600477; Path=/; HttpOnly
| Set-Cookie: _csrf=1thsrzl3dfhhgLXqP2EYU0Yr2VU6MTY0NzIzNjc0NjEwNzA3MDM1NQ; Path=/; Expires=Tue, 15 Mar 2022 05:45:46 GMT; HttpOnly; SameSite=Lax
| Set-Cookie: macaron_flash=; Path=/; Max-Age=0; HttpOnly
| X-Frame-Options: SAMEORIGIN
| Date: Mon, 14 Mar 2022 05:45:46 GMT
| <!DOCTYPE html>
| <html lang="en-US" class="theme-">
| <head data-suburl="">
| <meta charset="utf-8">
| <meta name="viewport" content="width=device-width, initial-scale=1">
| <meta http-equiv="x-ua-compatible" content="ie=edge">
| <title> Catch Repositories </title>
| <link rel="manifest" href="data:application/json;base64,eyJuYW1lIjoiQ2F0Y2ggUmVwb3NpdG9yaWVzIiwic2hvcnRfbmFtZSI6IkNhdGNoIFJlcG9zaXRvcmllcyIsInN0YXJ0X3VybCI6Imh0dHA6Ly9naXRlYS5jYXRjaC5odGI6MzAwMC8iLCJpY29ucyI6W3sic3JjIjoiaHR0cDovL2dpdGVhLmNhdGNoLmh0Yjoz
| HTTPOptions:
| HTTP/1.0 405 Method Not Allowed
| Set-Cookie: i_like_gitea=e96c0ccbcd15e95b; Path=/; HttpOnly
| Set-Cookie: _csrf=idequJw78CbNGhYDHkv0-BdGyA06MTY0NzIzNjc1NDIxOTc0MjU3Nw; Path=/; Expires=Tue, 15 Mar 2022 05:45:54 GMT; HttpOnly; SameSite=Lax
| Set-Cookie: macaron_flash=; Path=/; Max-Age=0; HttpOnly
| X-Frame-Options: SAMEORIGIN
| Date: Mon, 14 Mar 2022 05:45:54 GMT
|_ Content-Length: 0
5000/tcp open upnp?
| fingerprint-strings:
| DNSStatusRequestTCP, DNSVersionBindReqTCP, Help, RPCCheck, RTSPRequest, SMBProgNeg, ZendJavaBridge:
| HTTP/1.1 400 Bad Request
| Connection: close
| GetRequest:
| HTTP/1.1 302 Found
| X-Frame-Options: SAMEORIGIN
| X-Download-Options: noopen
| X-Content-Type-Options: nosniff
| X-XSS-Protection: 1; mode=block
| Content-Security-Policy:
| X-Content-Security-Policy:
| X-WebKit-CSP:
| X-UA-Compatible: IE=Edge,chrome=1
| Location: /login
| Vary: Accept, Accept-Encoding
| Content-Type: text/plain; charset=utf-8
| Content-Length: 28
| Set-Cookie: connect.sid=s%3AhXxhtsBrUI_XDU_i3ZgGjgu033W-HpWG.KVpt61zx0JMhA1rttVRXksZCCwcY0tu9zcySTvWADoI; Path=/; HttpOnly
| Date: Mon, 14 Mar 2022 05:45:50 GMT
| Connection: close
| Found. Redirecting to /login
| HTTPOptions:
| HTTP/1.1 200 OK
| X-Frame-Options: SAMEORIGIN
| X-Download-Options: noopen
| X-Content-Type-Options: nosniff
| X-XSS-Protection: 1; mode=block
| Content-Security-Policy:
| X-Content-Security-Policy:
| X-WebKit-CSP:
| X-UA-Compatible: IE=Edge,chrome=1
| Allow: GET,HEAD
| Content-Type: text/html; charset=utf-8
| Content-Length: 8
| ETag: W/"8-ZRAf8oNBS3Bjb/SU2GYZCmbtmXg"
| Set-Cookie: connect.sid=s%3Aq0XcpC9e25YC_BRaqUaIpr7SAkXEtcer.%2BQNjRwdTsHh5gZU8NxXCD0%2F02xispYhj9lSo2TGZgmQ; Path=/; HttpOnly
| Vary: Accept-Encoding
| Date: Mon, 14 Mar 2022 05:45:55 GMT
| Connection: close
|_ GET,HEAD
8000/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-favicon: Unknown favicon MD5: 69A0E6A171C4ED8855408ED902951594
| http-methods:
|_ Supported Methods: GET HEAD OPTIONS
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Catch Global Systems
2 services unrecognized despite returning data. If you know the service/version, please submit the following fingerprints at https://nmap.org/cgi-bin/submit.cgi?new-service :
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
---[snip]---
SF:"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x20close\r\n\r\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Mon Mar 14 06:47:39 2022 -- 1 IP address (1 host up) scanned in 161.48 seconds
Web Services
The main service on port 80 appears to be static, with a download link to an APK file;
Gitea
The service on port 3000 is an instance of Gitea version 1.14.1;
Viewing the source of the page showed a subdomain;
Trying common usernames and passwords in the login page didn’t work. Only one user was found in the explore tab, and doesn’t have any repo;
Let’s Chat
The service on port 5000 is an instance of Let’s Chat, which is available at https://github.com/sdelements/lets-chat. This is a self-hosted chat app for small teams;
Testing simple usernames and passwords in the login page didn’t yield anything.
Cachet
The service on port 8000 is an instance of Cachet, and according to it’s GitHub page, it is a a beautiful and powerful open source status page system;
At the bottom of the page, there is a link to /dashboard
and /subscribe
. The /subscribe
page gave a 500 internal server error, while the /dashboard
redirected to /auth/login
, which gave a login form;
Some quick searches showed that this service is vulnerable to unauthenticated SQL injection;
I looked for any sort of PoC for this vulnerability, but I couldn’t find any. So I installed the application locally to see if I can figure it out, but I couldn’t. The vulnerable function is defined in app/Models/Traits/SearchableTrait.php
;
I modified this code in an attempt to see when it’s being used, but that didn’t work. So I just moved on.
APK Reversing
The homepage of the main site links to an APK file at /catchv1.0.apk
. I checked for other APK files that may exist on the server by bruteforcing the version number, but none was found.
I used apktool
to unpack the APK file, which will give me the decoded resources used by the app, but the code will all be in .smali
;
Using unzip
, I extracted the APK file to obtain the .dex
file;
Using dex2jar
, I converted the .dex
file into .jar
, and decompiled it using cfr
;
As per the AndroidManifest.xml fille decoded by apktool
, the class com.example.acatch.MainActivity
is the main activity of the application, which is the class that will be called when the application is launched;
The application appears to be an android webview application, which is a technology used by android apps that make them act like browsers for accessing websites. Looking through the code, a HTTPs URL was found;
This doesn’t make sense because port 443 is not open on the server;
Going to the subdomain over HTTP also showed the exact same homepage.
I made a mistake at this stage of the box and just ignored the APK, since the main class doesn’t appear to be doing anything more than viewing that URL, which isn’t even valid. As it turns out, there are some very interesting entries in the XML file res/values/strings.xml
, which is used by android to define strings in key-value mappings that make it easy to reuse data throughout the application. Going through the file, I found some very interesting entries;
The token for Gitea just didn’t work, so I’m guessing it had been revoked. The token for Let’s Chat is a base64 string, so I decoded it, which gave me 61b86aead984e2451036eb16:d588468ff8bae446379a57fa2b4e63a2368224336b5949c5
. This sort of token format is usually used in HTTP Authorization: bearer header, which contains the username and password separated by a colon. Testing the values as username and password in the login form of Let’s Chat didn’t work. So I tried passing the token in the authorization header, and it appeared to work, but just hanged;
Viewing the source code of the page, the authentication appeared to have worked;
Let’s Chat
Let’s Chat has a powerful API, and it’s documentation is available at https://github.com/sdelements/lets-chat/wiki/API. With the help of the guide, I was able to enumerate valid users;
This gave me four usernames: admin
, john
, will
, and lucas
. There are 3 rooms in the chat service, none of which are password-protected;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[
{
"id": "61b86b28d984e2451036eb17",
"slug": "status",
"name": "Status",
"description": "Cachet Updates and Maintenance",
"lastActive": "2021-12-14T10:34:20.749Z",
"created": "2021-12-14T10:00:08.384Z",
"owner": "61b86aead984e2451036eb16",
"private": false,
"hasPassword": false,
"participants": []
},
{
"id": "61b8708efe190b466d476bfb",
"slug": "android_dev",
"name": "Android Development",
"description": "Android App Updates, Issues & More",
"lastActive": "2021-12-14T10:24:21.145Z",
"created": "2021-12-14T10:23:10.474Z",
"owner": "61b86aead984e2451036eb16",
"private": false,
"hasPassword": false,
"participants": []
},
{
"id": "61b86b3fd984e2451036eb18",
"slug": "employees",
"name": "Employees",
"description": "New Joinees, Org updates",
"lastActive": "2021-12-14T10:18:04.710Z",
"created": "2021-12-14T10:00:31.043Z",
"owner": "61b86aead984e2451036eb16",
"private": false,
"hasPassword": false,
"participants": []
}
]
Dumping the messages in the first channel gave a credential;
Trying this credential in the login page of Cachet instance at http://catch.htb:8000/auth/login
, I was able to log in;
Cachet
Cachet has a few vulnerabilities, which are well explained in the blog post https://blog.sonarsource.com/cachet-code-execution-via-laravel-configuration-injection. The newline injection vulnerability in the web app immediately caught my attention, as it can be used to override the parameters defined in the configuration file, which is in .env
. Going through the file, the parameter DB_HOST
looks interesting as it defined the address of the backend database to use, which by default is MySQL.
The newline injection can be triggered when updating the mail settings according to the article;
Here is a sample of the configuration file;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
APP_ENV=production
APP_DEBUG=false
APP_URL=http://localhost
APP_KEY=SomeRandomString
DB_DRIVER=mysql
DB_HOST=localhost
DB_DATABASE=cachet
DB_USERNAME=homestead
DB_PASSWORD=secret
DB_PORT=null
DB_PREFIX=null
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_DRIVER=sync
CACHET_EMOJI=false
MAIL_DRIVER=smtp
MAIL_HOST=mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ADDRESS=null
MAIL_NAME=null
MAIL_ENCRYPTION=tls
---[snip]---
Doing a normal update generated the following request;
Notice how the parameter names are all submitted in the request as some sort of index key inside the variable config
. This hints at a possible mass assignment vulnerability. So I changed the config[mail_host]
submitted in the request to config[db_host]
and set the value to my IP, then start up metasploit and used the module auxiliary/server/capture/mysql
to capture MySQL handshake, and it worked;
Forwarding the request, metasploit captured a challenge for the user will
;
To my surprise, john
was unable to crack the hash using rockyou.txt
, which is the standard for HTB boxes when it comes to crackable passwords;
Looking further into the article mentioned above, I learned that Cachet is also vulnerable to configuration leaks. This happens when you supply a parameter name wrapped in ${}
, like ${DB_HOST}
. Using this, I was able to leak the plain MySQL password by setting the value of the Mail From Address
field to ${DB_PASSWORD}
, submitting the request, and reloading the page;
This gave me the password;
Since metasploit earlier told me the database user is will
, I tested the credential on the SSH service running on the host, and I was able to login;
Privesc
No sudo permission is available for the user will. Running LinPEAS also didn’t show anything very interesting at a quick glance. Running pspy
on the host showed something;
Going to the /opt/mdm
directory, I found the shell script that’s being run as root;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#!/bin/bash
###################
# Signature Check #
###################
sig_check() {
jarsigner -verify "$1/$2" 2>/dev/null >/dev/null
if [[ $? -eq 0 ]]; then
echo '[+] Signature Check Passed'
else
echo '[!] Signature Check Failed. Invalid Certificate.'
cleanup
exit
fi
}
#######################
# Compatibility Check #
#######################
comp_check() {
apktool d -s "$1/$2" -o $3 2>/dev/null >/dev/null
COMPILE_SDK_VER=$(grep -oPm1 "(?<=compileSdkVersion=\")[^\"]+" "$PROCESS_BIN/AndroidManifest.xml")
if [ -z "$COMPILE_SDK_VER" ]; then
echo '[!] Failed to find target SDK version.'
cleanup
exit
else
if [ $COMPILE_SDK_VER -lt 18 ]; then
echo "[!] APK Doesn't meet the requirements"
cleanup
exit
fi
fi
}
####################
# Basic App Checks #
####################
app_check() {
APP_NAME=$(grep -oPm1 "(?<=<string name=\"app_name\">)[^<]+" "$1/res/values/strings.xml")
echo $APP_NAME
if [[ $APP_NAME == *"Catch"* ]]; then
echo -n $APP_NAME|xargs -I {} sh -c 'mkdir {}'
mv "$3/$APK_NAME" "$2/$APP_NAME/$4"
else
echo "[!] App doesn't belong to Catch Global"
cleanup
exit
fi
}
###########
# Cleanup #
###########
cleanup() {
rm -rf $PROCESS_BIN;rm -rf "$DROPBOX/*" "$IN_FOLDER/*";rm -rf $(ls -A /opt/mdm | grep -v apk_bin | grep -v verify.sh)
}
###################
# MDM CheckerV1.0 #
###################
DROPBOX=/opt/mdm/apk_bin
IN_FOLDER=/root/mdm/apk_bin
OUT_FOLDER=/root/mdm/certified_apps
PROCESS_BIN=/root/mdm/process_bin
for IN_APK_NAME in $DROPBOX/*.apk;do
OUT_APK_NAME="$(echo ${IN_APK_NAME##*/} | cut -d '.' -f1)_verified.apk"
APK_NAME="$(openssl rand -hex 12).apk"
if [[ -L "$IN_APK_NAME" ]]; then
exit
else
mv "$IN_APK_NAME" "$IN_FOLDER/$APK_NAME"
fi
sig_check $IN_FOLDER $APK_NAME
comp_check $IN_FOLDER $APK_NAME $PROCESS_BIN
app_check $PROCESS_BIN $OUT_FOLDER $IN_FOLDER $OUT_APK_NAME
done
cleanup
After studying the code for a while, I undertood what it does;
- Loop over all apk files in the directory
$DROPBOX
point to, which is/opt/mdm/apk_bin
. We have write access to this directory. - Obtain the filename, and remove the
.apk
extension in the filename and append_verified.apk
- Generate a 24 character long hex string, which the APK file will be renamed to in the
$DROPBOX
directory. - Verify if the APK is signed by calling
sig_check()
, which usesjarsigner -verify <apk-file>
, and check if the exit code is 0. I doubt we need to worry about this because testingjarsigner -verify
on my machine, even on unsigned APK file, exited with0
exit code, which is what the script is looking for. - Call
comp_check()
, which decompile the APK file usingapktool
, and check if thecompileSdkVersion
defined in the android manifest is at least 18 usinggrep
. - Call
app_check()
, which extract the name of the app fromres/values/strings.xml
using the keyapp_name
, ensure that it contains the stringCatch
, and pass it in an unsafe way to a shell command that create directory usingmkdir + <app_name>
.
After some local testing, step 6 appears to be exploitable if we can inject a command in the name of the app we drop into the /opt/mdm/apk_bin
directory. My first attempt was to do this using apktool
. I used the catchv1.0.apk app downloaded earlier from the site. For some reasons, apktool
is unable to compile the decompiled and modified APK, so I started looking for other alternatives.
Since this is a very simple change, as we are only modifying the name of the app, I started looking for android apps that could get the job done, and I found a great one named APK Editor Pro. So installed it, and did it on android by;
- Loading the apk file
- Selecting the “Full Edit” option
- Selecting the “Decode All Files” option
- Moving to the “Files” tab
- Opening
res/values/strings.xml
- Changing line 30 from
<string name="app_name">Catch</string>
to<string name="app_name">Catch; /dev/shm/payload</string>
I then rebuit the apk, which saved it as gen_signed.apk
. I copied it over to the web directory on my attack box, and created a file named payload
with my reverse shell. I chose this approach to avoid having characters in the name of the app that may not be allowed.
I SSHed back into the box as will, and download the gen_signed.apk
and payload
file to /dev/shm
. I made the payload executable, and moved the APK to /opt/mdm/apk_bin
, and waited for the cron task to process it;
After a few seconds, my malicious APK file was processed, and I got a reverse shell on the box as root;
Summary
- Identified port 22, 80, 3000 (Gitea), 5000 (Let’s Chat), and 8000 (Cachet) using
nmap
- Reversed the APK file obtained from the main site, and recovered some tokens.
- One of the tokens gave access to the API of Let’s Chat
- Chat logs obtained using the API revealed a credential for john
- This credential is reused on the Cachet instance.
- Inside Cachet as john
- Exploited a mass assignment vulnerability to hijack backend MySQL server used, and capture handshake using metasploit’s
auxiliary/server/capture/mysql
. The handshake could not be cracked usingrockyou.txt
- Exploited a configuration leak to obtain the plaintext password of the MySQL backend, which gave me access to the box as user will over SSH.
- Exploited a mass assignment vulnerability to hijack backend MySQL server used, and capture handshake using metasploit’s
- Inside the box as will
- Identified a cron task running as root that calls
/opt/mdm/verify.sh
- This script process APK files in a directory we can write to.
- Studied the code, and built a malicious APK file that exploited a command injection vulnerability in how the name of the app as defined in
res/values/strings.xml
is processed, which gave me access to the box as root
- Identified a cron task running as root that calls