CVE-2026-20841:Windows记事本中的任意代码执行漏洞

访问原始链接 Google 翻译

在这份TrendAI研究服务漏洞报告的节选中,TrendAI研究团队的Nikolai Skliarenko和Yazhi Wang详细介绍了Windows记事本应用程序中一个近期已修复的命令注入漏洞。该漏洞最初由Delta Obscura的Cristian Papa和Alasdair Gorniak发现。成功利用此漏洞可能导致在受害者账户的安全上下文中执行任意命令。以下是他们关于CVE-2026-20841报告的一部分,经过少量最小化修改。


微软Windows记事本中报告了一个远程代码执行漏洞。该漏洞是由于对Markdown文件中的链接验证不当造成的。

远程攻击者可以通过诱使受害者下载并交互恶意文件来利用此漏洞。成功利用此漏洞可能导致在受害者账户的安全上下文中执行任意命令。

漏洞详情

微软Windows附带一个名为Windows记事本的默认文本编辑应用程序。历史上,此应用程序仅提供最基本的编辑功能。然而,现代版本的Windows默认包含一个改进和扩展的记事本。这个新版本支持多种文件格式、Markdown渲染以及Copilot增强功能。

Markdown 是一种轻量级标记语言,允许用户使用简单的语法创建格式化文本。它广泛用于编写文档、博客文章和README文件。它支持多种格式化选项,包括(但不限于)标题、样式文本、编号和项目符号列表以及链接。Markdown支持两种主要的链接格式:标准和内联。标准链接格式为:

link-name

渲染时,仅向用户显示链接文本("link-name")。

内联链接使用以下格式:

<link/path>

渲染时,它们被转换为等效的标准链接:

link/path

微软Windows记事本中报告了一个远程代码执行漏洞。该漏洞是由于处理Markdown文件时对链接的验证不当造成的。

当记事本打开一个文件时,如果应用程序检测到该文件需要特殊渲染(在本例中为Markdown),则输入文件会被标记化(tokenized)。此处的标记化意味着将原始文件文本分割成一系列小的、可识别的片段("tokens"),渲染器可以逐个处理它们。检测是基于文件扩展名进行的。只有".md"扩展名被发现会触发Markdown渲染,因为应用程序通过调用 sub_1400ED5D0() 使用固定的字符串比较来确定是否应渲染Markdown。Markdown文件是逐个标记渲染的。

函数 sub_140170F60() 处理Markdown文件中的链接点击。它过滤链接值,并将其传递给 ShellExecuteExW() 调用。

发现对链接执行的过滤不充分,因为它允许使用恶意构造的协议URI,例如"file://"和"ms-appinstaller://",从而在受害者的安全上下文中执行任意文件。ShellExecuteExW() 使用配置的协议处理程序,并且根据系统配置,可能暴露额外的可利用协议。

远程攻击者可以通过诱使受害者下载恶意构造的Markdown文件、打开它并点击恶意链接来利用此漏洞。成功利用此漏洞可能导致在受害者账户的安全上下文中执行任意命令。

注意事项

  • 默认情况下,使用".md"文件扩展名的文件并未注册为由记事本打开。但是,当在记事本中手动打开时,它们会被渲染为Markdown,从而允许触发漏洞。
  • 在传递给 ShellExecuteExW() 调用之前,攻击者控制的链接路径中的任何"\"序列都会被转换为""。

源代码分析

以下代码片段取自Notepad.exe版本11.2508。TrendAI研究人员添加的注释已高亮显示。

在 sub_140170F60() 中:

