谁在线上?利用WindowsTelephonyService服务器模式漏洞:从任意文件写入到RCE

admin 2026-01-26 02:39:18 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文分析了WindowsTelephonyService服务器模式中的漏洞,展示了如何通过操纵mailslot参数实现任意文件写入原语。攻击者可利用该原语修改配置文件提升权限,进而加载恶意DLL实现远程代码执行。该漏洞已被分配CVE-2026-20931并修复,适用于特定服务器配置场景。 综合评分: 95 文章分类: 漏洞分析,漏洞POC,渗透测试,内网渗透


cover_image

谁在线上?利用 Windows Telephony Service 服务器模式漏洞:从任意文件写入到 RCE

Sergey Bliznyuk Sergey Bliznyuk

securitainment

2026年1月23日 23:56 中国香港

| 原文链接 | 作者 | | — | — | | https://swarm.ptsecurity.com/whos-on-the-line-exploiting-rce-in-windows-telephony-service/ | Sergey Bliznyuk |

几十年来,Windows 一直支持 computer telephony integration (计算机电话集成),为应用程序提供了管理电话设备、线路与通话的能力。尽管现代部署越来越依赖基于云的 telephony 解决方案,但经典的 telephony 服务在 Windows 中仍然默认可用,并且依旧在一些专门场景中被使用。因此,这些遗留的 telephony 组件仍然构成了默认 Windows 攻击面的一个组成部分。

本文研究探讨了我在 Telephony Service 的 server mode 中发现的一处漏洞:它允许低权限客户端向 Telephony Service 可访问的文件写入任意数据,并在特定条件下实现远程代码执行。

Windows Telephony 概览

Windows 通过 Tele­pho­ny Ap­pli­ca­tion Pro­gram­ming In­ter­face (TAPI) 对外提供 telephony 功能,使用户态应用可以通过统一的抽象层与 telephony 设备和服务交互。

TAPI 主要有两种形态:TAPI 2.x 提供过程式的 C 风格 API;TAPI 3.x 则基于 COM 实现。虽然两套 API 形式不同,但它们依赖相同的底层架构:应用与 TAPI runtime 通信,由后者把请求转发给 Telephony Service Providers (TSPs)。

TSP 是由厂商提供的组件,用于封装特定设备或服务的逻辑,并与底层的 telephony backend 对接,例如物理 telephony 硬件、PBX 系统或 VoIP endpoint。对客户端应用而言,这些差异都被 TAPI 抽象层屏蔽了。

什么是 Telephony Service

应用与 Windows telephony stack 的交互方式主要有两种:调用 tapi32.dll导出的 TAPI 2.x functions,或使用 tapi3.dll提供的 TAPI 3.x COM interface。在这两种情况下,这些库大多扮演客户端 wrapper 的角色:负责封送 (marshal) 请求,并将其转发给真正实现 telephony 逻辑的系统服务。

这个系统服务就是 Telephony服务 (TapiSrv)。它实现了实际的 TAPI 功能,并通过 tapsrvRPC 接口将能力暴露给客户端应用。当应用调用 TAPI 时,请求最终由 TapiSrv处理:它选择合适的 TSP,并协调相应的底层交互过程。

该服务以 NETWORK SERVICE账号运行,启动类型为手动,但当某个进程首次通过 tapi32.dll或 tapi3.dll发起 TAPI 请求时,它会被按需自动启动。其完整实现位于 tapisrv.dll库中。

(该图来自 MSDN,虽然已经过时,但有助于理解整体结构)

TAPSRV RPC 接口

概述

TAPI client 与 Telephony service 之间通过一个名为 tapsrv的经典 MSRPC 接口通信。其对应协议 MS-TRP 已经 公开文档化。默认情况下,这个接口仅允许本地调用者访问

但在 Windows Server 系统上,TAPI 可以被配置为接受远程客户端连接。该行为由以下注册表值控制:

HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Telephony\Server\DisableSharing

也可以通过 TelephonyMMC 管理单元 (TapiMgmt.msc) 进行配置。

