2026-11-09 12:05:13
#吐槽 我说怎么感觉上个月(10月)workbuddy 里的 DeepSeek 消耗积分速度超级低,换算后比 DeepSeek 官方和 OpenCode Go 的要低多了,我还以为是workbuddy有什么神奇的优化上下文技术,结果昨天才知道,合着是腾讯这逗比「9-10~9-25号有DS 5折积分活动(这个活动大家都知道,毕竟当时界面上直接写着呢)然后9-25号之后,界面里删除了5折活动的文案,但其实:DeepSeek 5折积分活动被他们延续到了11-05号,但他们完全忘了宣传这事了」于是导致了两个事情:
1. 大家都以为 5 折在09-25就结束了,之后的消耗速度是正价,但其实整个10月DeepSeek 的积分都是半价的。
2. 11-05开始大家突然感觉积分非常不耐用,之前100积分能用接近1亿的DS4.1Flash,修现在几乎直接砍半,大家还以为DS的锅呢,结果:这TM才是正常的消耗速度,之前是打五折的。
3. 总而言之就是腾讯默默给了大家快 1 个多月的DeepSeek 5 折优惠,但完全没告诉大家,让大家误解了,所以11-05~11-08期间使用 DeepSeek-V4.1 Flash 模型所产生的积分消耗全部按原价 5 折优惠结算,差额部分(即实际消耗积分总额的 50%)将全部返还……😂
2026-11-08 21:39:56

声明:本文为前作《宝塔 Nginx 免费防火墙 8.4/8.5 赶紧卸载吧:偷偷上传文件,凭据明文裸奔》的续篇。基于插件 8.4 与 8.5 官方全新安装包的真实逐字节 diff 结果撰写。
我知道 8.6 已经出了,但我这不是写得慢嘛,可以先预告一下:8.5→8.6 的修复方式也挺“幽默”的。
我那篇把“民国三年一场雨”(宝塔论坛内ID「宝塔技术-小强」)制作的 宝塔 Nginx 免费防火墙 8.4 扒了个底朝天之后,不少站长朋友留下了评论,说看完感觉太可怕了,连夜登录服务器把这玩意给卸载干净了。
本来以为这事告一段落,我也可以安心去搬我的砖了。
结果第二天下午,定时运维 agent 提示我测试机里的 宝塔 Nginx 免费防火墙 存在更新:
C. 主机「**」 → 「面板插件」-「宝塔 Nginx 免费防火墙」:存在更新[8.5],现有版本[8.4]。
说实话,看到这个消息时,我甚至在心底闪过一丝欣慰:可以啊!作者这响应挺迅速嘛!
不管之前代码写得多粗糙,只要看到社区反馈后,能连夜重构加固、把那些要命的 RCE 和免检后门堵上,那多少还算是个负责任的维护者,毕竟没要钱,这种被广泛使用的开源项目,我感觉维护者只要能力所能及地把大问题修了就行。
于是我兴冲冲地登录上测试服务器的宝塔面板,把 8.5 的最新安装包(free_waf.tar.gz)下载下来,解压,拉进本地仓库,跟上周那份被审计得千疮百孔的 8.4 源码执行一下diff命令:
git diff -w free_waf_8.4/ free_waf_8.5/
看着终端里的 diff 结果,我直接愣住了。
随后,我TM直接蚌埠住了,我决定收回刚才看到作者更新时的那丝欣慰。
这哪里是安全修复啊?这特么是一场足以载入中文开源史册的赛博行为艺术。
整个 8.5 版本,改动量小到一只手就能数得过来。
一万七千多行的工程文件,只动了 7 处。
首先给大家欣赏一下本次 8.5 更新中最具黑色幽默气息的核心“修复”:
--- a/free_waf_main.py
+++ b/free_waf_main.py
@@ -304,6 +304,7 @@
+ # 云端查杀。需要上传到云端进行研判
def send_baota2(self, filename):
cloudUrl = 'https://www.bt.cn/api/panel/btwaf_submit'
pdata = {
是的,你没看错,他在那个未经用户允许、静默把整份 PHP 源码和服务器 IP 打包外传的函数上方,郑重其事地敲了一行中文注释:
# 云端查杀。需要上传到云端进行研判
我整个人都不知道该用什么表情了
不是,他这行注释是写给谁看的?!
原本他的行为叫“隐蔽外传用户商业源码与敏感配置”;
加了这行注释之后,是不是在作者的逻辑宇宙里,这事能原地变得合情合理合法合规了?
“谁说我的 Nginx 免费防火墙偷偷摸摸静默传用户的源码了?那是你们没看代码啊!我第 304 行不是写了‘需要上传研判’吗?我都明确交代过了,是用户自己不看注释的,我告知用户了,绝对不是未经允许的静默上传行为!”
按照这个逻辑,入室行窃的小偷只要在撬棍上贴一张便签写着“入室研判,需将物品带回家里进行成色评估”,那在警察和法官面前就属于正常开展业务调研了对吧?!


free_waf_main.py 的 247-293 行)free_waf_main.py 的 142-160 行、304-338 行——两者是不相干的。被发现存在静默上传后,他哪怕在前端界面加个复选框让用户自己选呢?哪怕改成行业通用的“只上传文件 SHA-256 哈希比对”呢?
诶,他不!他甚至懒得去动哪怕一行执行代码,硬生生在毫不相关的地方加了一行注释,就仿佛给自己念了一句免责咒语,这个偷偷上传用户文件、引起泄密的问题就被他“完美修复”了。
剩下的 6 处修改,也是本次更新唯一的代码改动:作者把全局 6 处明文接口的 http://,批量替换成了 https://。
我哭死,作者他心里是有网络安全标准的!(虽然作者自己博客的证书都过期好久了)
userInfo.json(access_key 和 uid),在公网上明晃晃地裸奔;中间人确实嗅探不到了,中间人直呼内行。
可问题是:你擅自偷偷把用户的核心数据未经授权打包送给其他人(主要是宝塔面板自己的)这个事实,变了吗?!
合着把你的银行卡和密码,从放在快递袋里寄给陌生人改成放到保险箱里寄给陌生人,它就不是寄给陌生人了吗?!

