基于Nezha面板的僵尸网络工具分析
原创 花十一一 2026-11-03 09:03 北京

摘要:一台托管于 Contabo 法兰克福节点的服务器,以 Python SimpleHTTPServer 方式对外开放了文件列表,页面攻击脚本可被任意下载。目录本身不是零散脚本的堆积,而是覆盖扫描、验真、利用、植入与远控等攻击工具,sqlite.db 中仍有会话写入,痕迹未及清理,说明这套武器库当时处在使用状态。
协议层的成熟度高于常见的脚本拼装。RDB 报文级投毒并自研 Redis Module、ECDSA 签名伪造、nsenter 容器逃逸三项均在隔离环境中复现成功,其中 Portainer 私钥可直接产出合法签名头,绕过原有身份校验。编排层则相反:13 处工程缺陷中,cron 表达式语义错误是最重的一处,47 台主机的每日持久化任务从未执行,五条入侵路径的自动化在此断链。
C2 由原版 Nezha 面板与官方 agent 组成,二进制与官方发布版本字节一致,因此基于文件哈希的检测规则全部失效。有效检测须下沉到配置路径 /tmp/nz.yml、进程名 /tmp/na、systemd unit 与外联域名。
综合能力结构、代码习惯与运营纪律,该体系更接近个人或小团队自建的武器库。
一、样本概况
样本托管于Contabo法兰克福节点(144.91.103.172:8000),Python SimpleHTTPServer默认目录页,无任何鉴权。61个文件按功能分为五类:
类别 | 数量 | 关键文件 |
Python漏洞利用脚本脚本 | 18 | rogue.py rce_drive.py es13_rce.py pin_batch.py |
Shell调度脚本 | 9 | scanline.sh harvest_loop.sh ct_guard.sh |
Redis RCE模块 | 3 | exp.so(Debian 12编译) exp.so.bak(RHEL 8编译) |
密钥 | 2对 | Portainer ECDSA P-256私钥、SSH密钥对 |
C2面板数据库 | 1 | sqlite.db,49台主机记录+2697条登录会话 |
这些脚本之间并非彼此孤立。scanline.sh 每 30 分钟拉起一轮收割循环,循环内调用 rce_drive.py 完成验真与利用;一旦拿到 RCE,rce_drive.py 自动把公钥写入目标机的 authorized_keys,同时把落地记录回写进 sqlite.db。pipeline2.py、pin_batch.py 等脚本共用同一份 masscan 输出池,sqlite.db 则由面板与收割脚本同时读写,既是受害者台账,也兼作运营审计底账。源码、私钥、台账三类文件一并明文挂在无鉴权目录,单从这种存放方式看,运营方更在意的是扩张速度。
# scanline.sh#!/bin/bash# 产线1:持续 masscan 扫新段(每 2 小时一轮全段循环)# 5025 段 × 多端口 @300k ppsP=/opt/harvestL=/tmp/scan_round.logecho "=== SCAN $(date -u +%m-%d\ %H:%M) ===" >> $L# 19 端口masscan -p6379,6380,6381,16379,26379,2379,8088,6066,27017,9200,8123,8086,11211,5672,8983,4222,15672 \--rate=300000 -iL /tmp/segs.txt --open-only --excludefile /tmp/excl.txt \-oL /tmp/scan_new.txt >> $L 2>&1echo "scan done: $(grep -c open /tmp/scan_new.txt) open" >> $L# 合并到主池(去重追加)cat /tmp/scan_new.txt | grep "^open" | sort -u >> /tmp/multi.txt 2>/dev/nullsort -u /tmp/multi.txt > /tmp/multi_dedup.txt && mv /tmp/multi_dedup.txt /tmp/multi.txtecho "total pool: $(grep -c open /tmp/multi.txt)" >> $L
二、五条入侵路径
整套体系由 5 条独立入侵路径组成,按目标服务各建一条,共用同一个 masscan 扫描池(30 万 pps,17 个端口)和一套报告管道。每条路径都走同一个闭环:扫描,验真,利用,落地 agent,纳管面板。
有利用脚本的(5 类) | 只收集、还没写利用脚本的(储备) |
Redis 6379/6380/6381/16379/26379、YARN 8088、Spark 6066、ES 9200、Portainer 9001(跳板机本地扫) | etcd 2379、ClickHouse 8123、InfluxDB 8086、memcached 11211、RabbitMQ 5672/15672、Solr 8983、NATS 4222、MongoDB 27017 |
右边那 12 个端口全是经典未授权目标——说明操作者在为扩大打击面做储备。Portainer 9001 不走主扫,而是 15 台跳板机各自本地 masscan 后 SSH 回传汇总。

