Home Catch - HackTheBox
Post
Cancel

Catch - HackTheBox

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;

  1. Loop over all apk files in the directory $DROPBOX point to, which is /opt/mdm/apk_bin. We have write access to this directory.
  2. Obtain the filename, and remove the .apk extension in the filename and append _verified.apk
  3. Generate a 24 character long hex string, which the APK file will be renamed to in the $DROPBOX directory.
  4. Verify if the APK is signed by calling sig_check(), which uses jarsigner -verify <apk-file>, and check if the exit code is 0. I doubt we need to worry about this because testing jarsigner -verify on my machine, even on unsigned APK file, exited with 0 exit code, which is what the script is looking for.
  5. Call comp_check(), which decompile the APK file using apktool, and check if the compileSdkVersion defined in the android manifest is at least 18 using grep.
  6. Call app_check(), which extract the name of the app from res/values/strings.xml using the key app_name, ensure that it contains the string Catch, and pass it in an unsafe way to a shell command that create directory using mkdir + <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;

  1. Loading the apk file
  2. Selecting the “Full Edit” option
  3. Selecting the “Decode All Files” option
  4. Moving to the “Files” tab
  5. Opening res/values/strings.xml
  6. 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 using rockyou.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.
  • 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
This post is licensed under CC BY 4.0 by the author.
Contents