虽然把本地 modem 或 telephony 设备远程共享出来通常用处不大,但该功能确实适用于 PBX 系统或电话交换机等 server-side telephony 部署场景。在这些场景中,telephony 硬件及其配套 TSP 会集中安装在服务器上,多台支持 TAPI 的客户端远程连接,而无需各自维护独立的 TSP 安装。客户端可通过 tcmsetup /c <SERVER NAME>命令配置使用远程 TAPI server。

启用远程访问后,该接口会通过 tapsrvnamed pipe 暴露出来,这意味着客户端必须先通过 SMB 完成认证才能建立连接。在这种配置下,TAPI server 还会向 Active Directory 发布与服务相关的信息,使其在域环境中相对容易被发现。

请求分发模型

tapsrvRPC 接口非常精简,只包含 三个可调用方法:ClientAttachClientDetach和 ClientRequest。前两个分别负责会话的建立与销毁;而所有与 telephony 相关的操作都通过 ClientRequest触发。

ClientRequest接受一个二进制 blob,表示序列化后的请求包。该包的前 4 字节是 Req_Func字段,它作为索引进入内部的 dispatch table;其余部分则是针对所选操作封送后的参数。

MS-TRP 规范基本记录了支持的 Req_Func值及其对应的 packet 布局,并且与 Win32 TAPI 2.x 的 API surface 高度相似。从概念上讲,这相当于在 MSRPC 之上又增加了一层分发逻辑,本质上是“RPC 套 RPC”的设计。类似的模式也出现在其他 Windows 服务中,例如 RasMan服务暴露的 RASRPC 接口 (我几个月前也在那发现过一个 LPE)。

客户端会话建立

在 TAPI 的术语中,_client_ 指连接到 TAPI server 接口的机器;而 line application则是运行在该 client 上、发起 telephony 请求的程序。客户端会话通过调用 ClientAttach建立,其函数签名如下:

longClientAttach(
&nbsp; &nbsp; &nbsp;[out] &nbsp; PCONTEXT_HANDLE_TYPE *pphContext,
&nbsp; &nbsp; &nbsp;[in] &nbsp; &nbsp;long&nbsp; &nbsp; lProcessID,
&nbsp; &nbsp; &nbsp;[out] &nbsp;&nbsp;long&nbsp; &nbsp;*phAsyncEventsEvent,
&nbsp; &nbsp; &nbsp;[in, string] &nbsp; &nbsp;wchar_t&nbsp;*pszDomainUser,
&nbsp; &nbsp; &nbsp;[in, string] &nbsp; &nbsp;wchar_t&nbsp;*pszMachine
&nbsp; &nbsp; );

在会话初始化阶段,服务会评估调用者的安全上下文,并给 client 分配内部的权限标记 (privilege flags)。后续的多种 telephony 操作会基于这些标记来控制对敏感功能的访问。

CheckTokenMembership(hClientToken, pBuiltinAdministratorsSid, &bIsLocalAdmin);

if&nbsp;(bIsLocalAdmin || IsSidLocalSystem(hClientToken)) {
&nbsp; &nbsp; ptClient->dwFlags |=&nbsp;8;
}

if&nbsp;(bIsLocalAdmin || IsSidNetworkService(hClientToken)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; || IsSidLocalService(hClientToken)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; || IsSidLocalSystem(hClientToken)) {
&nbsp; &nbsp; &nbsp;ptClient->dwFlags |=&nbsp;1;
}

