BaoTa-Linux-Panel-安全审计报告

46次阅读
没有评论

宝塔 Linux 面板 (BaoTa Panel) 代码安全审计报告

  • 审计对象: 宝塔 Linux 面板 v11.8.0 源码(约 1042 个 Python 文件 / 43 万行)
  • 审计日期: 2026-08-11
  • 审计范围: 反序列化、命令执行/RCE、SQL 注入、未授权访问、硬编码凭据、XSS、SSRF、越权/IDOR 等可获取系统/用户/应用权限或数据的问题
  • 运行环境: Flask + gevent WSGIServer,以 root 权限运行于 /www/server/panel

⚠️ 前置说明:本面板按设计即以 root 运行,本身就是一个"登录后等于 root"的管理系统。因此本次审计的核心风险判断标准是:漏洞是否能让攻击者突破其原本拥有的权限边界——即未认证访问、低权限子账号提权到 root、或从"仅操作数据"升级为"执行系统命令/读取系统文件"。


风险总览

严重程度 数量 类别
🔴 严重 (Critical) 9 越权提权链(未认证/子账号 → root RCE)、任意文件读取、存储型 XSS
🟠 高危 (High) 10 命令注入、SQL 注入、SSRF、存储型 XSS
🟡 中危 (Medium) 10 条件性命令注入、zip-slip、未授权分支、二次注入
🟢 低危 (Low) 4 硬编码默认值、反序列化链(需前置条件)、信息泄露

一、反序列化

1.1 【低危】pickle 反序列化汇点(需前置条件链)

项 内容
文件 class/cachelib/serializers.py:50
汇点 BaseSerializer.loads → pickle.loads(dumps)
用途 Flask session / cache 序列化
可达性 仅当攻击者能写入 cache/session 文件并知晓对应键名时才可触发;面板 session 为服务端文件存储,不直接可控
结论 不可独立利用,属于纵深缺陷

1.2 【低危】task.py pickle.loads(MD5 完整性校验保护)

项 内容
文件 class/task.py:2455
汇点 pickle.loads(f_data)
保护 task.py:2452 校验 MD5 完整性(需要先能写文件 + 伪造 MD5)
结论 需命令注入/文件写前置,独立利用难度高

1.3 【信息】PluginLoader 动态库非 pickle

class/PluginLoader.x86_64.Python3.7.so 经 strings 分析:含 AES 加密(_ZN3AES7EncryptEPKcPcli)、db_encrypt/db_decrypt、base64、rapidjson、popen/system 符号——为 AES 加密插件加载器,不存在 pickle 反序列化接口。

反序列化小结:三个潜在汇点均需"先获得文件写入 + 键名/完整性控制"的前置条件,而获取该前置条件本身已经是更严重的漏洞。无反序列化直接可利用的 RCE。


二、命令执行 / RCE

前提:public.ExecShell()(class/public.py:633)默认 shell=True。以下所有 ExecShell 汇点均已确认。

2.1 🔴【严重】多控制器命令注入(面板任意命令执行)

面板存在 15 处以上 用户参数直拼 shell 的汇点,任意登录用户(含低权限子账号)均可通过 /system、/files、/ftp、/cron、/database、/plugin、/ssl 等路由触发,获取 root 权限。其中已在源码中逐一核实的:

# 文件:行 入口 危险代码 注入参数
1 class/system.py:1216 /system?action=ServiceAdmin execStr = "/etc/init.d/" + get.name + " " + get.type name/type 无任何白名单校验
2 class/system.py:1559 /system?action=upgrade_panel shell = 'nohup ... upgrade_panel {} &>...'.format(get.version) version 仅校验非 None
3 class/files.py:3514-3516 /files?action=SetFileAccess 'chmod ' + all + ' ' + get.access + " '" + get.filename + "'" 及 'chown ' + all + ' ' + get.user access/user 未加引号、未校验(仅 filename 经 CheckDir)
4 class/files.py:4724-4729 /files?action=exec_git nohup git clone {} ... / nohup {} ... giturl,正则 ^(https?|git|ssh|ftp|ftps)://[^\s/$.?#].[^\s]*$ 中 [^\s]* 允许 ; & 反引号等元字符,可注入
5 class/ftp.py:713 /ftp?action=ModifyFtpUserAccess subprocess.check_call(cmd, shell=True) time_restrictions/max_files/max_sim_sessions 完全无校验(IP 参数有 ipaddress 校验、username 有正则,但这三个裸奔)
6 class/panelSSL.py:1228-1231 /ssl?action=GetSSLInfo 'rm -rf /etc/letsencrypt/archive/' + get.siteName siteName 未在 publicObject 的 path/xss 校验列表内(但需有效 partnerOrderId)
7 class/panelTask.py:583 解压任务 unzip -X -P '"+password+"' -o ... zip 密码无转义(rar 分支 line 595 有转义、7z 分支用双引号,唯独 zip 分支裸拼)
8 class/crontab.py:1544 /cron?action=GetShell nohup .../modify_second_cron.sh {} {} {} &.format(time_type, second, cronName) second/time_type 直拼

