宝塔面板未授权访问phpmyadmin
关于宝塔
之前是用过一段时间的宝塔的,感觉很方便,功能也很齐全。
但是宝塔一直被披露有很多安全漏洞。
今天看了源码才知道是用flask写的。
这次的未授权访问phpmyadmin影响的版本:
Linux正式版7.4.2
Linux测试版7.5.13
Windows正式版6.8
环境搭建
docker pull tim2docker/baota2:v1
sudo docker run -d -it --name="pma" -p 8888:8888 -p 888:888 -p 2279:2279 tim2docker/baota2:v1
sudo docker exec -it pma bt restart
sudo docker exec -it pma bt default
使用docker简单方便快速的搭建出环境
初始化完成后访问他给定的url,使用账号密码登录进入宝塔面板
并进入软件管理启动php,mysql,nginx
至此,环境搭建完毕!
漏洞复现
直接访问http://192.168.112.129:888/pma
可以很清楚的看到已经成功的进入到了phpmyadmin,并且已经是登录状态了,绕过了权限认证!
简单叙述一下原理
1. 关于宝塔中用户进入phpmyadmin的认证
在7.4.2点击访问phpmyadmin有两种方式:
- 通过
安全面板授权
访问- 这种方式访问的url为:
http://192.168.112.129:2279/phpmyadmin/index.php
- 可以看到是通过宝塔的端口以及认证实现的
- 这种方式访问的url为:
- 通过
Nginx/Apache
访问- 这种方式访问的url为:
http://192.168.112.129:888/phpmyadmin_c8483cecf22859f0/index.php
- 这是为phpmyadmin服务单独开放的888端口
- 这种方式访问的url为:
然而在之前的版本只有一种方式:
- 通过
Nginx/Apache
访问
这不得不归功于7.4.2版本的升级:
更新日志:
1、增加phpMyAdmin安全访问模块
2、调整面板自带PHP解释器的可靠性
3、修复因增加ols环境导致的一些问题
4、修复小程序扫码登录和绑定问题
5、修改sessionid为通用key
然后贴一下phpmyadmin本身的认证机制:
phpmyadmin支持数种认证方法,默认情况下是Cookie认证,此时需要输入账号密码;用户也可以将认证方式修改成Config认证,此时phpmyadmin会使用配置文件中的账号密码来连接mysql数据库,即不用再输入账号密码。
2. 新版本宝塔7.4.2通过安全面板授权访问phpmyadmin的流程
在第一次点击安全面板授权登录的时候,bt前段会把数据库的账号密码发送到phpmyadmin,然后经过下面的代码处理,把账号密码写到配置文件,这样就不需要输入账号密码登录了。
核心代码在这里
if request.method == 'POST':
#登录phpmyadmin
if puri in ['index.php','/index.php']:
content = public.url_encode(request.form.to_dict())
if not isinstance(content,bytes):
content = content.encode()
self.re_io = StringIO(content)
username = request.form.get('pma_username')
if username:
password = request.form.get('pma_password')
if not self.write_pma_passwd(username,password):
return Resp('未安装phpmyadmin')
如果尝试登录phpmyadmin,并且请求附带pma_username
和pma_password
会执行self.write_pma_passwd(username,password)
def write_pma_passwd(self,username,password):
'''
@name 写入mysql帐号密码到配置文件
@author hwliang<2020-07-13>
@param username string(用户名)
@param password string(密码)
@return bool
'''
self.check_phpmyadmin_phpversion()
pconfig = 'cookie'
if username:
pconfig = 'config'
pma_path = '/www/server/phpmyadmin/'
pma_config_file = os.path.join(pma_path,'pma/config.inc.php')
conf = public.readFile(pma_config_file)
if not conf: return False
rep = r"/\* Authentication type \*/(.|\n)+/\* Server parameters \*/"
rstr = '''/* Authentication type */
$cfg['Servers'][$i]['auth_type'] = '{}';
$cfg['Servers'][$i]['host'] = 'localhost';
$cfg['Servers'][$i]['port'] = '{}';
$cfg['Servers'][$i]['user'] = '{}';
$cfg['Servers'][$i]['password'] = '{}';
/* Server parameters */'''.format(pconfig,self.get_mysql_port(),username,password)
conf = re.sub(rep,rstr,conf)
public.writeFile(pma_config_file,conf)
return True
很清楚的可以看到直接把账号密码写死在了config.inc.php
。
但是,这样的话未授权的用户无论访问哪个url都是不能绕过授权的,第一个url没有授权的话会直接重定向到主页,第二个会让你填写账号密码
3. 错误的文件放置目录
根据2的叙述,使用bt面板进行流量转发的时候转发到的肯定是某个目录下存放的phpmyadmin应用,那么
只要能找到phpmyadmin的根目录就可以完成未授权访问了
很遗憾,bt的开发人员把新的pma目录放到了/www/server/phpmyadmin
这是老版本访问phpmyadmin的根路径
可以看到这个目录放了2个文件夹
- phpmyadmin_c8483cecf22859f0
- pma
第一个里面的config.inc.php并没有写账号密码
而之前的代码刚好是写到了pma里面这个配置文件里面的:
pma_config_file = os.path.join(pma_path,'pma/config.inc.php')
public.writeFile(pma_config_file,conf)
所以访问http://192.168.112.129:888/pma
即可成功访问phpmyadmin
这就导致了最后的未授权访问。
思考
其实宝塔这个不需要用户密码访问phpmyadmin想法真的挺不错的
给了用户很大的用户体验,有时候安全固然重要,但是用户体验也是一个很重要的指标
但是,这个漏洞的发生,确实给各大bt使用者,包括国家机关,政府企业以及个人用户带来了很大的损失,希望开发者多思考这些看似很愚蠢的很细小的逻辑错误。
Reference
来自p神的文章
https://www.anquanke.com/post/id/215288