if&nbsp;(TapiGlobals.dwFlags & TAPIGLOBALS_SERVER) {
if&nbsp;((ptClient->dwFlags &&nbsp;8) ==&nbsp;0&nbsp;) {
wcscpy&nbsp;((WCHAR *) InfoBuffer, szDomainName);
wcscat&nbsp;((WCHAR *) InfoBuffer,&nbsp;L"\\");
wcscat&nbsp;((WCHAR *) InfoBuffer, szAccountName);
if&nbsp;(GetPrivateProfileIntW(
"TapiAdministrators",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (LPCWSTR) InfoBuffer,
0,&nbsp;"..\\TAPI\\tsec.ini"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ) ==&nbsp;1) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ptClient->dwFlags |=&nbsp;9;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
}

从这段逻辑可以看出:标记值 8对应管理权限 (本地管理员或 SYSTEM);标记值 1则分配给服务账号。当启用 TAPI server mode 时,C:\Windows\TAPI\tsec.ini中 [TapiAdministrators]段落明确列出的用户也会被授予提升后的权限。

随后,为了调用与 line抽象相关的方法,client 需要通过发送 Initialize 请求来初始化 line application实例。

异步事件处理

Telephony 天生是事件驱动的:来电、状态变化以及媒体事件可能独立于 client 请求而发生。由于 MSRPC 采用同步的 request-response 模型,MS-TRP 协议实现了自己的机制,用于将 Telephony service 的异步事件投递给已连接的 client。

事件投递模型在初次 ClientAttach调用时协商确定,并会根据 client 是本地还是远程而有所不同。

对于本地 client,异步事件通过共享的同步对象投递。client 在 ClientAttach中提供进程标识 (lProcessID),并获得一个事件对象的 handle。当事件数据可用时,Telephony service 会 signal 该事件,提示 client 通过发送 GetAsyncEvents请求来取回待处理数据。

启用 TAPI server mode 后,协议提供两种替代机制来投递异步事件:_push_ 与 _pull_。具体采用哪种模型取决于 ClientAttach的入参。

在 push模式下,client 将 pszDomainUser置空,并在 pszMachine参数中提供用引号分隔的 RPC string binding (例如 CLIENT-PC-NAME"ncacn_ip_tcp"31337")。Telephony service 会向该 endpoint 建立反向 RPC 连接,绑定 remotesp 接口,并在异步事件发生时调用 RemoteSPEventProc方法。

在 pull模式下,client 在会话初始化时通过 pszDomainUser参数指定一个 mailslot 名称。Telephony service 会周期性地向该 mailslot 发送 DWORD大小的数据报,以提示有事件可供取回;client 随后通过 GetAsyncEvents拉取对应的事件数据。

无论采用哪种模型,server 都通过 client 在 Initializepacket 中提供的 InitContext字段值,把事件与某个特定的 line application 关联起来。该值被视为一个不透明的 4 字节标识符,并会作为该应用事件通知的一部分由 server 回显给 client。

Mailslot 小把戏

Mailslot 是一种遗留的 Windows IPC 机制,用于传输小型的单向消息。mailslot 写入端向一个命名 endpoint 发送数据报,而接收端被动读取收到的消息。从 client 侧来看,mailslot 可通过标准 Win32 文件 API (例如 CreateFileWriteFileCloseHandle) 进行访问。

mailslot 使用如下特殊路径语法来寻址:

\\<COMPUTERNAME>\MAILSLOT\<MailslotName>

对 client 而言,得到的 handle 行为类似一个只能写入的文件。跨网络时,mailslot 消息通过 NetBIOS-over-UDP 数据报传输 (或者说曾经如此:自 Windows 11 24H2 起远程 mailslot 已被禁用)。由于通信严格单向,发送方不会收到远程 mailslot 是否存在、消息是否被处理的任何确认。

如前一节所述,Telephony service 在 pull异步事件模型中会周期性地向 client 提供的 mailslot 名称发送数据报,从而通知远程 client 存在待处理事件。ClientAttach中负责初始化 mailslot handle 的关键代码路径如下:

if&nbsp;(wcslen (pszDomainUser) >&nbsp;0)
&nbsp; &nbsp; &nbsp; &nbsp; {
if&nbsp;((ptClient->hMailslot =&nbsp;CreateFileW(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pszDomainUser,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; GENERIC_WRITE,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; FILE_SHARE_READ,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (LPSECURITY_ATTRIBUTES)&nbsp;NULL,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; OPEN_EXISTING,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; FILE_ATTRIBUTE_NORMAL,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (HANDLE)&nbsp;NULL
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; )) != INVALID_HANDLE_VALUE)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
goto&nbsp;ClientAttach_AddClientToList;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ...
&nbsp; &nbsp; &nbsp; &nbsp; }

关键点在于:服务直接将用户可控的 pszDomainUser字符串传给 CreateFileW,却没有校验它是否指向 mailslot 路径——既没有检查路径是否以 \\*\MAILSLOT\命名空间开头,也没有验证它是否对应一个 mailslot 对象。

因此,client 可以把 pszDomainUser伪装成任意文件路径,而不是 mailslot 名称。只要目标文件已存在且对 NETWORK SERVICE账号可写,Telephony service 就能成功打开它,并在后续将异步事件数据写入该文件。换言之,基于 mailslot 的事件投递机制可以被“改造”为一个在服务安全上下文下执行的任意文件写入原语。

构造文件写入原语

到这里为止,攻击者已经控制了 Telephony service 把数据写到 哪里_;剩下的问题是写入的内容_ 是什么。

如前所述,在 pull异步事件模型中,Telephony service 通过向 client 指定的 mailslot 写入单个 DWORD值来发送通知。这个值实际上对应于产生该事件的 line application 在初始化时提供的 InitContext字段。

由于 InitContext完全由用户控制,并且 mailslot 路径本身又可以被重定向到任意文件,所以每次生成事件都会导致一次对指定文件的受控 4 字节写入。剩下的挑战是如何可靠地按需触发此类事件。

追踪异步事件入队的代码路径可以发现,许多路径深埋在 telephony 的通话处理逻辑之中。与其直接尝试触达这些路径,一个更简单且更可靠的办法是通过 NotifyHighestPriorityRequestRecipient来触发事件。

该辅助函数会把事件投递给全局唯一的“最高优先级”(highest-priority) line application。关键在于,它可以通过未公开的 TRequestMakeCallpacket (Req_Func = 121) 被远程触发;而这个 packet 正是公开 API tapiRequestMakeCall 的后端实现。

当 client 通过未公开的 LRegisterRequestRecipienthandler (Req_Func = 61) 注册或取消注册为 request recipient 时,最高优先级 line application 会被重新计算;该 handler 对应公开 API lineRegisterRequestRecipient 的后端实现。

相关逻辑如下:

if&nbsp;(dwRequestMode & LINEREQUESTMODE_MAKECALL)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
if&nbsp;(!ptLineApp->pRequestRecipient)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
// Add to request recipient list

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; PTREQUESTRECIPIENT &nbsp;pRequestRecipient;

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pRequestRecipient->ptLineApp = ptLineApp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pRequestRecipient->dwRegistrationInstance =
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pParams->dwRegistrationInstance;

EnterCriticalSection&nbsp;(&gPriorityListCritSec);

if&nbsp;((pRequestRecipient->pNext =
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; TapiGlobals.pRequestRecipients))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pRequestRecipient->pNext->pPrev = pRequestRecipient;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; TapiGlobals.pRequestRecipients&nbsp;= pRequestRecipient;

LeaveCriticalSection&nbsp;(&gPriorityListCritSec);

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ptLineApp->pRequestRecipient = pRequestRecipient;

// Recalculate global highest-priority client

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; TapiGlobals.pHighestPriorityRequestRecipient&nbsp;=&nbsp;GetHighestPriorityRequestRecipient();

if&nbsp;(TapiGlobals.pRequestMakeCallList)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
NotifyHighestPriorityRequestRecipient();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ...
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

优先级依据“应用模块名”在某个列表中的出现顺序来确定:

PTREQUESTRECIPIENT&nbsp;GetHighestPriorityRequestRecipient()
{
&nbsp; &nbsp; BOOL &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bFoundRecipientInPriorityList =&nbsp;FALSE;
&nbsp; &nbsp; WCHAR &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; *pszAppInPriorityList,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; *pszAppInPriorityListPrev = (WCHAR *)&nbsp;LongToPtr(0xffffffff);
&nbsp; &nbsp; PTREQUESTRECIPIENT pRequestRecipient,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pHighestPriorityRequestRecipient =&nbsp;NULL;
&nbsp; &nbsp; WCHAR *pszPriorityList =&nbsp;NULL;

EnterCriticalSection&nbsp;(&gPriorityListCritSec);

&nbsp; &nbsp; pRequestRecipient = TapiGlobals.pRequestRecipients;

if&nbsp;(RpcImpersonateClient(0) ==&nbsp;0)
&nbsp; &nbsp; {
// Fetch the priority list for current user
GetPriorityListTReqCall(&pszPriorityList);
&nbsp; &nbsp; }

while&nbsp;(pRequestRecipient)
&nbsp; &nbsp; {
// Calculate the index of app's module name in priority list
if&nbsp;(pszPriorityList &&

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (pszAppInPriorityList =&nbsp;wcsstr(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pszPriorityList,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pRequestRecipient->ptLineApp->pszModuleName
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; )))
&nbsp; &nbsp; &nbsp; &nbsp; {
if&nbsp;(pszAppInPriorityList <= pszAppInPriorityListPrev)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pHighestPriorityRequestRecipient = pRequestRecipient;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pszAppInPriorityListPrev = pszAppInPriorityList;

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bFoundRecipientInPriorityList =&nbsp;TRUE;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }
elseif&nbsp;(!bFoundRecipientInPriorityList)
&nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pHighestPriorityRequestRecipient = pRequestRecipient;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; pRequestRecipient = pRequestRecipient->pNext;
&nbsp; &nbsp; }

LeaveCriticalSection&nbsp;(&gPriorityListCritSec);

return&nbsp;pHighestPriorityRequestRecipient;
}

该列表在模拟 (impersonate) client 的情况下从注册表中读取:

RPC_STATUS&nbsp;GetPriorityListTReqCall(WCHAR **ppszPriorityList)
{
&nbsp; &nbsp; HKEY hKey =&nbsp;NULL;
&nbsp; &nbsp; HKEY phkResult =&nbsp;NULL;
EnterCriticalSection(&gPriorityListCritSec);
if&nbsp;( !RegOpenCurrentUser(0xF003F, &phkResult) )
&nbsp; &nbsp; {
if&nbsp;( !RegOpenKeyExW(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; phkResult,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Telephony\\HandoffPriorities",
0,
0x20019,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &hKey) )
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
// Load the value from the specified registry key
GetPriorityList(hKey,&nbsp;L"RequestMakeCall", ppszPriorityList);
RegCloseKey(hKey);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
RegCloseKey(phkResult);
&nbsp; &nbsp; }
LeaveCriticalSection(&gPriorityListCritSec);
returnRpcRevertToSelf();
}

具体来说,服务会读取 client 的 HKCUhive 下的以下键值:

HKCU\Software\Microsoft\Windows\CurrentVersion\Telephony\HandoffPriorities\RequestMakeCall

默认情况下,这个列表通常只包含一个条目:DIALER.EXE。如果需要,还可以通过未公开的 LSetAppPriority请求 (Req_Func = 69) 插入额外条目。

用于优先级比较的 pszModuleName字段由 client 在 Initializepacket 中提供,这让攻击者能够完全控制自己的 line application 如何被排序。

有了以上拼图,就可以在 NETWORK SERVICE安全上下文下构造一个可靠的任意 DWORD写入原语。

首先,攻击者通过调用 ClientAttach建立 client 会话,并在 pszDomainUser参数中指定目标文件路径。这会使 Telephony service 打开该文件一次,并保留得到的 handle,以供后续事件通知写入使用。

对于每一个需要写入的 4 字节值,攻击者按如下步骤操作:

  1. 提交 Initializepacket (Req_Func = 47),并设置:
  • InitContext

    为期望写入的 DWORD

  • pszModuleName

    为 DIALER.EXE(或其他高优先级条目)

  1. 使用 LRegisterRequestRecipient将该 line application 注册为 request recipient

    (Req_Func = 61dwRequestMode = LINEREQUESTMODE_MAKECALLbEnable = 1)。

  2. 提交 TRequestMakeCallpacket (Req_Func = 121) 触发事件。

  3. 使用 GetAsyncEvents(Req_Func = 0) 出队事件,从而完成写入。

  4. 取消注册 request recipient (LRegisterRequestRecipientbEnable = 0)。

  5. 使用 Shutdown(Req_Func = 86) 关闭 line application。

重复执行上述序列,攻击者就能向任意一个“已存在且对 Telephony service 可写”的文件写入任意数据。

从文件写入到 RCE

在这个阶段,要实现利用需要一个 NETWORK SERVICE可写的既有文件。一个非常显眼的候选项就是前面提到的 C:\Windows\TAPI\tsec.ini。在以 server mode 运行 Telephony service 的系统上,该文件一定存在,并且对服务账号可写。

该文件在其他配置项之外,还定义了哪些用户会被 Telephony service 视为管理员。通过在 [TapiAdministrators]下添加条目 (例如 "[TapiAdministrators]\r\nDOMAIN\\attacker=1"),远程的低权限域用户就可以为自己在 Telephony service 内授予管理员权限。完成修改后再通过 ClientAttach建立新会话,就会得到一个带有管理权限标记的 client context。

一旦拥有 Telephony service 的管理权限,就可以访问更多的攻击面。其中一个非常强力的原语来自 MS-TRP 协议文档中的 GetUIDllName请求。

根据规范:

GetUIDllName packet 与 TUISPIDLLCallback packet、FreeDialogInstance packet 一起,用于在服务器上安装、配置或移除 TSP。

查看实现可以发现:非管理调用者只能从注册表中预定义的列表里选择 provider;而管理 client 则被允许从任意路径加载 provider DLL。

switch&nbsp;(pParams->dwObjectType)
&nbsp; &nbsp; &nbsp;{
case&nbsp;TUISPIDLL_OBJECT_LINEID:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;...
case&nbsp;TUISPIDLL_OBJECT_PHONEID:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;...
case&nbsp;TUISPIDLL_OBJECT_PROVIDERID:
// If the client is not admin and is requesting to
// remove a provider or to install one from the path
// supplied in request (rather than by index in registry),
// return an error
if&nbsp;((ptClient->dwFlags &&nbsp;8) ==&nbsp;0&nbsp;&& (pParams->bRemoveProvider || pParams->dwProviderFilenameOffset != TAPI_NO_DATA)) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pParams->lResult = LINEERR_OPERATIONFAILED;
return;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}

if&nbsp;(pParams->dwProviderFilenameOffset != TAPI_NO_DATA) {
// The path is supplied in request
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;TCHAR &nbsp; *pszProviderFilename = pDataBuf + pParams->dwProviderFilenameOffset;
if&nbsp;(ptDlgInst->hTsp =&nbsp;LoadLibrary(pszProviderFilename)) {
if&nbsp;(pfnTSPI_providerUIIdentify = (TSPIPROC)&nbsp;GetProcAddress(ptDlgInst->hTsp,"TSPI_providerUIIdentify")) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;pParams->lResult =&nbsp;pfnTSPI_providerUIIdentify(pszProviderFilename);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;...
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;...
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;....
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; &nbsp;}

通过提交一个 GetUIDllName请求,将 dwObjectType设为 TUISPIDLL_OBJECT_PROVIDERID,并指定攻击者可控的 DLL 路径,我们就能让 Telephony service 加载该 DLL 并调用其导出的 TSPI_providerUIIdentify函数。这在服务上下文中提供了一个直接且可靠的代码执行原语。更进一步,如果该导出函数返回非零值,服务会在调用后卸载该 DLL,从而允许后续将 payload 从磁盘移除。

一个显而易见的投递方式是指定指向攻击者控制的 SMB 共享的 UNC 路径。实践中,当该共享托管在同一域内的标准 Windows 机器上时,这通常能稳定工作。然而,攻击者自建的 SMB server (例如 impacket-smbserver或 Samba) 可能触发 guest access 限制,导致 LoadLibrary以 ERROR_SMB_GUEST_LOGON_BLOCKED失败。

由于已经拥有任意文件写入原语,一个可靠的替代方案是在本地落地 DLL。

可以使用 accesschk来识别合适的可写文件。例如,下列文件在几乎所有系统上都倾向于存在:

  • C:\Windows\System32\catroot2\dberr.txt
  • C:\Windows\ServiceProfiles\NetworkService\AppData\Local\Temp\MpCmdRun.log
  • C:\Windows\ServiceProfiles\NetworkService\AppData\Local\Temp\MpSigStub.log

尽管通过 4 字节的事件写入来拼接一个“payload 大小”的 DLL 相对较慢,但这能彻底消除对外部基础设施的依赖。

为了演示代码执行,可以构造一个最小的 proof-of-concept TSP DLL。在下例中,Telephony service 在安装 provider 时会调用导出函数 TSPI_providerUIIdentify;该函数执行一条命令并将结果写入磁盘:

#include<Windows.h>

extern"C"&nbsp;__declspec(dllexport)
LONG __stdcall&nbsp;TSPI_providerUIIdentify(LPWSTR lpszUIDLLName)
{
wchar_t&nbsp;cmd[] =&nbsp;L"cmd.exe /c whoami /all > C:\\Windows\\Temp\\poc.txt";
&nbsp; &nbsp; STARTUPINFO si;
&nbsp; &nbsp; PROCESS_INFORMATION pi;
ZeroMemory(&si,&nbsp;sizeof(si));
&nbsp; &nbsp; si.cb&nbsp;=&nbsp;sizeof(si);
ZeroMemory(&pi,&nbsp;sizeof(pi));

if&nbsp;(CreateProcessW(NULL, cmd,&nbsp;NULL,&nbsp;NULL,&nbsp;FALSE, CREATE_NO_WINDOW | NORMAL_PRIORITY_CLASS,&nbsp;NULL,&nbsp;NULL, &si, &pi))
&nbsp; &nbsp; {
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
&nbsp; &nbsp; }
return0x1337;
}

TSPI_providerUIIdentify的返回值会被回传给 RPC client,从而提供一个清晰的信号来表明 payload 已被执行:

披露与补丁时间线

  • Nov 6, 2025

    – 向 Microsoft 报告漏洞。

  • Dec 22, 2025

    – Microsoft 确认该问题属于安全漏洞。

  • Dec 23, 2025

    – 在 Microsoft Bug Bounty Program 下获得 $5,000 奖励。

  • Dec 29, 2025

    – 分配 CVE-2026-20931。

  • Jan 13, 2026

    – 作为 2026 年 1 月 Patch Tuesday 更新的一部分发布修复。

  • Jan 19, 2026

    – 发布本文。

该漏洞遵循 coordinated vulnerability disclosure 实践进行披露。Microsoft 的公告可在 2026 年 1 月 Security Update Guide 中的 CVE-2026-20931 条目下找到。

结论

本研究表明,即便是很少被使用的 Windows 遗留子系统,也仍可能暴露出复杂且强力的攻击面。对 TAPI 的探索比我预期的有趣得多——这也提醒我们,一些最有收获的研究往往藏在平台中那些容易被忽略的角落里。

最后值得强调的是:本文描述的漏洞 仅影响将 TAPI 配置为 server mode 的系统。这是一种面向集中式 telephony 基础设施的相对少见配置,因此在现实环境中的暴露面会显著受限。


Who’s on the Line? Exploiting RCE in Windows Telephony Service

免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:securitainment Sergey Bliznyuk Sergey Bliznyuk《谁在线上?利用 Windows Telephony Service 服务器模式漏洞:从任意文件写入到 RCE》

评论:0   参与:  0