import json from urllib.parse import parse_qs from bot import visit_url from mako.template import Template from mako.lookup import TemplateLookup import os from urllib.parse import urlparse, parse_qs from threading import Thread
MODULE_DIR = os.path.join(os.path.dirname(__file__), 'templates') ifnot os.path.exists(MODULE_DIR): try: os.makedirs(MODULE_DIR) except OSError as e: print(f"Warning: Could not create Mako module directory: {e}") MODULE_DIR = None
html_template = """ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Pixel Rainbow Name</title> <style> @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); body { font-family: 'Press Start 2P', cursive; background-color: #222; color: #fff; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; margin: 0; padding: 20px; box-sizing: border-box; } .container { background-color: #333; padding: 30px; border: 5px solid #555; box-shadow: 0 0 0 5px #444, 0 0 0 10px #333, 0 0 20px 10px #000; text-align: center; } h1 { font-size: 24px; color: #0f0; /* Green for a retro feel */ margin-bottom: 20px; text-shadow: 2px 2px #000; } label { font-size: 16px; color: #ccc; display: block; margin-bottom: 10px; } input[type="text"] { font-family: 'Press Start 2P', cursive; padding: 10px; font-size: 16px; border: 3px solid #555; background-color: #444; color: #fff; margin-bottom: 20px; outline: none; } input[type="submit"] { font-family: 'Press Start 2P', cursive; padding: 10px 20px; font-size: 16px; color: #fff; background-color: #007bff; border: 3px solid #0056b3; cursor: pointer; transition: background-color 0.2s; } input[type="submit"]:hover { background-color: #0056b3; } .name-display { margin-top: 30px; font-size: 32px; /* Base size for rainbow text */ font-weight: bold; padding: 10px; } .rainbow-text { /* Fallback for browsers that don't support background-clip */ color: #fff; /* Rainbow effect */ background: linear-gradient(to right, hsl(0, 100%, 50%), /* Red */ hsl(30, 100%, 50%), /* Orange */ hsl(60, 100%, 50%), /* Yellow */ hsl(120, 100%, 50%),/* Green */ hsl(180, 100%, 50%),/* Cyan */ hsl(240, 100%, 50%),/* Blue */ hsl(300, 100%, 50%) /* Magenta */ ); -webkit-background-clip: text; background-clip: text; color: transparent; /* Make the text itself transparent */ /* Animate the gradient */ animation: rainbow_animation 6s ease-in-out infinite; background-size: 400% 100%; text-shadow: none; /* Remove any inherited text-shadow */ } .rainbow-text span { /* Ensure individual spans also get the effect if we were to wrap letters */ -webkit-background-clip: text; background-clip: text; color: transparent; } @keyframes rainbow_animation { 0%, 100% { background-position: 0 0; } 50% { background-position: 100% 0; } } .instructions { font-size: 12px; color: #888; margin-top: 30px; } </style> </head> <body> <div class="container"> <h1>Pixel Name Display!</h1> <form method="GET" action="/"> <label for="name">Enter Your Name:</label> <input type="text" id="name" name="name_input" autofocus> <input type="submit" value="Show Fancy Name"> </form> % if name_to_display: <div class="name-display"> Your fancy name is: <div class="rainbow-text">NAME</div> </div> % endif <p class="instructions"> Enter a name and see it in glorious pixelated rainbow colors! </p> <p class="instructions"> Escaped characters: ${banned} </p> <input type="submit" value="Report Name" onclick="reportName()"> <script> function reportName() { // Get from query string const name = new URLSearchParams(window.location.search).get('name_input'); if (name) { fetch('/report', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name }) }) .then(response => { if (response.ok) { alert('Name reported successfully!'); } else { alert('Failed to report name.'); } }) .catch(error => { console.error('Error reporting name:', error); }); } } </script> </div> </body> </html> """
defescape_html(text): """Escapes HTML special characters in the given text.""" return text.replace("&", "&").replace("<", "<").replace(">", ">").replace("(", "(").replace(")", ")")
defrender_page(name_to_display=None): """Renders the HTML page with the given name.""" templ = html_template.replace("NAME", escape_html(name_to_display or"")) template = Template(templ, lookup=lookup) return template.render(name_to_display=name_to_display, banned="&<>()")
# Parse the path and extract query parameters parsed_url = urlparse(self.path) params = parse_qs(parsed_url.query) name = params.get("name_input", [""])[0] for b in banned: if b in name: name = "Banned characters detected!" print(b)
# Render and return the page self.send_response(200) self.send_header("Content-Type", "text/html") self.end_headers() self.wfile.write(render_page(name_to_display=name).encode("utf-8")) defdo_POST(self): # Handle POST requests to report names ifself.path == "/report": content_length = int(self.headers['Content-Length']) post_data = self.rfile.read(content_length) name = json.loads(post_data.decode('utf-8')).get("name", "") print(f"Received name: {name}") if name: print(f"Reported name: {name}") self.send_response(200) self.end_headers() self.wfile.write(b"Name reported successfully!") Thread(target=visit_url, args=(name,)).start() else: self.send_response(400) self.end_headers() self.wfile.write(b"Bad Request: No name provided.") else: self.send_response(404) self.end_headers()
defrun_server(server_class=HTTPServer, handler_class=SimpleHTTPRequestHandler, port=8000): server_address = ("0.0.0.0", port) httpd = server_class(server_address, handler_class) print(f"Starting http server on port {port}...") print(f"Access the page at http://0.0.0.0:{port}") try: httpd.serve_forever() except KeyboardInterrupt: print("\nServer stopped.") finally: httpd.server_close()
import tempfile import time from selenium import webdriver from selenium.webdriver.chrome.options import Options import sys from urllib.parse import quote
URL_BASE = "http://localhost:8000"
defvisit_url(name: str, timeout: int = 30): chrome_opts = Options() chrome_opts.add_argument("--headless=new") chrome_opts.add_argument("--disable-gpu") chrome_opts.add_argument("--disable-dev-shm-usage") chrome_opts.add_argument("--disable-extensions") chrome_opts.add_argument("--disable-popup-blocking") chrome_opts.add_argument("--blink-settings=imagesEnabled=false") # don't load images chrome_opts.add_argument("--log-level=3") # reduce logging chrome_opts.add_argument("--no-sandbox") user_data_dir = tempfile.mkdtemp(prefix="chrome-profile-") chrome_opts.add_argument(f"--user-data-dir={user_data_dir}") # use a temporary user data directory chrome_opts.add_argument("--user-data-dir=/tmp/chrome") # use a temporary user data directory driver = webdriver.Chrome(options=chrome_opts) try: driver.set_page_load_timeout(timeout) driver.set_script_timeout(5) driver.get(URL_BASE) driver.add_cookie({ "name": "flag", "value": "TFCCTF{~}", }) encoded_name = quote(name) driver.get(f"{URL_BASE}/?name_input={encoded_name}") # allow some time for JS to execute time.sleep(200) driver.quit() finally: driver.quit()
先看 main.py,猛地看上去,有一些被 ban 掉的字符,然后还有一个 template.render(),看上去就是一个典型的 SSTI。
看一下源代码一开始的导入部分,是 from mako.template import Template,那应该就是 mako 的 SSTI 了。
开启 web 看一下:
是个能把我们输入的 “name” 变成彩色字体进行输出的网页,有个输入框,然后有个 Show 和 Report 的功能。
前面说的 template.render() 是存在于 Show 的功能部分,因此这里可能存在 SSTI 。
defescape_html(text): """Escapes HTML special characters in the given text.""" return text.replace("&", "&").replace("<", "<").replace(">", ">").replace("(", "(").replace(")", ")")
所以我们要想办法绕过这些限制。
在以下这个部分:
1 2 3 4 5
defrender_page(name_to_display=None): """Renders the HTML page with the given name.""" templ = html_template.replace("NAME", escape_html(name_to_display or"")) template = Template(templ, lookup=lookup) return template.render(name_to_display=name_to_display, banned="&<>()")
defreplace_special_chars(a): # 定义要替换的字符和对应的替换模板 char_map = { '&': '${banned[0]}', '<': '${banned[1]}', '>': '${banned[2]}', '(': '${banned[3]}', ')': '${banned[4]}' } # 创建一个新的字符串,逐个字符替换 result = "" for char in a: if char in char_map: result += char_map[char] else: result += char return result