攻击示例(2.1-#1 ServiceAdmin):

GET /system?action=ServiceAdmin&name=nginx%3Bid%3B&type=start
# 实际执行: /etc/init.d/nginx;id; start  →  root 权限执行 id

根因:面板功能层对业务参数(服务名、版本号、权限、IP 列表、压缩包密码、站点名)缺少统一的 shell 元字符过滤,只对部分参数做局部校验,且各控制器零权限归属校验(见第五节)。


三、SQL 注入

3.1 🔴【严重】/data getKey — 读取任意表任意字段(SELECT 列/表名注入)

项 内容
文件 class/data.py:688-714
入口 /data?action=getKey&table=...&key=...&id=...(白名单含 getKey)
根因 ORM public.M() → class/db.py getField(keyName) → "SELECT " + __OPT_FIELD + " FROM " + __DB_TABLE。get.table 拼入表名、get.key 拼入列名,均为原生字符串,仅 id 参数化
PoC /data?action=getKey&table=users&key=password FROM users WHERE id=? -- &id=1 → 读出 admin 密码哈希
附加 data.py:706-711:当 table=config 且 key=mysql_root 时,直接用读取值执行 /usr/bin/mysql --user=root --password='{password}' → 可借注入拿到 MySQL root

3.2 🔴【严重】/data GetSql — log_type 直拼 WHERE

项 内容
文件 class/data.py:758-761(getData)
入口 /data?action=getData&table=logs&log_type=...(白名单含 getData)
根因 where = " type = '{}' ".format(get.log_type) 直拼,param=() 空参数化
PoC log_type=1' UNION SELECT password,2,3 FROM users --

3.3 🔴【严重】/database GetMysqlUser — MySQL root 查询注入

项 内容
文件 class/database.py:3415-3428
入口 /database?action=GetMysqlUser&sid=1&search=...
根因 user_data = mysql_obj.query("SELECT user FROM mysql.user WHERE ... and user like '%{}%' GROUP BY user;".format(search)) —— search 零转义直拼,以 MySQL root 身份执行
PoC search=x%' UNION SELECT current_user() --

SQL 注入小结:三处均为最直接的字符串拼接型注入,经 public.M() ORM 层(仅 where(param) 值参数化,field/table/order/limit 全裸)与直连数据库函数,可读面板用户密码哈希与 MySQL root 数据。均需登录(经 comm.local()),但低权限子账号同样可触发(控制器无权限归属检查)。


四、SSRF

4.1 🟠【高危】/cron check_url_connecte — 任意 URL 请求(内网探测)

项 内容
文件 class/crontab.py:1906-1921
入口 /cron?action=check_url_connecte&url=...
根因 response = requests.get(get['url'], timeout=30) 无 scheme/IP 限制,返回 status_code + public.xsssec(response.text)
利用 访问 http://127.0.0.1:8888/... 探测内网/云元数据(如 http://100.100.100.200/latest/meta-data/),读取内网服务响应文本
危害 内网横向探测、云厂商元数据访问(可凭 EC2/阿里云 InstanceRole 提权)

4.2 🟡【中危】/site check_ssl — 443 端口连接探测

项 内容
文件 class/panelSite.py:9234-9252
根因 socket.create_connection((get.hostname, 443)) + TLS 握手,通过错误差异做内网 443 端口连通性 oracle

五、越权 / IDOR(权限边界突破)

5.1 🔴【严重】Referer 缺失绕过子账号菜单鉴权

项 内容
文件 BTPanel/__init__.py:221-224
根因 user_Authority() 子账号功能权限检查仅在 request.referrer 存在时才生效;无 Referer 头的请求(curl 直连)直接跳过菜单授权
利用 低权限子账号对任意受限接口(如 /files?action=ExecShell、/system)用 curl --referer '' 直连 → 绕过功能授权,直接操作 root 级功能
验证 grep 确认 panelSite.py/database.py/crontab.py/files.py 中 0 处 session['uid']/get_user_power 归属判断

5.2 🔴【严重】set_temp_login — 子账号/任意用户创建临时登录 → 全面板权限

项 内容
文件 class/config.py:2850-2883;链:class/userlogin.py:285-340
根因 set_temp_login 仅检查 'tmp_login_expire' in session,不检查 session['uid'] != 1。当临时登录表为空时设置 pdata['id'] = 101;request_temp 据此设置 session['uid'] = 101(users 表中不存在该 uid)→ 后续菜单鉴权查询用户为空 → 跳过权限检查,获得完整面板(含文件管理、终端)访问
利用链 低权限账号 → 调用 set_temp_login 创建 48 位临时 token → 用该 token 请求 request_temp → 以"幽灵 uid=101"身份登录 → 完整 root 面板
同类 remove_temp_login(2886)、clear_temp_login(2903)、get_temp_login_logs(2922)同样缺 uid 检查

5.3 🟠【高危】set_admin_path — 无 uid 校验修改面板路径并重启

项 内容
文件 class/config.py:797
危害 任意登录用户可改面板访问路径;配合其他点可制造配置破坏

5.4 🟠【高危】IDOR — 数据库/站点/FTP 操作无归属校验

所有站点/数据库/FTP 管理接口(删除站点 DeleteSite、改 FTP 权限等)均不校验操作对象归属。低权限子账号可:

  • 删除/修改其他子账号甚至管理员的站点(DeleteSite 无 owner 校验)
  • 查看/导出任意站点配置、数据库备份
  • 修改任意 FTP 用户权限

六、未授权访问 / 任意文件读写

6.1 🔴【严重】/download — 认证后任意文件下载(root)

项 内容
文件 BTPanel/__init__.py:1442-1494
根因 filename = request.args.get('filename') → html.unescape(filename) → 仅检查非设备/目录 → send_file(filename, as_attachment=True)。无 path_safe_check、无路径白名单
利用 任意已登录用户下载 /etc/shadow、/www/server/panel/data/userInfo.json、config/api.json、SSL 私钥等
验证 comm.local() 确认需登录;但低权限子账号同样可达(见 5.1/5.2 提权)

6.2 🟠【高危】/down/<token> — 预认证任意文件分享

项 内容
文件 BTPanel/__init__.py:1497、/down 分享(token 校验)→ line 1599 发送 find['filename']
根因 token 文件分享机制对目标文件不做 path_safe_check(仅对分享子路径 fname 检查)。若攻击者可构造/猜出分享记录(token 为随机串,需一定条件),可下载任意路径
风险 分享的 filename 完全由创建者指定;配合 download_file 等任意读可把任意文件放进分享池

6.3 🔴【严重】/files download_file — 回收站路径穿越任意文件读取

项 内容
文件 class/files.py:1842-1873
根因 rname = get.rname.strip() 无 path_safe_check;file_path = os.path.join(recycle_bin, rname) 对 ../../ 会规范化 → shutil.copyfile(file_path, "/.Recycle_bin/download/"+name) → 再从 /.Recycle_bin/download 下载
利用 rname=../../etc/shadow&name=x → os.path.exists 通过 → 复制 /etc/shadow 到可下载目录 → 读取任意文件
验证 已亲自核对代码,os.path.join 接受 .. 穿越,os.path.exists 对目标存在时为 True

6.4 🟡【中危】/static/、/code 仅检查 session 键存在性

项 内容
文件 BTPanel/__init__.py:279-285
根因 静态文件与 /code 分发只判断 'login'/'admin_auth'/'down' 键是否存在,不校验值是否有效
说明 session 键值本身难以伪造(服务端存储),但属于弱校验模式

6.5 🟡【中危】/mail_sys/send_mail_http.json 未认证分支

项 内容
文件 BTPanel/__init__.py:2235-2381
根因 若 mail_sys 插件已安装,该接口跳过 comm.local()(无认证),参数白名单 ['mail_from','password','mail_to','subject','content','subtype','data'],可被未认证者滥用发信(垃圾邮件/钓鱼),且 line 2359-2367 对插件模板做 render_template_string(t_body, data=data)
风险 未认证 SMTP 滥发 + 插件模板内容若含用户数据则有模板注入面(当前 data 为插件自有数据,风险受限于插件)

6.6 🟡【中危】get_module_template — 路径穿越读取任意 .html

项 内容
文件 class/config.py:3569-3591(sms 分支:render_template_string)、class/panelPush.py:154-161
根因 sfile = '{}/class/msg/{}.html'.format(panelPath, module_name) —— module_name 未校验可含 ../,读取面板目录外任意 .html

七、XSS

7.1 🔴【严重】存储型 XSS — 未认证 User-Agent → 管理员登录日志页

项 内容
写入端 class/public.py:8302-8322 record_client_info:user_agent = request.headers.get('User-Agent','') 未清洗直接入库;class/userlogin.py:582-587 __login_error() 在登录失败时(未认证触发,20+ 处调用)写入 client_info 表
回显端 class/logsModel/panelModel.py:462-521 get_panel_login_log:直接返回 user_agent 字段,无转义(仅 remote_addr 过 return_area)
渲染端 BTPanel/static/js/utils.js:5132-5133 $reader_column_type case 'text':value 原样入 config → line 4601-4613 直接拼接进 <td><span>template</span></td> raw HTML → 管理员打开"登录日志"即执行
利用 攻击者用恶意 User-Agent(如 <script>fetch('/system?action=...')</script>)发起登录请求 → 写入库 → 管理员查看日志页触发 → 窃取管理员 session / 以管理员身份操作
验证 三条链路均亲自核对源码确认

7.2 🔴【严重】存储型 XSS — 操作日志内 raw innerHTML 渲染

项 内容
写入端 class/public.py:454-491 WriteLog:args[i](站点名、域名、描述等用户可控参数)经 line 478 logMsg.replace 直接拼入,无转义清洗即插入 logs 表
回显端 class/logsModel/panelModel.py:46-83 get_logs_bytype:对 type in ('项目管理','网站管理') 的日志 select() 原样返回 log 字段,无转义
渲染端 BTPanel/static/js/panel-view-ops.js:123-124:render 函数 innerHTML:gt(new DOMParser().parseFromString(e.log,'text/html').body.innerHTML) —— 操作日志内容经 innerHTML 直接渲染(gt 仅全角→半角转换,不转义)
利用 低权限子账号/管理员创建含 <script>/<img onerror> 的站点名、域名或描述 → 写操作日志 → 管理员打开"网站管理"操作日志页 → JS 在管理员上下文执行 → 窃取会话/以管理员调用命令注入 → root RCE
验证 写入端(WriteLog 无清洗)、回显端(get_logs_bytype 无转义)、渲染端(innerHTML sink)三处均亲自核对源码确认

7.3 🟠【高危】系统级 XSS 渲染缺陷 — bt_tools.table type:'text' raw HTML

utils.js 的表格组件对 type:'text' 列不做 HTML 转义(仅 type:'html' 才显式渲染)。任何后端返回的未转义字段(站点名、FTP 文件名、日志内容、备注、域名)在 type:'text' 列中均可执行 HTML/JS。这是一个系统级放大器,令所有后端未转义回显点都升级为存储型/反射型 XSS。

7.4 🟡【中危】FTP 操作日志文件名回显

class/ftplog.py:391-493 get_action_log 将 FTP 日志行中的 file 字段(攻击者可控的 FTP 文件名)原样返回;前端 templates/default/ftp.html:255-272 用 type:'text' 渲染 → 存储型 XSS。

7.5 【低危】xsssec 过滤分析

public.xsssec/xsssec3(class/public.py:2111/2121)将 < > " ' 替换为全角字符,对 HTML 正文场景可阻断标签注入;但 & 未处理,且错误页 panel_error.html 为 .format() 模板——在属性/URL 上下文中仍存在绕过空间,属低危残留。


八、硬编码凭据

8.1 🟢【低危】login_token 默认值 '1234567890'

项 内容
文件 class/public.py:5623 get_login_token_auth()
根因 data/login_token.pl 文件缺失时返回硬编码 '1234567890';该值经 common.py:251-255 在已登录 session 上做二次校验
风险 需要先有有效 session 才能触发该校验,单独不构成认证绕过;但属于"依赖文件存在才安全"的脆弱默认值。若文件被误删/权限异常,所有会话用同一个已知 token

8.2 🟢【低危】API token 默认兜底

class/panelApi.py set_token:token_crypt 缺失时重新生成随机 token,默认 config/api.json 中 token 为空串时理论上存在空 token 风险,但代码路径会在开启 API 时强制生成随机 token,实际被 t_type=='2' 分支兜底 → 无法真实空态利用。


九、其他

9.1 🟡【中危】zip-slip — tar 解压路径穿越

项 内容
文件 class/files.py:3374+ __untargz
根因 filename = item.name(tar 内原始文件名),unzip_path = os.path.join(get.dfile, filename);Python <3.12 的 tarfile.extract 对 ../../ 不做防护 → 恶意 tar 包可写到解压目录之外
缓解 Python 3.12+ tarfile 默认 filter 防护;宝塔面板当前 Python 版本需现场确认

9.2 🟡【中危】panelTask 任务队列二次注入

class/panelTask.py:189(wget)/:212(cp)/_zip(442-463)/_unzip(583-622):任务队列中的 task_shell/other 以单引号包裹但未转义,存储值中若含单引号可闭合注入。需任务写入前置(通常本身需要面板权限)。

9.3 🟡【中危】panelPlugin 插件安装命令注入

class/panelPlugin.py:442(input_package→bash {})、:3906-3916(input_zip cp -a -r {tmp_path}/*)、:2957(install version 入库)、:3017-3019(uninstall)等多处插件安装参数直拼 shell。需已登录(插件安装本身是面板功能)。

9.4 🟢【低危】crontab GetShell 设计性风险

class/crontab.py:1518-1599:计划任务 shell 本身就是"root 执行用户脚本"的设计功能——任何可建"shell 类任务"的账号即可 root RCE。这与 2.1-#8 的 second 参数注入(独立绕过)不同,核心风险是子账号是否有建 shell 任务的权限被 5.1/5.2 绕过。


十、攻击链组合(严重度最高)

以下链使"未认证 → root RCE"成为可能,是本次审计最重要的结论。

链 A:子账号 → root RCE(完全利用 5.1+2.1)

低权限子账号登录
  → 5.1 Referer 绕过(无 Referer 直连)跳过菜单鉴权
  → 2.1-#1 ServiceAdmin(name 注入) / 或 files.ExecShell
  → root 命令执行

链 B:子账号 → 幽灵 uid → 完整面板

低权限子账号登录
  → 5.2 set_temp_login(无 uid 校验)生成临时 token
  → userlogin.request_temp 设置 session['uid']=101(幽灵 uid)
  → 权限表查询为空 → 跳过所有菜单鉴权 → 完整 root 面板(含终端、文件、数据库)

链 C:未认证 → 存储型 XSS → 管理员会话

任意访客发送恶意 User-Agent 的登录请求
  → 7.1 写入 client_info(未认证触发)
  → 管理员打开登录日志页
  → 7.1+7.2 raw HTML 渲染 → 执行 JS
  → 以管理员 session 调用 2.1 任一命令注入 → root RCE

链 D:认证(任意子账号) → 任意文件读取

5.1/5.2 获得面板访问
  → 6.1 /download(filename=../../etc/shadow) 或 6.3 download_file(rname=../../etc/shadow)
  → 读取 /etc/shadow、面板数据、SSL 私钥

十一、修复建议(按优先级)

优先级 措施
P0 1) public.ExecShell 全部改为参数化/禁用 shell 或增加全局 shell 元字符白名单校验助手;2) ServiceAdmin 的 name 用服务名白名单、type 用 start/stop/restart/reload 白名单;3) upgrade_panel version 用 \d+\.\d+\.\d+ 校验
P0 4) 修 5.2:所有 temp_login 相关接口校验 session['uid']==1(管理员专属);5) 修 5.1:user_Authority 判断不再依赖 Referer 有无
P0 6) ORM field/table/order/limit 参数一律白名单校验;修 3.1/3.2/3.3 三处 SQL 直拼
P0 7) /download、download_file、/down 增加 path_safe_check + 面板路径白名单
P1 8) 修复 2.1-#5 FTP、-#6 GetSSLInfo、-#7 unzip 密码、-#8 crontab 注入
P1 9) 修复 4.1 SSRF(限制 scheme 与内网 IP);10) 7.1 入库前清洗 UA、7.2 前端 text 列默认转义
P1 11) 控制器层统一增加资源归属校验(站点/库/FTP/文件按 uid 过滤)
P2 12) 移除 login_token 硬编码默认值;13) mail_sys 接口恢复认证或加签名;14) get_module_template module_name 白名单;15) tar/zip 解压走安全过滤

附录:已排除/降级项

项 结论
PluginLoader.so AES 插件加载器,无 pickle → 非反序列化 RCE
cachelib pickle / task.py pickle 需文件写+键名/完整性前置,独立不可利用
render_template_string(SSTI) 6 处调用点均为静态插件模板 + \w+ 校验 + 空/插件自有数据上下文,非用户可控模板串
/public get_ping(panelPing.Test) 类不存在,死代码返回 404,不可达
xsssec 全角替换 HTML 正文可阻断,属性上下文残留低危
API token 空值 set_token 开启时强制生成随机 token,无法空态伪造
/code 路由 仅校验 session 键存在,非值;单独不可利用
正文完
 0
评论(没有评论)