void __fastcall sub_140170F60(link_handle *Block)
{

   switch ( LOWORD(Block->field_48) )
   {
      case 0xFFFF:
      case 3:
      case 5:
        goto LABEL_56;
      case 2:
        Block->field_50 = 0i64;
        Block->field_58 = Block->field_20;
        LOWORD(Block->field_48) = 4;
        sub_14003D910(&Block->field_50, Block);
        return;
      case 4:
        v31 = Block->field_50;
        v2 = sub_14005C0F0((__int64)Block, (unsigned __int8 *)(*(_QWORD *)(v31 + 40) + 80i64));
        sub_14005C9D0((_QWORD *)(v3 + 16), &Block->field_120, (const void *)(v3 + 80), v2);
        if ( !Block->field_128 )
        {
          std::_Xout_of_range("invalid unordered_map key");
          goto LABEL_56;
        }
        v4 = *(__int64 **)(Block->field_128 + 32);
        v5 = *v4;
        if ( *(_QWORD *)(*v4 + 24) )
        {
          v6 = (const WCHAR *)(v5 + 8);
          if ( *(_QWORD *)(v5 + 32) > 7ui64 )
          v6 = *(const WCHAR **)v6;
        }
        else
        {
          v6 = 0i64;
        }
        _original_string = &Src
        if ( v6 )
          _original_string = v6;
        string_length = -1i64;
        do
          ++string_length;
        while ( _original_string[string_length] );
        *(_OWORD *)&Block->link_dst = 0i64;
        Block->max_length = 0i64;
        Block->length = 0i64;
        strncpy((char **)&Block->link_dst, _original_string, string_length);
        string_base = (int *)&Block->link_dst;
        if ( Block->length > 7ui64 )
          string_base = (int *)Block->link_dst;
        end_of_string = (char *)string_base + 2 * Block->max_length;
        // The only protection against malicious links is implemented here
        // Verify that the string has any data except '\' and '/' symbols
        string_ptr = sub_14002C820(string_base, end_of_string);
        if ( string_ptr == (int *)end_of_string )
          goto continue_to_processing;
        do
        {
          if ( *(_WORD *)string_ptr != '\\' && *(_WORD *)string_ptr != '/' )
            break;
          string_ptr = (int *)((char *)string_ptr + 2);
        }
        while ( string_ptr != (int *)end_of_string );
        if ( string_ptr == (int *)end_of_string )
          goto continue_to_processing;
        break;
      default:
        __debugbreak();
        return;
}
// remove trailing '/' and '\' symbols
while ( 1 )
{
   v12 = (int *)(end_of_string - 2);
   v13 = *((_WORD *)end_of_string - 1);
   if ( v13 == '\\' || v13 == '/' )
     break;
   end_of_string -= 2;
   if ( string_ptr == v12 )
     goto continue_to_processing;
}
if ( string_ptr != (int *)end_of_string )
{
   do
   {
      v14 = (int *)(end_of_string - 2);
      v15 = *((_WORD *)end_of_string - 1);
      if ( v15 != '\\' && v15 != '/' )
        break;
      end_of_string -= 2;
   }
   while ( string_ptr != v14 );
  }
continue_to_processing:
  *(_OWORD *)&Block->field_80 = 0i64;
  Block->field_90 = 0i64;
  Block->field_98 = 0i64;
  // configure the launched process

  // by passing the potentially attacker-controlled data
  strncpy((char **)&Block->field_80, string_base, (end_of_string - (char *)string_base) >> 1);
  *(_OWORD *)&Block->shell_start_struct.fMask = 0i64;
  *(_OWORD *)((char *)&Block->shell_start_struct.lpVerb + 4) = 0i64;
  *(_OWORD *)((char *)&Block->shell_start_struct.lpParameters + 4) = 0i64;
  *(_OWORD *)(&Block->shell_start_struct.nShow + 1) = 0i64;
  *(_OWORD *)((char *)&Block->shell_start_struct.lpIDList + 4) = 0i64;
  *(_OWORD *)((char *)&Block->shell_start_struct.hkeyClass + 4) = 0i64;
  *(HANDLE *)((char *)&Block->shell_start_struct.hMonitor + 4) = 0i64;
  HIDWORD(Block->shell_start_struct.hProcess) = 0;
  Block->shell_start_struct.cbSize = 112;
  Block->shell_start_struct.fMask = 1088;
  Block->shell_start_struct.lpVerb = L"open";
  v16 = (const WCHAR *)&Block->field_28;
  if ( Block->field_40 > 7ui64 )
    v16 = *(const WCHAR **)v16;
  // v16 is the attacker-controlled link value
  // it may use file:// protocol to launch a remote file
  // in the context of the victim
  Block->shell_start_struct.lpFile = v16;
  if ( v6 )
  {
     v17 = (const WCHAR *)&Block->field_80;
     if ( Block->field_98 > 7ui64 )
        v17 = (const WCHAR *)Block->field_80;
  }
  else
  {
     v17 = 0i64;
  }
  Block->shell_start_struct.lpDirectory = v17;
  Block->shell_start_struct.nShow = 1;
  // The vulnerability is triggered here
  if ( ShellExecuteExW(&Block->shell_start_struct) )

[...Truncated for readability...]

检测指南

要检测利用此漏洞的攻击,检测设备必须监控和解析以下可用于传递攻击以利用此漏洞的应用协议流量:

  • FTP,通过端口21/TCP、20/TCP
  • HTTP,通过端口80/TCP
  • HTTPS,通过端口443/TCP
  • IMAP,通过端口143/TCP
  • NFS,通过端口2049/TCP、2049/UDP、111/TCP、111/UDP
  • POP3,通过端口110/TCP
  • SMTP,通过端口25/TCP、587/TCP
  • SMB/CIFS,通过端口139/TCP、445/TCP

检测设备必须检查传输文件扩展名为".md"的Markdown文件的流量。如果发现此类文件传输,检测设备必须在文件内容中搜索链接。

检测设备必须检查链接路径是否包含字符串"file:"或"ms-appinstaller:"。

如果找到"file:",检测设备必须使用以下不区分大小写的正则表达式搜索Markdown文件内容:

(\x3C|[[^\x5d]+]()file:(\x2f|\x5c\x5c){4}

如果找到"ms-appinstaller:",检测设备必须使用以下不区分大小写的正则表达式搜索Markdown文件内容:

(\x3C|[[^\x5d]+]()ms-appinstaller:(\x2f|\x5c\x5c){2}

如果任何正则表达式匹配,则链接包含指向远程资源的路径。该流量必须被视为恶意;很可能正在进行利用此漏洞的攻击。此指南也应能检测到最近在GitHub上发布的公开PoC。

注意事项

  • 字符串匹配不区分大小写。
  • 检测指南基于供应商提供的补丁。然而,该补丁将链接限制为仅本地文件和HTTP(S) URI,这可能导致大量误报。因此,检测指南侧重于可能访问和执行远程文件的格式。由于这个原因,它可能导致漏报。
  • 易受攻击的函数使用配置的协议处理程序,并且根据系统配置,可能暴露额外的可利用协议。

结论

此漏洞已由微软在2026年2月的发布周期中修补。他们指出没有变通方案,但确实将用户交互列为利用的先决条件。要完全修复该漏洞,正确的操作是测试并部署供应商提供的补丁。

特别感谢TrendAI研究团队的Nikolai Skliarenko和Yazhi Wang对此漏洞提供了如此详尽的分析。有关TrendAI研究服务的概述,请访问https://go.trendmicro.com/tis/vulnerabilities.html。

威胁研究团队将在未来带来其他优秀的漏洞分析报告。在此之前,请在Twitter、Mastodon、LinkedIn或Bluesky上关注该团队,以获取最新的漏洞利用技术和安全补丁信息。