如图masscan 先产出攻击目标,五条路径并行完成验真与利用,落地动作统一为 /tmp/na 加公钥注入,最终汇入一个原版哪吒面板,战报经 Telegram 播报。
入侵路径 | 端口 | 手法 | 技术要点 |
Redis(主力) | 6379-6384 | 主从复制投毒 | RDB报文级伪造+自研Module |
Elasticsearch | 9200 | MVEL脚本注入 | 只对ES 1.x有效 |
YARN | 8088 | 假应用提交 | 伪装成sysmon-update |
Spark | 6066 | 假应用提交 | 按响应JSON验真 |
Portainer | 9001 | ECDSA签名伪造 | nsenter容器逃逸 |
Redis 路径:两步验真与反蜜罐
验真逻辑在pipeline2.py中实现,分两步:
s.sendall(b"PING\r\n")s.sendall(b"CONFIG SET dir /tmp\r\n")
第二步之所以必要,原因在于蜜罐通常只实现协议层应答:PING 与 CONFIG SET 都能回 +OK,而 CONFIG GET dir 不会返回数组格式,真实副作用无法伪造。存活池与可写池由此分离,后续的利用火力不会浪费在蜜罐上。
投毒实现:RDB报文级载荷替换
rogue.py在攻击机本地起rogue master,诱导受害Redis执行SLAVEOF拉取"RDB文件",实际传输的是Redis Module二进制exp.so。受害端MODULE LOAD后注册两条新命令:
system.exec // popen执行,有输出回显,用于验证与取数system.shell // system()执行,仅返回退出码,用于隐蔽操作
自研模块的动机并不复杂:现成工具通常只负责加载,加载之后便无法回读执行结果;自研版本一并提供有回显的 exec,RCE 之后可立即验证并取数。两个exp.so源码完全相同,分别以Debian 12和RHEL 8的GCC编译,对应不同glibc的受害环境,属于典型的预编译载荷矩阵。
落地与反取证
rce_drive.py共11步,最后两步将Redis的dbfilename和dir配置还原为原值。落地文件全部位于/tmp,agent为/tmp/na。事后仅审计Redis配置或日志,痕迹只剩一个/tmp/exp.so和一条MODULE LOAD。
Portainer 路径:ECDSA 签名伪造
五条路径中,这一条的技术复杂度最高。Portainer Agent与Server之间以ECDSA签名验证身份,签名内容为MD5("Portainer-App")。攻击者以本地私钥直接构造请求头:
md5d = hashlib.md5(b'Portainer-App').digest()ds = priv.sign(md5d, ec.ECDSA(Prehashed(hashes.MD5())))
验证通过后调用Portainer API定位特权容器,启动Privileged+挂载宿主根目录+共享PID命名空间的容器,随后nsenter -t 1进入宿主机PID 1的全部命名空间。经沙箱验证,目录中的ECDSA P-256私钥功能完整,可直接产出合法签名头。
植入与回连:命令执行到C2心跳
五条路径的利用手法各不相同,但漏洞利用之后的植入动作是标准化的。以 Redis 路径为例,链条分三级转换:未授权访问经CONFIG SET加SLAVEOF转为任意文件写,文件写配合MODULE LOAD转为受控命令执行,最后由rce_drive.py完成植入。落地是标准四件套:
文件 | 作用 | 特点 |
/tmp/exp.so | Redis恶意模块 | 一次性载荷,42行C源码,用完即弃 |
/tmp/na | agent本体(18.5MB官方nezha-agent) | 无扩展名非标准路径,规避关键词检测 |
/tmp/nz.yml | agent配置 | 指向C2域名n.uau.pp.ua:443 |
/etc/systemd/system/nezha-agent.service | 持久化 | Restart=always,进程被杀自动拉起 |
agent本体并非现场生成,而是托管在攻击者自己的分发目录(即8000端口武器库),受害机获得命令执行后直接拉取落盘。武器库同时承担武器仓库与投递源两个角色,这也是整个仓库必须公网可访问的原因。
植入细节有三处工程化设计:authorized_keys以grep -q ... || echo ...幂等写入,避免重复注入;systemd unit设置Restart=always实现进程级自愈;全部落地路径选/tmp,且RCE完成后还原Redis配置,只留最少痕迹。
回连通道则完全伪装成合法监控流量:agent读取/tmp/nz.yml后,经gRPC over TLS连向标准443端口的n.uau.pp.ua,用硬编码的agent_secret注册入面板,自动生成UUID命名并在面板侧触发Telegram战报播报。从流量形态看,与一家公司用哪吒监控自己的服务器没有区别。
回连侧信号比植入侧可靠,原因也在这里:agent 二进制是官方的,配置路径却不是——正常哪吒部署的配置位于标准安装目录,本案例却落在 /tmp/nz.yml、进程名为 /tmp/na。配合DNS+SNI关联封禁n.uau.pp.ua,整条回连链即可掐断,agent只会无限重连而永远收不到命令。
三、战果统计:49台失陷
阶段 | 规模 | 依据 |
masscan扫描池 | 73轮 | round_no.txt |
ES HTTP存活池 | 4467台 | esline.sh |
五条路径攻击目标总数 | 至少5000+ | 各脚本产物 |
失陷主机 | 49台 | sqlite.db servers表 |
受害机横跨 10+国家、涵盖阿里云/华为云/腾讯云/OCI/Contabo/Hetzner/OVH/Vultr/DO/Linode全部主流VPS——结合5025段的规模和300kpps的速率(全轮巡一遍需数天),扫描面是面向公网的广域扫描(大概率按云厂商/AS网段组织的清单),不是针对特定行业的定点打击。sqlite.db中有一台腾讯云失陷主机的备注原文完整记录了攻击链与验证方式。
id | 224 |
name |
|
note |
|
uuid |
|
created_at |
|
updated_at |
|
user_id | 1(admin) |
四、C2:原版哪吒面板
整个C2由原版哪吒(Nezha)监控面板v2.4.x与官方agent构成。将攻击者使用的agent与官方发布件逐项比对,结果如下:
对比项 | 攻击者的 | 官方发布件 |
nezha-agent大小 | 18571426字节 | 18571426字节 |
SHA256 | 130f212a...ab6 | 130f212a...ab6 |
特征字符串扫描 | 0命中 | 无 |
两者大小一致,SHA256一致,特征串扫描零命中。我们最初怀疑样本被改过,比对后确认没有。恶意性并不来自二进制篡改,而是来自配置与编排行为。
基于agent二进制哈希的检测规则,在本案例里基本抓不到东西。有效检测点在于配置文件路径(/tmp/nz.yml、/etc/nezha/config.yml指向异常域名)、新增systemd unit nezha-agent.service、以及外联域名。
五、持久化缺陷与画像
面板crons表中有3条远程下发的后门任务:
ID | 功能 | 调度表达式 | 覆盖 |
10 | keepalive,自愈公钥 | */5 * * * *(正确) | 2台 |
11 | 强制改回C2域名 | 0 0 0 1 1 *(写错) | 47台 |
12 | 每日重植SSH公钥 | 0 0 0 1 1 *(写错) | 47台 |
0 0 0 1 1 *为6字段cron,第5位是月、第6位是星期,1 1即每年1月1日,并非攻击者预期的每日01:01。这两条"每日自愈"后门从未自动执行。
实际生效的仅cron#10(每5分钟自愈公钥并放行22端口),但只覆盖2台。
基于以上事实,处置顺序应为:先断C2(域名、IP、面板),再清理主机。cron后门虽失效,agent仍在实时上报,先掐线才能避免清理操作被攻击者实时观察。需要说明的是,我们无法确认攻击者是否在其他机器上另置了持久化方式,因此断链应先行。
最让人意外的是conntrack自保:扫描进程一旦发现连接跟踪表占用超过40%,即向自身发送SIGSTOP暂停扫描。其余几项能力也值得记录:RDB报文级伪造并在EOF标记上做随机化以对抗检测,RCE完成后将Redis配置还原为原值的反取证操作,以及五条路径并行运行的增量收割模式。
工程缺陷共13处,择要:
缺陷 | 后果 |
cron表达式语义错误 | 47台的每日后门从未生效 |
X.java的uuid占位符未替换 | 同一jar落地机器UUID全部相同 |
esline.sh调用不存在的es_batch.py | ES批量路径断链 |
realtime.py指向已迁移的旧节点 | 实时收割路径悬空 |
auto_namer.py备份版参数不匹配 | 实测抛TypeError |
面板jwt_secret、口令、Bot Token全明文随库 | 整条链的凭据体系被完整缴获 |
归因链:攻击者署名 YuUu
用泄露的Bot Token调用Telegram只读接口getMe与getChat(仅查询身份,不读取消息),结果:收割战报Bot为"挖呀挖"(@wyw_yuu_bot),存活告警Bot为"探针"(@tz_yuu_bot),两者的共同接收端chat 7654417798是一个私人账号,first_name为YuUu。
面板口令Yuu@N****2026中的Yuu、两个Bot用户名的_yuu_bot后缀、C2域名uau.pp.ua中的uau,加上Telegram账号的first_name,四处线索彼此独立,指向却一致。
综合判断,该行为体属于个人或小团队运营的僵尸网络。
七、检测与处置建议
受害主机侧排查命令:
ls -la /tmp/exp.so /tmp/na /tmp/nz.yml /etc/nezha/config.ymlgrep -l "INkbR1rkhU4" /root/.ssh/authorized_keyssystemctl status nezha-agent.servicegrep -E "SLAVEOF|MODULE LOAD" /var/log/redis/*.log
基础设施侧,出站封禁面板域jk.uau.pp.ua与agent上报域n.uau.pp.ua。Redis加固:6379-6384加requirepass、rename-command CONFIG、禁公网。
Portainer加固:更换默认agent签名密钥,本案例中的私钥经实测真实可用。
目录里那对SSH私钥属于攻击者自己的OCI主机,用于登录其自有基础设施;注入受害机的是system-audit公钥,对应的私钥并不在目录内。清理时只需要删掉authorized_keys里那把公钥。轮换主机自身的SSH密钥在这里没有意义,因为攻击者手里已经有一把可用的私钥。
本文转载自微信公众号「huasec」,查看原文。版权归原作者所有,如有侵权请联系删除。