上一篇文章里所有致命安全漏洞,在 8.5 里纹!丝!未!动!
我们来盘点一下那些在 8.5 里依然活蹦乱跳的“活化石”:
continue_key):
4.7.lua 第 1668 行的逻辑一行没动。只要 POST 参数名叫 kw、body、content,或者参数名长于 64 字节,WAF 依然当场眼瞎,SQL 注入和 XSS 载荷依然可以长驱直入。
算术验证码 ?btwaf=nil 秒破白送 1 小时通行证:
4.7.lua 第 573 行的 tostring(vn3) == uri_request_args['btwaf'] 依然活着。判断前依然不检查 vn3 ~= nil,黑客只要在 URL 后面带个 ?btwaf=nil,依然当场获取免检金牌。(这个倒是没默认启动,但是……既然这段逻辑完全没用,删一下花不了你几秒吧,起码比你打那段注释用的时间短吧)
硬编码 hackcnm 专属木马免杀白名单:
free_waf_main.py 第 136 行的 pc = 'hackcnm' 依然安详地躺在代码里。名字带 hackcnm 的木马依然享有这套防火墙的终身免检免死金牌。
本地管理路由 min_route 零鉴权一键解封:
4.7.lua 第 1777 行依然只认 remote_addr == '127.0.0.1'。本地的任何人依然能一键清空全网封禁 IP。
OpenResty 全局变量协程串号大乱炖:
4.7.lua 里十几个核心上下文变量依然连个 local 都舍不得加,并发稍高依然在内存全局表里疯狂串号、跨请求覆盖。
哪怕……哪怕你顺手在验证码那儿加一行 if vn3 ~= nil then 呢?5 个单词的事情。
哪怕你顺手把那个侮辱智商的 hackcnm 给删了呢?敲两下 Delete 键的事情。
他不改。他连一个字符都懒得去碰。
从 8.4 到 8.5,版本号膨胀了整整 0.1,核心改动就 6 个字母加 15 个汉字。这已经不是“技术水平有限”能解释的了,只能说这是彻头彻尾的敷衍。
在上周的文章发布前,我其实还在猜测:作者可能只是当年水平不足,东拼西凑改了改代码,自己并不清楚这些深层逻辑漏洞的危害。
但这个 8.5 版本,彻底击碎了我所有的幻想:
他不仅看懂了大家的反馈,他甚至知道大家在说他明文外传源码,所以他特地改了 HTTPS,还煞有介事地加了行注释解释了一下。但他压根就不在乎用户的安全死活,更不想花哪怕半小时去修修那些烂到骨子里的底层逻辑。 我不会说他做的不对,毕竟当下 WAF 业务确实不咋赚,更何况这是个免费的防火墙。
你的服务器安不安全并不重要,重要的是你挂着它,把它当个心理安慰剂,顺便把服务器的数据规规矩矩地供奉上去,来让厂商作为分析网络安全情况的数据。
话已至此,多说无益。要不要卸载,你们自己掂量。
PS:此文发布后,宝塔免费防火墙已经更新 8.6 了,我也分析了,只能说也很幽默,虽然扫描木马时确实不再上传用户文件了,但是修得也相当敷衍(未完待续……)
本文 宝塔 Nginx 免费防火墙 8.5 更新了什么?改了 6 个字母 s 和一行注释,漏洞一个没动 最初发表于 秋风于渭水。
2026-10-30 00:26:17
#运维 #吐槽 我又双叒叕被自己的安全配置拦到服务器外边了😂
这次是自己的锅,最近扫描器泛滥,每天光试图访问/.user.ini的IP就有1000+个,于是呢,我就大手一挥,在前端机的Nginx加了4行配置
location = /.user.ini {
access_log /var/log/nginx/blocked.log combined;
return 444;
}
(实际location 是写了一个很复杂的正则表达式啦,写成上边这样是方便大家理解)
fail2ban会盯着这个blocked.log文件,把里面444的IP都给封掉(DROP),结果啊结果,我居然忘了把后端机的IP放到fail2ban的白名单里了。😂
于是后端机的IP就被前端机的fail2ban给封掉了,偏偏WordPres和里面的插件一些请求涉及自己请求自己时,用的是域名而不是本地回环请求,于是,很多请求就只能等网络20秒超时了。
这也是最近大家发现在博客里发评论后,需要等好久评论才能成功发出去的根本原因。
表层原因原因是缓存管理插件在出现新评论后需要刷新页面缓存,升级到最新的PHP版本后,他代码逻辑出了点问题,导致报错,偏偏他这里报错的逻辑里是请求的主站域名路径,而不是本地路径,还重试了1次(正好DROP是10+10秒超时)所以导致评论最少也需要20多秒才能显示出来。
总而言之:是我自己犯傻了
2026-10-28 18:09:57
#运维 #吐槽 宝塔免费防火墙,我现在是强烈建议你们删了,永远别用了。
我静默明文上传问题的文章是10点半发的,下午18点作者就更新了8.5版本。
你们猜他怎么修的?一共修了7行代码
6行是给http加了个s,1行是在静默上传用户文件的函数处加了一行注释# 云端查杀。需要上传到云端进行研判。
我… 我直接笑出猪叫好不。他这行注释是写给谁看的?!是写给作者的 Python 解释器看的吗?还是写给作者的良心看的?还是说宝塔面板可以智能到,发现这行注释,当用户在前端面板里点下“木马查杀”的时候,屏幕上会凭空弹出一行大字:“请注意,作者已经在源码里留了注释,即将把你的 PHP 源码送去云端研判”?!
以前不声不响静默上传,真掰扯,我也只能说有“未经授权的数据外传”;
作者现在加了这行注释,行为直接原地升华了,好嘛,变成了自欺欺人的“坦白从宽”。
然后,8.4 升 8.5 这种小数点后一位的升版,居然只是加个6个字母s和一行中文注释???纯粹就是傲慢与敷衍的结合体:“我看了到了报告,但我就是不修,我就偷偷上传你们的文件,我就是给黑客的攻击参数留下后门,我就是不修,你们免费的用户不配浪费我的时间去修复这些复杂问题”
2026-10-26 10:43:48
声明:本文基于本地源码审计与测试机全新安装后的实测记录整理。感谢大肥鱼(DeepSeek)、克劳德(Claude)、奥特曼(ChatGPT)、北美豆包(Gemini)各路 AI 评审员协助完成交叉代码复核。
这事儿得从上周四下午的一条读者留言说起:
【安全咨询】宝塔 Nginx 免费防火墙是否存在自动上传源码和凭据明文传输的问题?
……
静态检查发现以下行为:
send_baota2()在 WebShell 扫描匹配规则后,可能把完整文件内容上传至http://www.bt.cn/api/panel/btwaf_submit。……
目前仅有静态代码检查结果,没有证据表明存在恶意后门或已经发生实际数据泄露。
其实之前给宝塔 Nginx 免费防火墙 8.3 打补丁的时候,我确实扫到过这段代码,但当时根本没往深处细想,只当是某种云查杀样本库逻辑,加上我日常也不用面板自带的木马查杀,脑子里一下就划过去了。
今天下午看到这条评论,我翻出源码看了一眼这部分的代码逻辑
好家伙,居然是真的!而且确实走的是明文的 HTTP!!
我整个人都惊呆了:不是,这玩意私底下走 HTTP 的地方还有什么?!(我第一个反应:这不就是某输入法明文上传用户输入信息事件的翻版嘛)
简单肉眼过了一下代码,我当场差点从工位上蹦起来:
userInfo.json,把面板绑定的 access_key 和 uid 直接走明文 HTTP 发给云端?!make && make install 现场编译运行?!有点离谱了吧?!这哪是防火墙,这分明是个”特洛伊木马全家桶”啊!
整个插件本体加规则,足足有接近2万行,指望我一个人看是看不过来的(而且我这个纯业余的水平)于是索性直接把 宝塔 Nginx 免费防火墙 8.4(署名作者”民国三年一场雨”,官方发布页在这儿)的代码结构拉出来,把相关文件和脚本都扒出来,摇上大肥鱼(DeepSeek)、克劳德(Claude)、奥特曼(ChatGPT)、北美豆包(Gemini)四路 AI 一起做了一轮交叉全量静态审计。
看完最终整理出的4方审查报告,我脑瓜子嗡嗡的,忍不住先在博客里发了一条碎碎念说说给大伙儿预警一下。
周末忙完工作,我抽空在自己的测试机上全新安装了 宝塔 Nginx 免费防火墙 8.4,做了真机实测(走本地回环链路闭环测,不碰公网,我能构造出测试环境的都实测了)。
真机跑下来,推翻了部分 AI 过度推断的吓人言论,将 P0 级问题数量从 20 多个压缩到了 13 个,比如:
multipart 头就能绕过 POST 防护——2024 年之后云端下发的规则给后面加了独立的 post_data() 来兜底,实测 -F "id=1 union select..." 仍然是 403;但真实现状,也没好到哪里去。
几条逆天漏洞,在真机上全·部·复·现·了。
属于那种但凡稍微不那么业余的 Linux 运维或者 Web 开发,看完都会当场吸氧的神奇代码。
顺便说一句,这插件上一回就出过事,上次那个 undefined 的锅还没凉透呢。
http改成了HTTPS,从明文上传,变成了加密上传。该偷偷外传的还偷偷外传,该绕过的还能绕过,最他居然在8.5里偷偷上传用户文件的地方,加了一行注释# 云端查杀。需要上传到云端进行研判,有时间加注释,给自己免责,没时间在面板上写一句“Webshell扫描会自动静默上传一切疑似文件”。所以我只能理解为:插件的静默上传和漏洞绕过都是作者允许并故意设计的。触发条件:在面板点过一次「扫描木马」,且站点里有任意 .php 文件命中规则。
代码位置:free_waf_main.py:142-160(scan 函数)与 304-309(send_baota2 函数)
你在宝塔面板点”webshell查杀”,以为这只是在本地拿规则扫一遍文件?
天真了我的朋友,现实是:只要你服务器上的任何一个 .php 文件命中了规则,它就会在后台神不知鬼不觉地调用 self.send_baota2(file)。它既不需要你点击确认,更不会弹窗问你”是否上传样本分析”。
# free_waf_main.py 第 154-158 行
result = {}
result[file] = r
self.__webshell_data.append(result)
# 云端查杀。需要上传到云端进行研判 # <--- 8.5新增注释,但用户界面上完全没提示过会上传云端
self.send_baota2(file) # <--- 毫无征兆地直接把文件送走
那这个 send_baota2 到底准备干什么呢?咱们继续看代码:
# free_waf_main.py 第 304-309 行
def send_baota2(self, filename):
cloudUrl = 'http://www.bt.cn/api/panel/btwaf_submit' # <--- http明文上传
pdata = {
'codetxt': public.ReadFile(filename), # <--- 文件全文
'md5': self.read_file_md5(filename),
'type': '0',
'host_ip': public.GetLocalIp(),
'size': os.path.getsize(filename),
}
ret = public.httpPost(cloudUrl, pdata)
return True # <--- 永不检查结果,绝对是true
啥玩意,直接上传??还是 http 链路?
为了抓包验证,我在测试机上写了一个只有 108 字节的测试文件,在里面故意留了一行假数据库密码:$db_pass = "SECRET_MARKER_DB_PASSWORD_42";。
实测截获的数据包如下:
url : http://www.bt.cn/api/panel/btwaf_submit
codetxt len : 108
codetxt : '<?php\n// WAF-SEC-AUDIT-MARKER-20261022-UNIQUE\n$db_pass = "SECRET_MARKER_DB_PASSW...'
host_ip : <面板请求的 Host 头>
md5 : f6e4c135a97d1616562346fd2edfdebc

