文章总结: 这篇文章详细分析了开心版宝塔13.0.0的破解机制,指出其通过修改安装器下载源、生成伪造本地身份文件、改写绑定检查与插件授权判断、替换PluginLoader原生库等多环节实现破解。文章拆解了安装器接管流程、伪造userInfo.json的生成逻辑、升级链路的修改方式,并对比了官方与破解版的差异,揭示了破解版存在的供应链安全风险。 综合评分: 82 文章分类: 代码审计,漏洞分析,安全工具,逆向分析
开心版宝塔13.0.0最新版破解机制技术分析
原创
佚名 佚名
星宇Sec
2026年9月13日 11:09 山东
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
很多人第一次看到开心版宝塔时,都会有一个直观疑问:它明明不是从官方节点安装的,为什么登录后仍然有账号信息,插件页面也能显示已授权?只看安装命令,很容易把它理解成换了一个下载地址,或者把插件期限往后改了几位。把文件真正展开后,会发现事情比这复杂一些。
开心版改动的是一整条运行链路。安装器先接管下载源,再写入一份本地身份资料;面板启动后,绑定检查、云端检查和插件期限判断又被分别改写;到了专业版功能真正做授权判断的地方,加载的PluginLoader原生库也已经不是官方版本。几个改动叠在一起,才形成了用户看到的“已绑定、未过期、可以使用部分专业功能”的效果。
下面按面板实际运行的顺序,把这条链路拆开来看:先看安装器拿到了什么,再看登录后显示的账号从哪里来,最后对照Python代码、请求结果和原生库,说明每一步究竟改变了什么。能从样本中直接确认的内容会给出代码或数据结构,无法仅凭静态文件证明的地方也会明确标出。
开心版整体运行链路
一、安装器如何接管初始文件
1.官方安装流程
官方启动命令通常是:
if [ -f /usr/bin/curl ];then
curl -sSO https://download.bt.cn/install/install_panel.sh
else
wget -O install_panel.sh https://download.bt.cn/install/install_panel.sh
fi
bash install_panel.sh KZOqff1mn
外层命令只取得并执行install_panel.sh。脚本随后下载public.sh和panel6_13.zip,把面板解压到/www/server/panel/,并安装面板专用Python环境。安装后主要涉及:
/www/server/panel/install/public.sh
/www/server/panel/
/www/server/panel/data/userInfo.json
/www/server/panel/class/PluginLoader*.so
/etc/init.d/bt
/usr/bin/bt
/usr/bin/btcli
public.sh负责节点和更新辅助操作,面板ZIP包含Python代码、插件和编译扩展。官方安装器还会处理Ubuntu的apt/dpkg锁、安装依赖、创建站点和备份目录并启动服务。
官方安装流程图
这条链里,外层命令只负责取下安装器;真正决定面板文件内容的是后面的public.sh和面板ZIP。
2.第三方在线闸门
开心版安装器开头新增:
download_Urls='https://download.miku.ci'
Check_Api=$(curl -Ss --connect-timeout 5 -m 2 \
$download_Urls/api/SetupCount)
if [ "$Check_Api" != 'ok' ];then
Red_Error "无法连接,安装过程已中止!"
fi
这里的SetupCount不是普通统计,而是安装开关。正文严格等于ok才继续,返回其他内容或请求超时就终止安装。对应的请求可以写成:
GET /api/SetupCount HTTP/1.1
Host: download.miku.ci
HTTP/1.1 200 OK
Content-Type: text/plain
ok
3.下载地址全部切换
官方安装器使用节点变量下载:
${download_Url}/install/public.sh
${download_Url}/install/src/panel6_13.zip
开心版改成:
${download_Urls}/install/public.sh
${download_Urls}/install/src/panel6_13.zip
样本中的download_Urls固定为https://download.miku.ci。因此第一次安装时,public.sh、面板Python代码、插件代码和原生扩展都由第三方节点提供。这个改动是后续破解的基础:如果仍安装官方ZIP,后面的Python短路和第三方PluginLoader不会自动出现。
官方版与开心版下载源对比图
4.生成伪造的本地身份
安装器在userInfo.json不存在时生成随机资料:
mock_uid="$((1000000 + (RANDOM * 32768 + RANDOM) % 2000000))"
mock_username="${mock_phone_prefix}$(tr -dc '0-9' </dev/urandom | head -c 8)"
mock_address="$(hostname -I 2>/dev/null | awk '{print $1}')"
mock_access_key="$(openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | head -c 32)"
mock_secret_key="$(openssl rand -base64 72 | tr -dc 'A-Za-z0-9' | head -c 48)"
mock_ukey="$(openssl rand -hex 16)"
echo "{\"uid\":${mock_uid},\"username\":\"${mock_username}\",\
\"address\":\"${mock_address}\",\"access_key\":\"${mock_access_key}\",\
\"secret_key\":\"${mock_secret_key}\",\"ukey\":\"${mock_ukey}\",\
\"state\":1}" > /www/server/panel/data/userInfo.json
它没有登录官方账号,也没有取得官方签发的授权,只是构造了一份字段齐全、state为1的本地对象。依赖文件存在、字段完整或状态值的代码,会先把它当作有效资料。
安装器还会写入:
/www/server/panel/data/not_workorder.pl
/www/server/panel/data/not_panelai.pl
/www/server/panel/data/not_evaluate.pl
这些文件用于关闭工单、PanelAI和评价相关入口,不是授权状态本身。
5.面板登录和宝塔账号绑定不是一回事
这里容易把两个“账号”混在一起。面板登录验证的是本机数据库里的管理员,宝塔账号绑定则是面板向宝塔云端申请一份userInfo.json。两者使用的接口和数据都不同。
面板登录入口在BTPanel/__init__.py的/login路由,密码校验由class/userlogin.py完成。代码从users表查询本地管理员,校验通过后只设置当前浏览器会话:
userInfo=public.M('users').where('id=?',(1,)).field(
'id,username,password,salt').find()
...
session['login']=True
session['username']=userInfo['username']
session['uid']=userInfo['id']
这一步不会生成宝塔云账号,也不会因为登录成功而向宝塔服务器注册。登录成功的返回包只是本地面板的结果,例如:
POST /login HTTP/1.1
Host: panel.example
Content-Type: application/x-www-form-urlencoded
username=<RSA密文>&password=<RSA密文>
HTTP/1.1 200 OK
Content-Type: application/json
{"status":true,"msg":"LOGIN_SUCCESS","login_time":1760000000}
真正的宝塔账号绑定走class/panelSSL.py中的GetAuthToken()。官方代码把手机号、密码摘要、服务器标识、操作系统和MAC地址放进data,再进行URL编码和十六进制编码,提交到https://api.bt.cn/Auth/GetAuthToken。开心版只把地址换成了https://api.miku.ci/Auth/GetAuthToken:
data['username']=public.rsa_decrypt(get.username)
data['password']=public.md5(public.rsa_decrypt(get.password))
data['serverid']=panelAuth().get_serverid()
data['os']='Linux'
data['mac']=self.get_mac_address()
pdata['data']=self.De_Code(data)
result=self.request_post(self.__BINDURL,pdata)
正常绑定时,服务端返回的data还是一段十六进制字符串,面板用En_Code()解码后才得到账号资料,然后覆盖写入data/userInfo.json。可以把它抽象成:
POST /Auth/GetAuthToken HTTP/1.1
Host: api.bt.cn
Content-Type: application/x-www-form-urlencoded
data=757365726e616d653d...(URL编码结果再转十六进制)
HTTP/1.1 200 OK
Content-Type: application/json
{"status":true,"data":"7b22756964223a..."}
官方面板只有在这条绑定流程成功后,才会把云端返回的uid、username、access_key、secret_key和serverid保存下来。开心版安装时没有调用这个接口,而是直接执行前面的echo ... > data/userInfo.json。因此它生成的是本地伪造资料,不是宝塔官网新注册的账号。
面板首页之所以会显示这个“账号”,是因为config.py的GetConcifInfo()调用了panelSSL.panelSSL().GetUserInfo(None)和public.get_user_info()。panelSSL.GetUserInfo()只要能从data/userInfo.json读到非空对象,就返回status=True并把username做成类似手机号的脱敏字符串;public.get_user_info()则把同一文件里的uid、username和access_key交给插件、证书等模块使用。开心版的public.is_bind()又直接return True,所以绑定拦截也不会出现。
安装器生成的文件最初没有serverid,随后public.get_user_info()发现字段缺失,会调用panelAuth.create_serverid(),按本机MAC、主机名和CPU信息计算64位标识并写回文件。这个serverid是本地设备标识,不是云端账号凭证。
本地登录与宝塔账号绑定流程图
所以“登录后自动绑定”更准确的说法是:本地管理员登录后,首页读到了安装器提前写好的伪造账号文件。只有用户主动打开绑定功能并提交宝塔账号,才会进入GetAuthToken();在当前开心版样本中,这个接口还被改指向第三方服务器。
二、升级链如何保持改版
只替换初始面板包是不够的。用户从面板点击升级或修复后,如果又从官方取包,改动就会被覆盖。因此开心版同时修改了update_panel.sh、panel/script/upgrade_panel.py、panel/script/upgrade_panel_optimized.py和panel/script/local_fix.sh。
1.版本和升级包切换
官方Shell升级器:
version=$(curl -Ss --connect-timeout 5 -m 2 \
http://www.bt.cn/api/panel/get_version)
开心版:
version=$(curl -Ss --connect-timeout 5 -m 2 \
https://www.miku.ci/api/panel/get_version)
升级ZIP和校验文件改为:
wget -T 5 -O /tmp/panel.zip \
https://download.miku.ci/install/update/LinuxPanel-${version}.zip
wget -T 5 -O /tmp/panel.zip.pl \
https://download.miku.ci/install/update/LinuxPanel-${version}.pl
面板内Python升级代码也使用:
down_url='https://download.miku.ci/install/update/LinuxPanel-{}.zip'.format(ver)
down_url='https://download.miku.ci/install/update/LinuxPanel-{}.pl'.format(ver)
info=httpGet('https://www.miku.ci/api/panel/get_panel_version')
因此第三方可以决定版本号、升级包、校验文件以及下一版的Python和原生代码。
官方与开心版升级来源流程图
2.修复模式直接执行远程Shell
升级脚本的修复分支:
url=get_home_node('https://download.miku.ci/install/update6.sh')
os.system('curl -k {}|bash'.format(url))
curl -k不检查TLS证书,|bash把远程内容直接交给Shell执行。local_fix.sh也固定下载第三方update6.sh后运行,远端脚本拥有与升级过程相同的文件修改权限。
3.清理官方授权扩展
升级器会删除部分官方编译型扩展:
/www/server/panel/class/libAuth.aarch64.so
/www/server/panel/class/libAuth.glibc-2.14.x86_64.so
/www/server/panel/class/libAuth.loongarch64.so
/www/server/panel/class/libAuth.x86-64.so
/www/server/panel/class/libAuth.x86.so
这不等于所有授权代码都被删除。Python的authorization.py、pluginAuth.py和部分PluginLoader.*.so仍然存在。准确的流程是:清理官方编译型授权扩展,再把第三方包的原生库写入同一目录,使Python加载第三方实现。
升级阶段还会执行:
chattr -i /www/server/panel/data/userInfo.json
sed -i 's/[0-9\\.][ ]\\+www.bt.cn//g' /etc/hosts
sed -i 's/[0-9\\.][ ]\\+api.bt.sb//g' /etc/hosts
第一条确保伪造身份文件可写,后两条删除指定的官方域名映射。升级结束后还会请求https://tj.bt.sb/api/count?...记录第三方运行次数。
三、Python层如何绕过检查
1.全局节点改成第三方
panel/class/public.py中的GetConfigValue增加:
if key=='home':
return 'https://www.miku.ci'
许多模块先调用GetConfigValue('home')取得根节点。因此这一处修改会影响证书、IP、IDC、版本和插件等多个请求。
2.is_bind()恒真
官方会检查绑定文件:
def is_bind():
if not os.path.exists('data/initBind.pl'):
writeFile('data/initBind.pl','True')
return False
return True
开心版改成:
def is_bind():
return True
调用方无论本地是否真的完成绑定,都会得到真值,绑定页面和初始化拦截器因此被跳过。
3.云域名检查提前返回
官方入口:
def check_domain_cloud(domain):
run_thread(cloud_check_domain,(domain,))
开心版:
def check_domain_cloud(domain):
return
run_thread(cloud_check_domain,(domain,))
官方的cloud_check_domain()会提交用户资料和域名,并把结果写入data/check_domain/。改版在创建线程前就返回,所以正常路径下不创建线程、不发HTTP请求、不写检查结果。它不是把失败响应改成成功,而是让请求不发生。
4.关闭部分云端判断和错误上报
改版中可以看到:
def get_improvement():
return False
def err_collect(error_info,type,error_id):
return
get_improvement()固定关闭用户体验改进计划,err_collect()阻止异常上报。check_auth_ip()还减少了通过PluginLoader.module_run()向云端查询IP归属的路径,更多时候使用本地判断。
5.插件接口全部迁移
panel/class/panelPlugin.py中的接口根域和缓存文件改为:
__plugin_save_file='/www/server/panel/data/plugin_list.pl'
__api_root_url='https://api.miku.ci'
插件主程序地址:
download_d_main_url='https://api.miku.ci/down/download_plugin_main'
插件ZIP地址:
public.downloadFile(
'https://www.miku.ci/api/Pluginother/get_file?fname='+
pluginInfo['versions'][0]['download'],toFile)
执行插件脚本前,还会替换脚本文本中的官方更新地址:
temp_file=temp_file.replace(
'http://download.bt.cn/install/public.sh',
'https://download.miku.ci/install/public.sh')
temp_file=temp_file.replace(
'https://download.bt.cn/install/public.sh',
'https://download.miku.ci/install/public.sh')
这使第三方控制范围从面板自身扩展到插件的安装和更新脚本。
插件安装第三方下载链流程图
6.把插件期限改成本地十年后
改版在插件信息返回前加入:
if'endtime'in softInfo:
softInfo['endtime']=time.time()+86400*3650
例如原始对象:
{"name":"btwaf","endtime":1735689600}
经过本地Python处理后:
{"name":"btwaf","endtime":当前时间戳+315360000}
第二个对象不是服务器返回包,而是本地对象覆盖结果。它能影响面板显示和依赖本地时间的判断,但不会改写服务端授权数据库。
插件期限endtime本地覆盖流程图
7.删除插件状态和解绑检查
官方获取插件列表后启动:
public.run_thread(self.get_cloud_list_status,args=(get,))
public.run_thread(self.is_verify_unbinding,args=(get,))
开心版删除了这两次调用。云端状态刷新和解绑验证不再按官方路径执行,面板更多依赖本地缓存和本地处理结果。
8.删除每日任务的ltd/pro退出条件
官方panel/script/daily.py:
from pluginAuth import Plugin
plugin_obj=Plugin(False)
plugin_list=plugin_obj.get_plugin_list()
if plugin_list["ltd"]<0 and plugin_list["pro"]<0:
sys.exit(0)
开心版删除整个授权退出分支,只执行:
from panelDaily import panelDaily
pd=panelDaily()
pd.check_databases()
这只改变每日数据库检查任务的入口条件,不等于所有专业版功能都被解锁。
四、原生PluginLoader才是核心替换点
1.Python层仍保留授权调用
panel/mod/base/public_aap/authorization.py仍然使用:
def only_pro_members(func:callable)->callable:
@wraps(func)
def _wrap_func(*args,**kwargs):
import PluginLoader
if PluginLoader.get_auth_state()<1:
raise NoAuthorizationException(
'Sorry. This feature is professional member only.')
return func(*args,**kwargs)
return _wrap_func
panel/class/pluginAuth.py仍然调用:
return PluginLoader.get_plugin_list(force)
这说明Python层主要解决外围问题:跳过绑定、停止云端检查、延长本地期限、删除每日任务退出分支。真正的get_auth_state()和get_plugin_list()仍要进入原生扩展。
2.第三方提供同名原生实现
官方包包含:
panel/class/PluginLoader.aarch64.Python3.7.so
panel/class/PluginLoader.i686.Python3.7.so
panel/class/PluginLoader.loongarch64.Python3.7.so
panel/class/PluginLoader.x86_64.Python3.7.so
panel/class/PluginLoader.x86_64.glibc214.Python3.7.so
第三方包除带架构后缀的文件外,还增加:
panel/class/PluginLoader.so
官方x86-64库约292296字节,SHA-256为:
80aae64a69df0cfb9d8c9617544722f46251aca337995536e5932f10fc0921f7
第三方PluginLoader.so和PluginLoader.x86_64.Python3.7.so约482768字节,SHA-256为:
29e0a1e68ad18734b2e2994ae373621f85c7651e152d4334d47bdb7e2b9fcb68
第三方库中能看到:
PyInit_PluginLoader
get_auth_state
flush_auth_key
get_plugin_list
plugin_run
module_run
db_encrypt
db_decrypt
check_panel_auth
libAuth
PyInit_PluginLoader保证Python可以导入模块,get_auth_state、get_plugin_list、plugin_run和module_run保持调用约定,其他接口涉及密钥、数据库和授权操作。Python代码不用改函数名,加载到的二进制实现已经换了。
典型调用链如下图所示。Python层的调用名称没有改变,变化发生在同名原生库被替换之后。
Python与PluginLoader原生库调用流程图
只修改userInfo.json只能改变输入资料,只修改is_bind()只能绕过绑定入口,只修改endtime只能改变本地期限显示。要让PluginLoader.get_auth_state()不再阻止功能,就必须让加载到的原生库给出对改版有利的结果,或者返回包含专业版字段的插件列表。
从文件差异、导出接口和调用关系可以确认官方授权相关原生库被替换,并且第三方库保留了Python需要的同名接口。至于get_auth_state()是否在所有机器上都返回1、插件内部是否还有第二次服务端校验,单凭静态文件无法继续推出。
五、请求和返回结果的变化
1.插件主程序请求
官方调用模型:
POST /down/download_plugin_main HTTP/1.1
Host: api.bt.cn
Content-Type: application/x-www-form-urlencoded
uid=89045&access_key=official-key&name=btwaf&version=8.0.1&os=Linux
开心版:
POST /down/download_plugin_main HTTP/1.1
Host: api.miku.ci
Content-Type: application/x-www-form-urlencoded
uid=1534217&access_key=random-access-key&name=btwaf&version=8.0.1&os=Linux
返回内容会被写入插件主程序文件,并使用Content-md5校验。校验只说明下载内容与第三方返回的MD5一致,不说明文件来自官方。
2.插件列表和endtime
插件列表可以抽象成:
{
"status":true,
"ltd":1,
"pro":1,
"list":[
{
"name":"btwaf",
"versions":[
{
"version":"8.0.1",
"download":"btwaf/8.0.1/btwaf.zip",
"md5":"<md5>"
}
],
"endtime":1735689600
}
]
}
第三方代码在本地把endtime覆盖成:
{"name":"btwaf","endtime":当前时间戳+315360000}
这个JSON是本地Python对象,不是服务器返回包。
插件期限endtime本地覆盖流程图
图中最后一步发生在本地。服务器给出的期限可能已经过期,面板在显示和后续判断前仍会先经过这次覆盖。
3.云域名检查没有“修改后的成功响应”
官方可能发送:
POST /api/panel/check_domain HTTP/1.1
Host: api.bt.cn
Content-Type: application/x-www-form-urlencoded
uid=89045&access_key=official-key&domain=example.com
开心版虽然保留了第三方URL,但入口已经是:
def check_domain_cloud(domain):
return
所以修改后的正常结果不是HTTP/1.1 200 OK,而是根本没有HTTP请求:不创建线程、不连接服务器、不写data/check_domain/*.pl。
4.AI用量请求
改版AI模块默认请求:
GET /plugin_api/chat/api/usage HTTP/1.1
Host: api.miku.ci
uid: 1534217
access-key: random-access-key
appid: bt_app_001
源码只能确认UID、access-key和应用ID被发送给第三方接口,不能仅凭此请求断定会上传密码、数据库或网站文件。
六、完整破解链条
开心版完整破解链条流程图
第一部分是表层绕过:随机userInfo.json、恒真的is_bind()、被短路的云检查和被延长的endtime,负责让面板在本地表现得像已绑定、未过期。第二部分是授权实现替换:Python仍调用PluginLoader.get_auth_state()等函数,但第三方替换了同名原生扩展,改变了这些函数的实际实现和返回结果来源。两部分合在一起,才形成完整的开心版授权绕过链。
七、结论
从这些改动可以看出,开心版并没有破解宝塔账号本身。它绕开的是面板在本地作出的几次判断。安装脚本先写入一份随机的userInfo.json,让面板有账号可读;is_bind()和云端检查被改成直接放行或直接返回;插件信息里的endtime被换成十年后的时间;仍然保留的专业版装饰器,则交给替换后的PluginLoader处理。
用户登录时验证的仍是本机users表,登录成功只产生面板会话。首页显示的宝塔账号来自安装阶段写入的本地文件,和用户是否真的在宝塔官网注册过没有直接关系。真正的官网绑定需要调用GetAuthToken()并把云端返回的数据写回这个文件;开心版只是把这个接口的地址也改到了第三方节点。
因此,面板里看到的“已绑定”和“长期有效”首先是本地状态,不是宝塔官方对这台服务器签发的新授权。原生库的完整行为、第三方接口会返回什么,以及后续更新包是否继续保持这些改动,单凭静态样本无法完全确定;但从安装源、账号文件、Python分支和原生导出接口已经能看出,这套版本是在本地重建了一条有利于绕过授权判断的信任链。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:星宇Sec 佚名 佚名《开心版宝塔13.0.0最新版破解机制技术分析》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。







评论