文章总结: 文档分析了Windows电话服务CVE-2026-20931漏洞,指出在服务器模式下,服务端未校验客户端提供的邮件槽路径即执行文件写入,导致任意文件写入原语。攻击者利用未公开RPC接口触发事件,可控写入数据内容,最终在NETWORKSERVICE权限下实现远程代码执行。该漏洞源于异步事件处理逻辑的校验缺失。 综合评分: 88 文章分类: 漏洞分析,实战经验,漏洞POC
谁在电话线上?利用 Windows 电话服务中的远程代码执行漏洞( CVE-2026-20931)
原创
PocketSec PocketSec
PocketSec
2026年3月3日 08:03 四川
几十年来,Windows 一直支持计算机电话集成,使应用程序能够管理电话设备、线路和通话。虽然现代部署越来越依赖于基于云的电话解决方案,但传统的电话服务仍然在 Windows 中开箱即用,并在一些特定环境中继续使用。因此,传统的电话组件仍然是 Windows 默认攻击面的一部分。
这项研究探索了我在电话服务服务器模式下发现的一个漏洞,该漏洞允许低权限客户端向服务可访问的文件写入任意数据,并且在某些情况下,可以实现远程代码执行。
Windows 电话概述
Windows 通过公开电话功能,该接口允许用户模式应用程序通过统一的抽象层与电话设备和服务进行交互。
TAPI 主要有两种形式:TAPI 2.x,提供过程式的 C 风格 API;以及 TAPI 3.x,使用 COM 实现。虽然 API 有所不同,但它们都依赖于相同的底层架构:应用程序与 TAPI 运行时通信,后者将请求转发给电话服务提供商 (TSP)。
TSP(电话服务处理器)是由供应商提供的组件,它封装了设备或服务特定的逻辑,并与底层电话后端(例如物理电话硬件、PBX 系统或 VoIP 终端)进行接口。从客户端应用程序的角度来看,这些差异隐藏在 TAPI 抽象层之后。
什么是电话服务
应用程序可以通过调用tapi32.dll导出的或使用tapi3.dll提供的COM 接口与 Windows 电话协议栈进行交互。在这两种情况下,这些库主要充当客户端包装器:它们对请求进行序列化,并将其转发给实际实现电话逻辑的系统服务。
该服务即电话服务 (TapiSrv)。它实现了实际的 TAPI 功能,并通过tapsrvRPC 接口将其暴露给客户端应用程序。当应用程序调用 TAPI 时,请求最终由TapiSrv处理,TapiSrv 会选择合适的 TSP 并协调相应的底层交互。
该服务以NETWORK SERVICE帐户运行,并配置为手动启动类型,但当进程首次通过tapi32.dll或tapi3.dll调用 TAPI 请求时,会按需自动启动。整个实现位于tapisrv.dll库中。
(MSDN 上的图表已过时,但可以提供大致的理解)
TAPSRV RPC 接口
Overview 概述
TAPI 客户端与电话服务之间的通信通过名为tapsrv的经典 MSRPC 接口进行。相应的协议 MS-TRP 已。默认情况下,此接口仅限本地调用方使用。
然而,在 Windows Server 系统上,TAPI可以配置为接受远程客户端连接。此行为由以下因素控制:
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Telephony\Server\DisableSharing
registry value and can also be managed through theTelephonyMMC snap-in (TapiMgmt.msc).注册表值也可以通过电话MMC 管理单元 (TapiMgmt.msc) 进行管理。
虽然远程访问本地调制解调器或电话设备很少用到,但此功能适用于服务器端电话部署,例如 PBX 系统或电话交换机。在这种情况下,电话硬件和相关的 TSP 集中安装在服务器上,多个支持 TAPI 的客户端通过远程连接,而无需维护单独的 TSP 安装。客户端可以通过tcmsetup /c命令配置为使用远程 TAPI 服务器。
启用远程访问后,接口会通过tapsrv命名管道公开,这意味着客户端必须先通过 SMB 进行身份验证才能建立连接。在此配置中,TAPI 服务器还会将服务相关信息发布到 Active Directory,使其在域环境中更容易被发现。
Request Dispatch Model 请求分发模型
tapsrvRPC 接口非常简洁,:
ClientAttach、ClientDetach和ClientRequest。前两个调用处理会话初始化和拆除,而ClientRequest用于调用所有与电话相关的操作。
ClientRequest接受一个表示序列化请求包的二进制数据块。该数据包的前四个字节包含一个Req_Func字段,它充当内部调度表的索引。缓冲区的其余部分包含特定于所选操作的序列化参数。
支持的Req_Func值及其对应的包布局主要记录在 MS-TRP 规范中,并且与 Win32 TAPI 2.x API 接口非常相似。从概念上讲,这相当于在 MSRPC 之上增加了一个额外的分发层——实际上是一种“RPC 嵌套 RPC”的设计。类似的模式也出现在其他 Windows 服务中,例如RasMan服务公开的 RASRPC 接口(几个月前我也在那里发现了一个)。
Client Session Setup 客户端会话设置
在 TAPI 术语中,客户端是指连接到 TAPI 服务器接口的计算机,而线路应用程序是指客户端系统上发出电话请求的程序。客户端会话通过调用ClientAttach函数建立,该函数具有以下签名:
long ClientAttach( [out] PCONTEXT_HANDLE_TYPE *pphContext, [in] long lProcessID, [out] long *phAsyncEventsEvent, [in, string] wchar_t *pszDomainUser, [in, string] wchar_t *pszMachine );
在会话初始化期间,该服务会评估调用者的安全上下文,并为客户端分配内部权限标志。这些标志随后会被各种电话操作查询,以控制对敏感功能的访问。
CheckTokenMembership(hClientToken, pBuiltinAdministratorsSid, &bIsLocalAdmin);if (bIsLocalAdmin || IsSidLocalSystem(hClientToken)) { ptClient->dwFlags |= 8;}if (bIsLocalAdmin || IsSidNetworkService(hClientToken) || IsSidLocalService(hClientToken) || IsSidLocalSystem(hClientToken)) { ptClient->dwFlags |= 1;}if (TapiGlobals.dwFlags & TAPIGLOBALS_SERVER) { if ((ptClient->dwFlags & 8) == 0 ) { wcscpy ((WCHAR *) InfoBuffer, szDomainName); wcscat ((WCHAR *) InfoBuffer, L"\\"); wcscat ((WCHAR *) InfoBuffer, szAccountName); if (GetPrivateProfileIntW( "TapiAdministrators", (LPCWSTR) InfoBuffer, 0, "..\\TAPI\\tsec.ini" ) == 1) { ptClient->dwFlags |= 9; } }}
基于此逻辑,标志值8对应于管理员权限(本地管理员或 SYSTEM),而标志值1则分配给服务帐户。启用 TAPI 服务器模式后,C:\Windows\TAPI\tsec.ini文件中[TapiAdministrators]部分明确列出的用户也将被授予提升的权限。
异步事件处理
电话通信本质上是事件驱动的:来电、状态变更和媒体事件可能独立于客户端请求而发生。由于 MSRPC 遵循同步请求-响应模型,MS-TRP 协议实现了自身的机制,用于将异步事件从电话服务传递给已连接的客户端。
事件传递模型在初始ClientAttach调用期间协商确定,并且会根据客户端是本地客户端还是远程客户端而有所不同。
对于本地客户端,异步事件通过共享的同步对象进行传递。客户端在ClientAttach期间提供其进程标识符 (lProcessID),并接收一个事件对象句柄。当事件数据可用时,电话服务会发出此事件信号,提示客户端通过发出GetAsyncEvents请求来检索待处理的数据。
启用 TAPI 服务器模式后,该协议提供了两种异步事件传递机制:推送和拉取。具体采用哪种机制取决于传递给ClientAttach参数。
在推送模型中,客户端将pszDomainUser参数留空,并在pszMachine参数中提供以引号分隔的 RPC 字符串绑定(例如CLIENT-PC-NAME”ncacn_ip_tcp”31337″)。电话服务会建立到端点的反向 RPC 连接,绑定到接口,并在异步事件发生时调用RemoteSPEventProc方法。
在拉取模型中,客户端在会话初始化期间通过pszDomainUser参数指定一个名称。电话服务会定期向该邮件槽发送DWORD大小的数据报,表明有事件可供检索。然后,客户端需要使用GetAsyncEvents来获取相应的事件数据。
在所有情况下,服务器都会使用客户端在Initialize数据包中提供的InitContext字段值,将事件与特定的应用程序关联起来。该值被视为一个不透明的 4 字节标识符,并由服务器作为应用程序事件通知的一部分回显。
邮箱投递口恶作剧
邮件槽是 Windows 系统中一种传统的进程间通信 (IPC) 机制,用于传输小型单向消息。邮件槽写入器向指定的端点发送数据报,而接收器则被动地读取传入的消息。客户端可以使用标准的 Win32 文件 API(例如CreateFile、WriteFile和CloseHandle来访问邮件槽。
邮件槽的寻址使用特殊的路径语法,格式如下:
\\<COMPUTERNAME> 邮箱插槽<MailslotName>
从客户端的角度来看,生成的句柄表现得像一个只写文件。邮件槽消息通过网络使用 NetBIOS over UDP 数据报传输(或者曾经是这样传输的——自起,远程邮件槽功能已被禁用)。由于通信是单向的,发送方无法收到远程邮件槽是否存在或消息正在被处理的确认信息。
如前一节所述,电话服务使用拉取异步事件模型ClientAttach通过定期向客户端提供的邮件槽名称发送数据报来通知远程客户端待处理的事件。ClientAttach 中负责初始化邮件槽句柄的相关代码路径如下所示:
if (wcslen (pszDomainUser) > 0) { if ((ptClient->hMailslot = CreateFileW( pszDomainUser, GENERIC_WRITE, FILE_SHARE_READ, (LPSECURITY_ATTRIBUTES) NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, (HANDLE) NULL )) != INVALID_HANDLE_VALUE) { goto ClientAttach_AddClientToList; } ... }
关键在于,该服务直接将用户控制的pszDomainUser字符串传递给CreateFileW,而没有验证它是否引用邮件槽路径——没有执行任何检查来确保路径以\*\MAILSLOT\命名空间开头,或者以其他方式对应于邮件槽对象。
因此,客户端可以提供任意文件路径,而无需指定邮件槽名称。只要目标文件已存在且NETWORK SERVICE帐户可写,电话服务就能成功打开该文件,并随后向其中写入异步事件数据。换句话说,基于邮件槽的事件传递机制可以在服务的安全上下文中被重新用作任意文件写入原语。
构建文件写入原语
此时,攻击者控制了电话服务的数据写入位置。剩下的问题是,写入的是什么数据。
如前所述,在拉取异步事件模型中,电话服务通过向客户端指定的邮件槽写入单个DWORD值来发送通知。该值实际上对应于在初始化生成该事件的线路应用程序期间提供的InitContext字段。
由于InitContext完全由用户控制,并且邮件槽路径本身可以重定向到任意文件,因此每个生成的事件都会导致向选定的文件写入一个受控的 4 字节数据。剩下的挑战是如何可靠地按需触发此类事件。
追踪异步事件入队的代码路径表明,许多路径都深深嵌入在电话呼叫处理逻辑中。与其尝试直接访问这些路径,不如采用更简单可靠的方法,即通过NotifyHighestPriorityRequestRecipient来触发事件。
此辅助函数会将事件传递给单个全局“最高优先级”线路应用程序。关键在于,它可以通过未公开的TRequestMakeCall数据包(Req_Func = 121)远程调用,该数据包是已公开的API 的后端实现。
当客户端通过未公开的LRegisterRequestRecipient处理程序(Req_Func = 61)注册或取消注册为请求接收者时,将重新计算优先级最高的线路应用程序,该处理程序支持API。
相关逻辑如下所示:
if (dwRequestMode & LINEREQUESTMODE_MAKECALL) { if (!ptLineApp->pRequestRecipient) { // Add to request recipient list PTREQUESTRECIPIENT pRequestRecipient; pRequestRecipient->ptLineApp = ptLineApp; pRequestRecipient->dwRegistrationInstance = pParams->dwRegistrationInstance; EnterCriticalSection (&gPriorityListCritSec); if ((pRequestRecipient->pNext = TapiGlobals.pRequestRecipients)) { pRequestRecipient->pNext->pPrev = pRequestRecipient; } TapiGlobals.pRequestRecipients = pRequestRecipient; LeaveCriticalSection (&gPriorityListCritSec); ptLineApp->pRequestRecipient = pRequestRecipient; // Recalculate global highest-priority client TapiGlobals.pHighestPriorityRequestRecipient = GetHighestPriorityRequestRecipient(); if (TapiGlobals.pRequestMakeCallList) { NotifyHighestPriorityRequestRecipient(); } } ... }
优先级是根据应用程序模块名称在列表中的顺序确定的:
PTREQUESTRECIPIENT GetHighestPriorityRequestRecipient(){ BOOL bFoundRecipientInPriorityList = FALSE; WCHAR *pszAppInPriorityList, *pszAppInPriorityListPrev = (WCHAR *) LongToPtr(0xffffffff); PTREQUESTRECIPIENT pRequestRecipient, pHighestPriorityRequestRecipient = NULL; WCHAR *pszPriorityList = NULL; EnterCriticalSection (&gPriorityListCritSec); pRequestRecipient = TapiGlobals.pRequestRecipients; if (RpcImpersonateClient(0) == 0) { // Fetch the priority list for current user GetPriorityListTReqCall(&pszPriorityList); } while (pRequestRecipient) { // Calculate the index of app's module name in priority list if (pszPriorityList && (pszAppInPriorityList = wcsstr( pszPriorityList, pRequestRecipient->ptLineApp->pszModuleName ))) { if (pszAppInPriorityList <= pszAppInPriorityListPrev) { pHighestPriorityRequestRecipient = pRequestRecipient; pszAppInPriorityListPrev = pszAppInPriorityList; bFoundRecipientInPriorityList = TRUE; } } else if (!bFoundRecipientInPriorityList) { pHighestPriorityRequestRecipient = pRequestRecipient; } pRequestRecipient = pRequestRecipient->pNext; } LeaveCriticalSection (&gPriorityListCritSec); return pHighestPriorityRequestRecipient;}
该列表是在模拟客户端操作时从注册表中检索的:
RPC_STATUS GetPriorityListTReqCall(WCHAR **ppszPriorityList){ HKEY hKey = NULL; HKEY phkResult = NULL; EnterCriticalSection(&gPriorityListCritSec); if ( !RegOpenCurrentUser(0xF003F, &phkResult) ) { if ( !RegOpenKeyExW( phkResult, L"Software\\Microsoft\\Windows\\CurrentVersion\\Telephony\\HandoffPriorities", 0, 0x20019, &hKey) ) { // Load the value from the specified registry key GetPriorityList(hKey, L"RequestMakeCall", ppszPriorityList); RegCloseKey(hKey); } RegCloseKey(phkResult); } LeaveCriticalSection(&gPriorityListCritSec); return RpcRevertToSelf();}
具体来说,该服务读取客户端HKCUhive 下的以下密钥:
HKCU\Software\Microsoft\Windows\CurrentVersion\Telephony\HandoffPriorities\RequestMakeCall
默认情况下,此列表通常包含一个条目:DIALER.EXE。如有必要,可以使用未公开的LSetAppPriority请求(Req_Func = 69)插入其他条目
用于优先级比较的pszModuleName字段由客户端在Initialize数据包中提供,这使得攻击者可以完全控制其线路应用程序的排名。
有了这些组件,就可以在NETWORK SERVICE安全上下文中构建可靠的任意DWORD写入原语。
首先,攻击者通过调用ClientAttach建立客户端会话,并在pszDomainUser参数中指定目标文件路径。这会导致电话服务打开该文件一次,并保留该文件句柄以用于后续事件通知。
对于要写入的每个 4 字节值,攻击者随后执行以下步骤:
提交一个Initialize数据包(Req_Func = 47),设置如下:
使用LRegisterRequestRecipient将线路应用程序注册为请求接收者
(Req_Func = 61,dwRequestMode = LINEREQUESTMODE_MAKECALL,bEnable = 1).
通过提交TRequestMakeCall数据包(Req_Func = 121)触发事件。
使用GetAsyncEvents(Req_Func = 0) 将事件从队列中取出,完成写入操作。
取消注册请求接收者(LRegisterRequestRecipient,bEnable = 0)。
使用Shutdown(Req_Func = 86) 关闭线路应用程序。
重复此序列,攻击者可以将任意数据写入任意预先存在的、电话服务可写入的文件。
从文件写入 RCE
现阶段,利用此漏洞需要一个可由NETWORK SERVICE写入的文件。一个特别明显的候选文件是前面提到的C:\Windows\TAPI\tsec.ini。在以服务器模式运行电话服务的系统中,此文件始终存在且可由服务帐户写入。
该文件与其他配置设置一起定义了电话服务将哪些用户视为管理员。通过在[TapiAdministrators]下添加条目(例如”[TapiAdministrators]\r\nDOMAIN\attacker=1″),远程的非特权域用户可以授予自己电话服务的管理权限。修改后通过ClientAttach建立新会话,将创建一个带有管理员权限标志的客户端上下文。
获得电话服务的管理权限后,攻击面会进一步扩大。其中一个特别强大的攻击手段是通过请求暴露出来的,该请求已在 MS-TRP 协议中记录。
根据规格说明:
GetUIDllName 数据包与 TUISPIDLLCallback 数据包和 FreeDialogInstance 数据包一起用于在服务器上安装、配置或删除 TSP。
审查实现情况可知,虽然非管理调用者只能从注册表中预定义的列表中选择提供程序,但管理客户端可以从任意路径加载提供程序 DLL。
switch (pParams->dwObjectType) { case TUISPIDLL_OBJECT_LINEID: ... case TUISPIDLL_OBJECT_PHONEID: ... case 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 ((ptClient->dwFlags & 8) == 0 && (pParams->bRemoveProvider || pParams->dwProviderFilenameOffset != TAPI_NO_DATA)) { pParams->lResult = LINEERR_OPERATIONFAILED; return; } if (pParams->dwProviderFilenameOffset != TAPI_NO_DATA) { // The path is supplied in request TCHAR *pszProviderFilename = pDataBuf + pParams->dwProviderFilenameOffset; if (ptDlgInst->hTsp = LoadLibrary(pszProviderFilename)) { if (pfnTSPI_providerUIIdentify = (TSPIPROC) GetProcAddress(ptDlgInst->hTsp,"TSPI_providerUIIdentify")) { pParams->lResult = pfnTSPI_providerUIIdentify(pszProviderFilename); } else { ... } } else { ... } } else { .... } }
通过提交一个dwObjectType设置为TUISPIDLL_OBJECT_PROVIDERIDGetUIDllName请求,并指定一个攻击者控制的 DLL 路径,我们可以让电话服务加载该 DLL 并调用导出的TSPI_providerUIIdentify函数。这在服务上下文中提供了一种简单可靠的代码执行原语。此外,如果导出的函数返回非零值,服务会在调用后卸载该 DLL,从而允许之后从磁盘中删除有效载荷。
一种显而易见的交付机制是指定一条指向攻击者控制的 SMB 共享的 UNC 路径。实际上,当共享托管在同一域内的标准 Windows 计算机上时,这种方法可靠有效。但是,攻击者托管的 SMB 服务器(例如impacket-smbserver或 Samba)可能会触发访客访问限制,导致LoadLibrary失败并返回ERROR_SMB_GUEST_LOGON_BLOCKED错误。
由于已经存在任意文件写入原语,因此本地 DLL 部署提供了一种可靠的替代方案。
可以使用accesschk来识别合适的可写文件。例如,以下文件几乎存在于所有系统中:
C:\Windows\System32\catroot2\dberr.txtC:\Windows\ServiceProfiles\NetworkService\AppData\Local\Temp\MpCmdRun.logC:\Windows\ServiceProfiles\NetworkService\AppData\Local\Temp\MpSigStub.log
虽然使用 4 字节事件写入来写入有效载荷大小的 DLL 速度相对较慢,但它完全消除了对外部基础设施的需求。
为了演示代码执行,可以构建一个最小的概念验证型 TSP DLL。在以下示例中,TSPI_providerUIIdentify导出(由电话服务在提供商安装期间调用)执行一个命令并将结果写入磁盘:
#include <Windows.h>extern "C" __declspec(dllexport) LONG __stdcall TSPI_providerUIIdentify(LPWSTR lpszUIDLLName){ wchar_t cmd[] = L"cmd.exe /c whoami /all > C:\\Windows\\Temp\\poc.txt"; STARTUPINFO si; PROCESS_INFORMATION pi; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); ZeroMemory(&pi, sizeof(pi)); if (CreateProcessW(NULL, cmd, NULL, NULL, FALSE, CREATE_NO_WINDOW | NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &pi)) { CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } return 0x1337;}
TSPI_providerUIIdentify的返回值会传播回 RPC 客户端,从而提供一个明确的信号,表明有效负载已执行:
披露和补丁时间表
Nov 6, 2025
– Vulnerability reported to Microsoft.2025 年 11 月 6 日– 已向微软报告漏洞。
Dec 22, 2025
– Microsoft confirmed the issue as a security vulnerability.2025 年 12 月 22 日——微软确认该问题为安全漏洞。
Dec 23, 2025
– $5,000 bounty awarded under the Microsoft Bug Bounty Program.2025 年 12 月 23 日– 根据微软漏洞赏金计划颁发 5,000 美元赏金。
Dec 29, 2025
– CVE-2026-20931 assigned.2025 年 12 月 29 日– CVE-2026-20931 被分配。
Jan 13, 2026
– Fix released as part of the January 2026 Patch Tuesday updates.2026 年 1 月 13 日– 此修复程序已作为 2026 年 1 月星期二补丁更新的一部分发布。
Jan 19, 2026
– This write-up published.2026 年 1 月 19 日——本文发表。
该漏洞已按照协调漏洞披露实践予以披露。
微软的公告可在 2026 年 1 月安全更新指南中找到,编号为。
结论
这项研究表明,即使是很少使用的传统 Windows 子系统,仍然会暴露出复杂而强大的攻击面。探索 TAPI 的过程远比我预想的有趣得多——这提醒我们,一些最有价值的研究往往隐藏在平台中那些容易被忽视的部分。
最后需要提醒的是,这里描述的漏洞仅影响 TAPI 配置为服务器模式的系统——这是一种相对不常见的设置,旨在用于集中式电话基础设施,这大大限制了实际的风险。
相关链接:https://habr.com/ru/companies/pt/articles/984934/
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:PocketSec PocketSec PocketSec《谁在电话线上?利用 Windows 电话服务中的远程代码执行漏洞( CVE-2026-20931)》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论