日志Python安全之SSTI——Flask/Jinja2

发布于:2025-03-15 ⋅ 阅读:(11) ⋅ 点赞:(0)

ssti的概念和模板引擎介绍等基础知识前面已经学过了,接下来直接进入正题

先了解flask/jinja2:

flask:

用python编写的一个框架,集成 Jinja2 模板引擎(用于动态生成 HTML 内容)。

Flask 的核心组件:

(1)路由:路由是 Web 应用程序中的一个机制,用于HTTP请求如GETPOST 等的 URL 路径与后端的处理逻辑(通常是 Python 函数关联起来,在 Flask 中,路由通过装饰器 @app.route() 来定义。找个代码解释:

基本路由:

from flask import Flask

app = Flask(__name__)

# 定义路由
@app.route('/')
def home():
    return "Hello, World!"

@app.route('/about')
def about():
    return "This is the about page."

@app.route('/'):将‘/’目录,映射到home函数,也就是当用户访问:http://localhost:5000/ 时,Flask 会调用home函数,并返回"Hello, World!"。

@app.route('/about'):同理

动态路由:

@app.route('/user/<username>')
def show_user_profile(username):
    return f"User: {username}"

<username>:动态部分,可以匹配任意字符串。

当用户访问 http://localhost:5000/user/john 时,username的值会是‘john’,Flask 会调用 show_user_profile 函数并返回“User:john”。

指定 HTTP 方法:

默认情况下,路由只响应 GET 请求。但是可以通过‘methods'参数指定路由支持的 HTTP 方法。

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        return "Login submitted!"
    else:
        return "Show login form."

GET请求访问/login时,返回登录表单,post请求提交表单时,返回Login submitted!

(2)请求和响应

Flask 提供了 request 对象来访问 HTTP 请求数据,以及 make_response 函数来生成 HTTP 响应。

request对象中常用的属性:

属性/方法 说明
request.method 获取 HTTP 请求方法(如 GETPOST 等)。
request.url 获取完整的请求 URL(如 http://example.com/path?query=value)。
request.path 获取 URL 的路径部分(如 /path)。
request.args 获取 URL 中的查询参数(GET 请求的参数),返回一个字典。
request.form 获取表单数据(POST 请求的数据),返回一个字典。
request.json 获取 JSON 格式的请求体(适用于 POSTPUT 请求)。
request.headers 获取请求的头部信息,返回一个字典。
request.cookies 获取客户端发送的 Cookies,返回一个字典。
request.files 获取上传的文件,返回一个字典。
request.remote_addr 获取客户端的 IP 地址。

默认情况下,视图函数返回的字符串会被 Flask 包装成一个 HTTP 响应,状态码为200 OK,内容类型为text/html

使用make_response自定义响应,make_response的第一个参数是响应内容,第二个参数是状态码,并且可以通过response.headers设置响应头部。找到一个常规代码参考:

from flask import Flask, request, make_response

app = Flask(__name__)

@app.route('/greet')
def greet():
    name = request.args.get('name', 'Guest')  # 获取 URL 参数
    response = make_response(f"Hello, {name}!")  # 生成响应
    response.set_cookie('username', name)  # 设置 Cookie
    return response

相应里还有一些设置cookie,返回文件啥的,看这个文章吧:https://blog.csdn.net/2401_88743143/article/details/146267602?sharetype=blogdetail&sharerId=146267602&sharerefer=PC&sharesource=2401_88743143&sharefrom=mp_from_link

(3)模板渲染 

Flask 默认使用 Jinja2 模板引擎来渲染 HTML 模板。通过 render_template 函数,可以将动态数据传递给模板并生成最终的 HTML。

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/hello')
def hello():
    name = "World"
    return render_template('hello.html', name=name)

templates/hello.html 文件中:

<!DOCTYPE html>
<html>
<head>
    <title>Hello</title>
</head>
<body>
    <h1>Hello, {{ name }}!</h1>
</body>
</html>

 Jinja2 模板引擎:

(1)Jinja2 的基本语法:

  • 变量:{{ variable }}

  • 控制结构: {% ... %} ,支持条件判断、循环等。

  • 过滤器:{{ variable|filter }} ,用于对变量进行处理。

  • 注释:用 {# ... #} 表示,注释内容不会被渲染。

 偷个代码方便分析:

应用示例:

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def home():
    # 定义上下文数据
    context = {
        'title': 'Home Page',
        'name': 'John',
        'is_student': True
    }
    # 渲染模板并返回响应
    return render_template('home.html', **context)

if __name__ == '__main__':
    app.run(debug=True)

模板文件:在templates/home.html文件中:

<!DOCTYPE html>
<html>
<head>
    <title>{{ title }}</title>
</head>
<body>
    <h1>Hello, {{ name }}!</h1>
    {% if is_student %}
        <p>You are a student.</p>
    {% else %}
        <p>You are not a student.</p>
    {% endif %}
</body>
</html>

{{ title }}会被替换为'Home Page'。{{ name }}会被替换为'John'。根据is_student的值,显示不同的内容。

好了,继续深入了解一下吧:

下面是一些基础的魔术方法:

  •  __class__:返回对象所属的类。
s = "hello"
print(s.__class__)  # 输出: <class 'str'>
  •  __bases__:以元组的形式返回一个类直接继承的父类。
class A:
    pass

class B(A):
    pass

print(B.__bases__)  # 输出: (<class '__main__.A'>,)

B继承自 A,所以B.__bases__返回(<class '__main__.A'>,)。

  • __mro__:返回方法解析顺序(Method Resolution Order, MRO),即类继承的顺序。
class A:
    pass

class B(A):
    pass

class C(B):
    pass

print(C.__mro__)
# 输出: (<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)

C.__mro__返回从C到object的继承链。

  • __subclasses__():返回一个类的所有直接子类。
class A:
    pass

class B(A):
    pass

class C(A):
    pass

print(A.__subclasses__())
# 输出: [<class '__main__.B'>, <class '__main__.C'>]

A.__subclasses__()返回 A 的所有直接子类 BC

  • __init__:在创建类的对象时自动调用,即初始化对象时调用,用于设置对象的初始状态。
    class Person:
        def __init__(self, name):
            self.name = name
    
    p = Person("John")
    print(p.name)  # 输出: John

    __init__方法在创建Person对象时被调用,用于初始化name属性。

代码解释:

  • self 是一个指向当前对象的引用,用于访问对象的属性和方法。

  • name 是一个参数,表示创建对象时需要传递的名字。

  • self.name = name 将传递的 name 参数赋值给对象的 name 属性。

  • __globals__:返回函数所在命名空间的全局变量字典。
x = 10

def foo():
    y = 20
    print(foo.__globals__)

foo()
# 输出: {'x': 10, ...(其他全局变量)}

foo.__globals__ 返回 foo 函数所在模块的全局变量字典。

注入思路|payload

感觉这个好麻烦ing

注入思路:随便找一个内置类对象用__class__拿到他所对应的类,用__bases__拿到基类(<class 'object'>),用__subclasses__()拿到子类列表,在子类列表中直接寻找可以利用的类getshell

''.__class__.__bases__[0].__subclasses__()
().__class__.__mro__[2].__subclasses__()
request.__class__.__mro__[1]

接下来只要找到能够利用的类(方法、函数)就好了:

找可利用的类(脚本):

from flask import Flask,request
from jinja2 import Template
search = 'eval'   
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
    num += 1
    try:
        if search in i.__init__.__globals__.keys():
            print(i, num)
    except:
        pass

这个里面大哥总结了许多python2、python3通用payload:https://xz.aliyun.com/news/7341?time__1311=YqfxgiDt5eq05DK5qCqGKK4Qwtxjh2u8bD&u_atoken=9664eb0ebd7cdbb8b7bb128aec035d0f&u_asig=1a0c399b17419533867796649e0111#toc-10

Python安全之SSTI——Flask/Jinja2-腾讯云开发者社区-腾讯云

好吧做个例题巩固一下

[WesternCTF2018]shrine

分析代码, app.config['FLAG'] = os.environ.pop('FLAG')这段代码将FLAG存储到 app.config里并且从环境变量里删除它,@app.route('/shrine/') def shrine(shrine): def safe_jinja(s): s = s.replace('(', '').replace(')', '') blacklist = ['config', 'self'],这是一个过滤,先将所有的()替换成‘ ’,然后绕过直接出现'config', 'self',所以需要间接访问,最后渲染,因为将FLAG放到了app.config中,所以需要访问config

构造payload:

/shrine/{{url_for.__globals__['current_app'].config['FLAG']}}

访问 /shrine/ 路径时,Flask 会调用 shrine 函数来处理请求,shrine 函数会将用户输入作为模板字符串渲染,从而触发服务器端模板注入(SSTI)漏洞

了解了一下payload的执行进程:

url_for

url_for 是 Flask 的一个全局函数,用于生成 URL。

url_for.__globals__ 返回 url_for 函数所在模块的全局变量字典。

url_for.__globals__['current_app']

current_app 是 Flask 的一个全局变量,指向当前的 Flask 应用实例。

通过 url_for.__globals__['current_app'],可以获取当前的 Flask 应用实例。

current_app.config['FLAG']

current_app.config 是 Flask 应用的配置字典。

current_app.config['FLAG'] 获取配置项 FLAG 的值。

当时不理解payload里也有config为啥也能绕过,搜了一下知道了黑名单的局限性:

黑名单机制只会将 直接出现的 config 替换为 None。在这个 Payload 中,config 并不是直接出现的,而是通过 url_for.__globals__['current_app'].config 访问的。黑名单机制无法检测到这种用全局函数间接访问方式。

最后得到flag

看别人的wp还用了一种payload

/shrine/{{get_flashed_messages.__globals__['current_app'].config['FLAG']}}

get_flashed_messages 是 Flask 的一个全局函数,用于获取闪现消息(flashed messages)。闪现消息是 Flask 中用于在请求之间传递消息的一种机制。

通过 get_flashed_messages.__globals__,可以访问 Flask 应用的全局变量。

自己写payload还是difficult滴,但是感觉没太用到上面的方法,还是得多做点题,多分析大佬们构造的payload

其他:这个对有些模块的解释还是不错的CTF_Web:从0学习Flask模板注入(SSTI)_ctf flask-CSDN博客