108 字节的代码全文,连同里面的假数据库密码,一字不差地打包走明文 http:// 送出去了!
这个 host_ip 字段public.GetLocalIp() 在面板里取的是 request.headers.get('host'),也就是你访问面板时用的那个 Host 头(1.2.3.4:8888 之类)对于大部分人来说,就是你服务器的公网 IP。
更绝的是它的查杀规则写得极其宽泛:"str_rot13"、base64_decode\($_、"eval(" 这些都能命中(rule.json 一共 52 条规则,含 eval 的 8 条、含 base64_decode 的 4 条、含 str_rot13 的 2 条)。就问你,随便找个现代 PHP 程序、WordPress 插件、甚至任何涉及加解密的程序,有几个能逃过这些规则?
这他喵的一命中,你的项目代码、里面的数据库链接信息,就在不经意间顺着明文 HTTP 裸奔到了外部。
其实下边的 send_baota()(311-323 行)更厉害:它接收的是一个服务器本地文件路径(不是上传的临时文件),代码只做了一次 os.path.exists() 检查,没有任何后缀或目录限制,/etc/shadow、panel/data/default.db、root/.ssh/id_rsa 都能当”待检样本”发出去(用户在面板里手动上传文件的链路)。也就是说,只要面板接口被调用,它就等价于”读本地任意文件并外传”。
触发条件:任何一次 POST 请求,只要参数名落进白名单,或参数名长度超过 64 字符。
代码位置:4.7.lua:1668-1678(continue_key 函数)
如果说上面的文件外传是内鬼行为,那这条过滤规则就是给黑客铺红毯。Nginx 免费防火墙 8.4 在检查 POST 请求参数时,很贴心地写了一个白名单:
(你在面板安装免费防火墙时,安装脚本会自动从宝塔的 CDN 拉取 free_waf_data.zip——这是防火墙的数据文件,4.7.lua 就是其中给 Nginx 用的脚本)
function continue_key(key)
if method ~='POST' then return true end
key = tostring(key)
if string.len(key) > 64 then return false end; -- <--- 参数名的长度超过 64 个字符
local keys = {"content","contents","body","msg","file","files","img","newcontent","message","subject","kw","srchtxt",""} -- <--- 参数名叫这些名字的话
for _,k in ipairs(keys)
do
if k == key then return false end; -- <--- 直接返回 false
end
return true;
end
啥意思呢:就是在处理 POST 请求时,只要参数名的长度超过 64 个字符,或者参数名叫 kw(搜索关键字)、body(正文)、content(内容)、msg(留言)、file 之类的名字……它就直接返回 false。
然后呢,你注意看啊,它在外层的处理逻辑里,可是只要返回 false,就让整个参数直接跳过检测的!
我在测试机上跑了几个测试:
id=<script>alert(1)</script> → 拦截(拦截了XSS,正常工作)[65个字符的长参数名]=union select 1,2,3 → 放行(绕过成功!)kw=union select 1,2,3 → 放行(绕过成功!)body=<script>alert(1)</script> → 放行(绕过成功!)同一个 XSS 载荷,参数名叫 id 就被 403 铁拳制裁,但是只需要改一个名字,咱们叫 kw 或者 body,防火墙就当场直接给放行了。
我琢磨了一下作者可能的心路历程:”啊啊啊啊,规则怎么总误伤常见的正常程序啊,有了,我直接把它们设置成白名单不就减少误拦截率了嘛。”
然后直接把最容易在XSS上出事的正文、留言、搜索框全设置为免检了?一次 WAF 绕过就这么简单,这 WAF 到底在防什么?
PS:而且他这白名单也没白到位,如果开着防火墙的 POST 检测,像是这篇文章这种正文代码块中有涉及加解密参数、外源加载命令的文章,反而会被他的POST检测拦截掉。攻击者能走白名单,管理自己反而被拦截是什么鬼啊。
config.json:一次响应就能把 WAF 配置写坏触发条件:网络遭遇中间人劫持,或宝塔的接口被投毒,返回了非预期结构的 JSON。
代码位置:free_waf_main.py:936-942(test_check_zhilist 函数)
在同步所谓”云端蜘蛛列表”时,插件代码写成了这样:
# free_waf_main.py 第 936-942 行
url = 'http://www.bt.cn/api/bt_waf/getSpiders'
data_list = json.loads(public.httpPost(url, data22, timeout=3))
if data_list:
for i22 in data_list:
path = "/www/server/free_waf/%s.json" % i22 # <--- 哪有这么写的?
if os.path.exists(path):
ret = json.loads(public.ReadFile(path))
localhost_json = list(set(ret).union(data_list[i22]))
public.WriteFile(path, json.dumps(localhost_json))
宝塔 Nginx 免费防火墙先从明文 HTTP 接口拉取一个字典,然后拿字典的 Key(i22)直接拼接文件路径。
作者大概非常笃定,返回的 Key 只会是 1、2 这种编号,所以代码里没有任何校验和限制!
如果链路上遭遇中间人劫持,或者远端接口返回一个 {"config": ["x"]},会发生什么?
咱自己当自己的中间人实际测试一下呗,原本结构完好的 config.json 字典对象({"open": true, "log": true, ...})瞬间被变成了一个JSON 字符串数组:["retry_cycle", "log", "retry", "open", "x", ...]。

注意,它此时还是一个合法的 JSON。语法完全正确,json.decode 照样能解析。问题出在类型上——现在解析出来的是个数组,不是字典了。配置结构一旦被破坏,Lua 在加载时读到的 config['open'] 就是 nil。
紧接着,4.7.lua:2054 的逻辑被触发:
if not config['open'] then return false end
WAF 就不再执行后面那一整套检测了。
那到底会怎样?开始我以为会直接让WAF失效,实测了一下,解决比”直接让WAF失”还更麻烦:
| 阶段 | 操作 | 实测结果 |
|---|---|---|
| ① 文件改坏了,不 reload |
config.json 变成数组 |
WAF 完全不受影响,SQLi 照样 403。因为 config 是 init_by_lua 阶段读的,只改文件不会触发重载 |
② 执行 nginx -s reload
|
触发新 worker | 40 次 SQLi 请求,35 次放行、5 次 403。新旧 worker 的 Lua VM 并存,请求落到哪个 worker,结果就不同 |
| ③ 继续访问 | — | 出现站点返回 000(连接失败),nginx 重启后恢复 |
也就是说:它不是”立刻就让WAF关了”,是”下次你改Nginx配置或打开防火墙页面改了东西关闭后开始漏拦截,而且漏得很随机”。
这个”不确定”本身就是灾难:同一个风险,有时拦有时放,看日志定位原因的难度大幅提升。
再说面板这边的情况,宝塔 Nginx 免费防火墙的界面会变成这样的:防火墙已经开启的绿色开关还在 + 一个”请求过程发现错误!”的弹窗 + 拦截统计和安全天数全空。所以你可能怀疑防火墙面板坏了,但可能不会怀疑防火墙已经不工作了。
?btwaf=nil 秒破:白送 1 小时免检金牌触发条件:站点开启了「CC 增强模式」(cc.increase),触发验证码后,在 URL 后面挂一个 ?btwaf=nil。
代码位置:4.7.lua:573-582(security_verification 函数)
这里得先补一个前提,这个「CC 增强模式」默认是关闭的,在默认配置下根本不会被激活:这部分代理代码cc2() 的第一行是 if not site_config[server_name]['cc']['increase'] then return false end,而全新安装的 site.json 里,站点的 cc 只有 open / cycle / limit / endtime 四个可用键值,压根没有 increase 字段。我是手动加了 "increase": true 再 reload,才让这段逻辑跑起来的。(属于残留的无用逻辑,但可以看出作者那时候的代码水平)
前提说完了,我们来看实际代码:
function security_verification()
if uri_request_args['btwaf'] then
vn3,_ = ngx.shared.free_waf:get(ip)
if tostring(vn3) == uri_request_args['btwaf'] then -- <--- 这就直接比了?
ngx.shared.free_waf:delete(ip)
ngx.shared.free_waf:set(ip,666,3600) -- <--- 贴心的1小时免检
return false
end
end
注意看第 4 行:它在拿到缓存值 vn3 后,它完全没有判断 vn3 ~= nil,而是直接把它转成字符串去跟 URL 传进来的 ?btwaf= 比较!
蛤?在 Lua 里,tostring(nil) 的返回值正是字符串 "nil" 好不!
在测试机实测:触发了 CC 防护验证码时,只要在 URL 后面挂上 ?btwaf=nil,校验当场通过!

实测记录(对照组很关键):
| 请求 | 结果 |
|---|---|
127.0.0.2 首次触发 + ?btwaf=nil
|
404 放行 |
127.0.0.2 第二次(不带参数) |
404,因为 666 还在生效 |
127.0.0.3 首次(对照) |
弹验证码 4 + 18 = ?
|
127.0.0.5 首次(对照) |
弹验证码 17 + 17 = ?
|
通过之后,代码下一行还贴心地附赠一道护身符:ngx.shared.free_waf:set(ip, 666, 3600)。只要拿到这个 666 标记,接下来整整 1 小时内,该 IP 会被视为受信任访客,跳过所有 CC 频率检测。
所以刷量脚本连识别验证码的步骤都省了,直接在请求后缀挂上 ?btwaf=nil 就能畅行无阻。当然前提是你手动去把这个”增强模式”打开了——默认配置下这段逻辑压根不执行,我只是想吐槽下这个代码写的真的是有点毛糙。
真要防刷流,还是去看对抗刷流攻击的那套 Nginx 打法吧,至少规则是自己写的、自己看得懂。
触发条件:管理员打开防火墙插件设置面板,触发 get_total_all 逻辑。getSpiders 受 session 限制每天一次,addSpider 则是每次都会跑。
代码位置:free_waf_main.py:929-946, 978-983
如果你在看上边第 3 个问题时稍微往上看几行就会发现一个更加神奇的逻辑:
# free_waf_main.py 第 929-946 行
def test_check_zhilist(self, get):
try:
userInfo = json.loads(public.ReadFile('/www/server/panel/data/userInfo.json'))
data22 = {"access_key": userInfo['access_key'], "uid": userInfo['uid']}
url = 'http://www.bt.cn/api/bt_waf/getSpiders' # <--- http明文上传
data_list = json.loads(public.httpPost(url, data22, timeout=3))
# free_waf_main.py 第 978-983 行
userInfo = json.loads(public.ReadFile('/www/server/panel/data/userInfo.json'))
data22 = {"type": i, "ip_list": json.dumps(tt),"access_key": userInfo['access_key'], "uid": userInfo['uid']}
url='http://www.bt.cn/api/bt_waf/addSpider' # <--- 还是http明文上传
if len(tt)>=1:
public.httpPost(url, data22)
public.WriteFile('/www/server/free_waf/zhizhu'+i+'.json',"")
session_id = self.__get_md5("free_waf_test" + time.strftime('%Y-%m-%d'))
if not session_id in session:
self.test_check_zhilist(None)
getSpiders 这条每个管理员每天只会触发一次;而下面的 addSpider 部分,只要 zhizhu*.json 里有内容,每次打开面板都会触发。
不管是每天一次还是每次吧,咱们的结论不会变:它会把 access_key 和 uid 装进 JSON,直接通过明文 HTTP 发送给了远端接口(http://www.bt.cn/api/bt_waf/getSpiders 和 addSpider)。
这东西发给宝塔服务器本身的行为是没问题的,这属于插件的鉴权措施,宝塔面板里的很多收费插件也有类似的逻辑,来验证你是否有启用插件的权限。
但问题是,宝塔 Nginx 免费防火墙用的是 HTTP,是明文不加密的,中间链路随便搞点劫持嗅探,你的宝塔官方账号凭据就直接拱手送人了。
而且这事还有下半场:addSpider 拿回来的那个”蜘蛛 IP 列表”会被写进 zhizhu*.json,而 4.7.lua 的 zhizu_chekc() 会把这些 IP 直接塞进 ngx.shared.spider 全量放行。HTTP 明文通道是能被中间人劫持注入的,就等于能远程往防火墙的”免检名单”里塞人——这其实比凭据泄露本身更吓人。
触发条件:系统里缺 /usr/local/lib/lua/5.1/cjson.so,然后你打开一次插件设置页。
代码位置:free_waf_main.py:1163-1188(__check_cjson 函数)
# free_waf_main.py:1163行-1188行
def __check_cjson(self):
cjson = '/usr/local/lib/lua/5.1/cjson.so'
if os.path.exists(cjson):
if os.path.exists('/usr/lib64/lua/5.1'):
if not os.path.exists('/usr/lib64/lua/5.1/cjson.so'):
public.ExecShell("ln -sf /usr/local/lib/lua/5.1/cjson.so /usr/lib64/lua/5.1/cjson.so");
if os.path.exists('/usr/lib/lua/5.1'):
if not os.path.exists('/usr/lib/lua/5.1/cjson.so'):
public.ExecShell("ln -sf /usr/local/lib/lua/5.1/cjson.so /usr/lib/lua/5.1/cjson.so");
return True
c = '''wget -O lua-cjson-2.1.0.tar.gz http://download.bt.cn/install/src/lua-cjson-2.1.0.tar.gz -T 20
tar xvf lua-cjson-2.1.0.tar.gz
rm -f lua-cjson-2.1.0.tar.gz
cd lua-cjson-2.1.0
make
make install
cd ..
rm -rf lua-cjson-2.1.0
ln -sf /usr/local/lib/lua/5.1/cjson.so /usr/lib64/lua/5.1/cjson.so
ln -sf /usr/local/lib/lua/5.1/cjson.so /usr/lib/lua/5.1/cjson.so
/etc/init.d/nginx reload
'''
public.writeFile('/root/install_cjson.sh', c) # <--- 脚本内容里含 http 下载
public.ExecShell('cd /root && bash install_cjson.sh') # <--- 不校检直接root权限运行了
return True
明明安装包里已经带了编译好的 cjson.so(install.sh:109-111 会把它复制到 /usr/local/lib/lua/5.1/、/usr/lib64/lua/5.1/、/usr/lib/lua/5.1/),但偏偏作者以防万一写了一个校验函数:每次打开插件首页(get_total_all() 第一行就是它),如果发现系统路径缺了文件,就在 /root 下生成一个 Shell 脚本,通过明文 http:// 下载源码包,并以 root 权限就地执行 make && make install。
不是,这是什么胆子啊,用 HTTP 拉取云端文件,然后看都不看就写入本地文件,直接用 root 权限编译并执行了?
到了 P1 级别,看似是一些”虽然存在但不太好直接利用”的问题,但这几个逻辑依然让人哭笑不得:
hackcnm 木马免杀后门代码位置:free_waf_main.py:120-140
在遍历站点文件准备查杀木马时,作者留下了这样一段神仙逻辑:
# free_waf_main.py 第 133-140 行
def getdir_list(self, get):
path = get.path
if os.path.exists(path):
pc = 'hackcnm' # <--- 不是,这啥玩意?
rs = self.getdir(path, pc)
return rs
而在递归查询站点根目录搜索文件时:
elif str(l.lower())[-4:] == '.php' and str(dir + '/' + l).find(pc) == -1:
lis.append(dir + '/' + l)
啥?只要路径或者文件名里包含 hackcnm,Webshell 查杀就直接跳过该文件!?
所以,只要把 Webshell 命名为 hackcnm.php,或者扔在 hackcnm/ 目录下,在木马查杀里就能享受到永久免检待遇了???
代码位置:4.7.lua:1953-2026, 2095-2103
宝塔 Nginx 免费防火墙为了不误伤搜索引擎蜘蛛,设计了 PTR 反查机制,这确实是个很好的机制。但是,但是啊,在匹配域名时,使用的是无头无尾的模糊匹配,比如对百度的:string.find(ptrdname, "baidu")。只要含有baidu就行
而且更离谱的是,一旦请求被判定为合法的搜索引擎蜘蛛,WAF 在 run_btwaf() 里会把完整检测步骤里 18 个函数中的 14 个直接跳过:drop()(黑名单)、drop_abroad()(海外阻断)、cc() / cc2()(CC 防护)、user_agent()(UA检测)、url()(GET防护)、cookie()(cookie检测)、ThinkPHP_RCE5_0_23()、ThinkPHP_3_log()、scan_black(),以及站点级 URL 检测——只留下 args()、post()、post_data()、post_data_chekc() 四个参数级检测。
还记得前边「P0-2」是什么问题不?「处理 POST 请求时,只要参数名的长度超过 64 个字符,或者参数名叫 kw(搜索关键字)、body(正文)、content(内容)、msg(留言)、file 之类的名字,POST检测就可以跳过」
所以只要攻击者给自己的一台 VPS 配个类似 baidu.攻击者自己的域名.com 的 PTR 记录,再带上百度蜘蛛的 UA 发起请求,这套防火墙的防护模块就只剩args()对他有效了。
百度起码是匹配的 baidu,你猜对 360 爬虫它匹配的是什么?是 0,是数字 0 啊!!
123-45-60-78.static.isp.com 这种默认 rDNS 里带个 0 太常见了,虽然不能说人人都能变成 360 的蜘蛛,但这个几率真的不低,毕竟为了格式补位,PTR 里含有0的几率实在是太高了。
min_route 零鉴权一键清空黑名单代码位置:4.7.lua:1777-1786
Lua 里开了一个本地管理路由:
function min_route()
if ngx.var.remote_addr ~= '127.0.0.1' then return false end
if uri == '/get_btwaf_drop_ip' then
return_message(200,get_btwaf_drop_ip())
elseif uri == '/remove_btwaf_drop_ip' then
return_message(200,remove_btwaf_drop_ip())
elseif uri == '/clean_btwaf_drop_ip' then
return_message(200,clean_btwaf_drop_ip())
end
end
它只判断 remote_addr == '127.0.0.1',完全没有任何 Token 校验……我该说点什么好呢。
当然这个洞的利用要分场景,不能一概而论:
remote_addr 就是真实客户端 IP,外部直接打 GET /clean_btwaf_drop_ip 是打不进来的;remote_addr 全是 127.0.0.1,这时候一打一个准;curl、图片代理、webhook 回调、被拿下的低权限进程),只要能访问 http://127.0.0.1/,就能把封禁名单清空。也就是说,它用”来源地址”当”身份凭证”,把”本机来源”等同于”可信来源”。
如果说上面的漏洞是安全意识淡薄,那 P2 级问题就是纯正的软件工程事故:
4.7.lua 里的 run_btwaf() 定义了十几个核心变量(ip、uri、server_name、request_header),全都没加 local 声明! 这些变量统统挂在全局 _G 表里。当系统发起异步 DNS 查询协程挂起时,只要有并发请求进来,全局变量瞬间就被后一个请求覆盖。你的 IP、我的 URL 混在同一个 Worker 里,误封、漏封全看运气。(此条未做实测——测试需要足够的并发测试资源)
【磁盘】每次拦截无锁全量读写 total.json:每次触发拦截,都要把整个统计文件全量重写一遍。
| 场景 | 请求数 |
total 增量 |
磁盘写入 |
|---|---|---|---|
| 正常请求(404) | 200 | 0 | 220 KB |
| 拦截请求(403),串行 | 200 | +200(100%) | 1.18 MB |
| 拦截请求(403),并发 | 200 | +120(60%) | — |
说人话就是,会漏统计数据,多个 Worker 同时并行读-改-写,后写的会直接覆盖先写的。从而使得统计不可信,你看着面板上那个”已拦截 XXX 次”,实际可能少了一半。
.+)的畸形正则去跑匹配,虽然不能下一定会出事的结论,但确实存在可 ReDoS 攻击的点位。
【证书】下载扫描结果时关闭了证书校验: 上传文件给百度的接口检测webshell时走 HTTPS,这时候curl 默认会验证证书;但是”取扫描结果”时,却又特意把证书验证给关了。这导致中间人可以伪造返回的扫描结果:把真 webshell 说成”未查出风险”,或者把正常文件说成木马。
【白名单】你以为你开了禁止国外 IP 就真进不来了? 其实作者偷偷写了 5 个硬编码的白名单 IP:91.199.212.132/133/148/151/176。我查了一下,91.199.212.0/24 归属 AS48447 SECTIGO(英国,2008 年 RIPE 分配),是 CA 机构做 AutoSSL 域名控制验证的节点——所以这5个IP不是后门,防火墙之所以给白名单,是怕你开了”禁止海外访问”后,面板的证书续签功能炸了。但怎么保证这 5 个 IP 永远不会变呢?IP 一回收重分配,新持有者就自动继承了这份免检权。
【XSS】存在反射 XSS。 位于站点访客看到的 403 错误页,站长自己倒是没事
【权限】试图规则库全局可写:代码里是 chmod -R 666 /www/server/free_waf/rule 和 chmod 777 /www/wwwlogs/free_waf_log,归属 www:www。不过实测部署后目录是 drwx------ www:www(700)、文件 -rwx------——幸好宝塔的兜底目录策略把脚本意图覆盖掉了。
【写文件】任意文件写入 JSON:s_Name / ruleName 未校验直接拼 rule/xxx.json,攻击者可以玩路径穿越 ../ 穿越,以 root 向任意可写位置落 JSON,配置投毒好帮手啊。
【启动判断】神一样的判断 Nginx 成功启动的方式:判断 Nginx 成功居然是检测 netstat 输出中有没有出现字符串 80(netstat -nltp | grep nginx | grep 80 | wc -l)。这导致两个效果:
is_status 判 0,安装脚本会误以为把 Nginx 搞炸了,会走安装失败清理流程——删掉 free_waf.conf、重启 nginx、删掉 /www/server/panel/plugin/free_waf 这个插件目录(脚本自己也在里面),然后提示”安装失败”。注意它没删 /www/server/free_waf 主目录,会有残留文件。(把安装成功判定为安装失败)netstat 输出里还有 PID 字段呢,万一 PID 含 80(如 18042/nginx),脚本会因为匹配到 80 而判定”安装完成”,从 PID 的分布概率上来说误判率 4~5% 吧(把安装失败判定为安装成功)宝塔 Nginx 免费防火墙的普及率有多高,就不用我说了吧,我感觉基本只要在用这个面板的都会装一个。下面这份卸载清单,装过的都建议照着走一遍。
先花 30 秒思考一下三个问题:
/www/server/panel/plugin/free_waf/webshell.log 这个文件存在吗?存在且有内容 = 确认已经静默发出去了三条里中了一条,按下面的步骤操作,先确保自己的机器别成为筛子:

打开宝塔面板 → 软件商店 → 已安装,找到这个免费防火墙,停用并卸载。随后登录终端,先逐行确认下面的路径没写错——rm -rf 没有回收站,手一抖就是运维事故了——再执行清理:
# 检查并清理残留规则与运行目录(不放心就去面板的文件管理里手动删)
rm -rf /www/server/free_waf
rm -rf /www/server/panel/plugin/free_waf
rm -f /root/install_cjson.sh
# 检查 Nginx 配置中是否已解除挂载(确认没有残留 access_by_lua_file)
grep -rn "free_waf" /www/server/panel/vhost/nginx/
nginx -t && nginx -s reload
由于该插件会后台通过明文 HTTP 发送你的 userInfo.json,强烈建议登录宝塔官方后台(bt.cn),进入用户中心:
如果你之前在面板里点过”木马查杀”,去看一下这个查杀日志:
cat /www/server/panel/plugin/free_waf/webshell.log
凡是在这个日志里留下过名字的 .php 文件,都应当视为文件已经明文上传。(严格说 send_baota2 若因网络问题发送失败,文件未必被发出去过,但你没有证据证明它没送达,所以按”已泄露”处理。)
如果里面包含你的生产数据库配置文件(例如包含数据库密码、Redis 密码、第三方支付 Secret),请立刻全部改密码!
如果你需要基础的防 CC 和简单防护:
root:root 644,决不能图省事给它挂上可写权限。
免费的往往才是最贵的。这话我上次聊那个藏了 13 年的 Nginx 上古漏洞时就说过一遍,这次又应验了。
很多个人站长用宝塔,图的就是个开箱即用、省心省事。遇到”免费版 Nginx 防火墙”,很多人想都不想就装上去了,以为给自己服务器雇了个保镖。
谁能想到,这哪是保镖啊?
continue_key 只要参数名对上就直接免检放行);send_baota2 全自动静默上传);0的面具,它就主动递上一杯茶,顺手给发了一张 VIP 通行证(判定为360的爬虫,免检)。
审查报告的全文我就不发了,我就列个数字:完整报告里是 P0 级 13 个、P1 级 16 个、P2 级 12 个,这里只挑了最能说明问题的几条。
从本质上来说,宝塔 Nginx 免费防火墙这些漏洞,看起来都不像是恶意埋的雷(不存在故意制造漏洞或后门的行为),但作者为了使用方便、兼容性和不误判,留下来太多可以绕过防火墙的地方,说实话这东西要是自己写了给自己用的倒也算了,但是被大家这样广泛在生产环境使用,会给用户一种虚假的安全感。
不要建立:开源=安全、隐私的错误思想,更可能的情况是,确实开源了,但有技术的人不会用这个项目,没技术的看不懂这个项目,漏洞和问题代码就这么明晃晃的放在那里,很多年都无人发现。
以及宝塔面板因此收集了这么多年的用户文件,就算用户到宝塔的 HTTP 链路从来没有被劫持过,文件真的只发给了宝塔面板的官方接口http://www.bt.cn/api/panel/btwaf_submit,宝塔自身是否妥善保存了这些被自动上传的“待检测”文件呢?
虽然作者免费这么多年,并且坚持无偿更新的好心要领,但是面对这套代码,我除了苦笑,也就只剩那句老话了:
毁灭吧,赶紧的,累了。
2026-10-22 18:38:40
这真的是防火墙吗?论宝塔 Nginx 免费防火墙8.3/ 8.4 的代码写得有多离谱:
等我明天有空,再细看看代码(以上都是纯看代码得出的结论,没经过实际环境验证,但应该没差很多…吧…),给大家好好掰扯掰扯这里边的门道,我之前知道这玩意儿写得很粗糙,但没想到居然如此炸裂。
你以为你是给自己的服务器请了一个保安,其实这保安搞不好才是服务器里最大的威胁。