2026/5/18 21:52:59
网站建设
项目流程
大学生网站开发与设计实训报告,请问有没有做网站,wordpress管理员密码忘,哈尔滨建设网站的免费咨询Who’s on the Line? Exploiting RCE in Windows Telephony Service
Written by Sergey Bliznyuk on January 19, 2026
几十年来#xff0c;Windows一直支持计算机电话集成#xff0c;为应用程序提供管理电话设备、线路和通话的能力。虽然现代部署越来越依赖基于云的电话解决…Who’s on the Line? Exploiting RCE in Windows Telephony ServiceWritten by Sergey Bliznyuk on January 19, 2026几十年来Windows一直支持计算机电话集成为应用程序提供管理电话设备、线路和通话的能力。虽然现代部署越来越依赖基于云的电话解决方案但经典电话服务在Windows中仍然开箱即用并继续在特定环境中使用。因此遗留的电话组件仍然是默认Windows攻击面的一部分。本研究探讨了我在电话服务的服务器模式中发现的一个漏洞该漏洞允许低权限客户端向服务可访问的文件写入任意数据并在特定条件下实现远程代码执行。Windows电话技术概述Windows通过电话应用程序编程接口TAPI公开电话功能该接口允许用户模式应用程序通过统一的抽象层与电话设备和服务进行交互。TAPI有两种主要形式TAPI 2.x提供过程式的C风格API以及TAPI 3.x使用COM实现。虽然API不同但两者都依赖于相同的基础架构应用程序与TAPI运行时通信后者将请求转发给电话服务提供商TSP。TSP是供应商提供的组件封装了特定于设备或服务的逻辑并与底层电话后端如物理电话硬件、PBX系统或VoIP端点交互。从客户端应用程序的角度来看这些差异隐藏在TAPI抽象层之后。什么是电话服务应用程序通过调用tapi32.dll导出的TAPI 2.x函数或使用tapi3.dll提供的TAPI 3.x COM接口来与Windows电话堆栈交互。在这两种情况下这些库主要充当客户端包装器它们编排请求并将其转发给实际实现电话逻辑的系统服务。该服务就是电话服务TapiSrv。它实现了实际的TAPI功能并通过tapsrv RPC接口将其暴露给客户端应用程序。当应用程序调用TAPI调用时请求最终由TapiSrv处理TapiSrv选择合适的TSP并编排相应的低级交互。该服务在NETWORK SERVICE账户下运行并配置为手动启动类型但在进程首次通过tapi32.dll或tapi3.dll调用TAPI请求时会按需自动启动。整个实现位于tapisrv.dll库中。MSDN上的图表已经过时但它提供了基本的理解TAPSRV RPC接口概述TAPI客户端与电话服务之间的通信通过名为tapsrv的经典MSRPC接口进行。相应的协议MS-TRP是公开记录的。默认情况下此接口仅限于本地调用者。然而在Windows Server系统上TAPI可以配置为接受远程客户端连接。此行为由HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Telephony\Server\DisableSharing注册表值控制也可以通过电话管理MMC管理单元TapiMgmt.msc进行管理。虽然远程访问本地调制解调器或电话设备很少有用但此功能适用于服务器端电话部署如PBX系统或电话交换机。在此类场景中电话硬件和相关的TSP集中安装在服务器上多个支持TAPI的客户端远程连接而不是维护各自的TSP安装。客户端可以通过tcmsetup /c SERVER NAME命令配置为使用远程TAPI服务器。启用远程访问时该接口通过tapsrv命名管道暴露这意味着客户端必须首先通过SMB进行身份验证以建立连接。在此配置中TAPI服务器还会将服务相关信息发布到Active Directory使其在域环境中相对容易被发现。请求分发模型tapsrv RPC接口非常精简仅包含三个可调用的方法ClientAttach、ClientDetach和ClientRequest。会话初始化和拆除由前两个调用处理而ClientRequest用于调用所有与电话相关的操作。ClientRequest接受一个代表序列化请求数据包的二进制块。此数据包的前四个字节包含一个Req_Func字段该字段充当内部调度表的索引。缓冲区的其余部分包含特定于所选操作的编组参数。支持的Req_Func值和相应的数据包布局主要在MS-TRP规范中记录并紧密镜像Win32 TAPI 2.x API接口。从概念上讲这导致在MSRPC之上有一个额外的分发层——实际上是一种“RPC中的RPC”设计。类似的模式出现在其他Windows服务中例如RasMan服务暴露的RASRPC接口几个月前我在该服务中也发现了一个LPE漏洞。客户端会话设置在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]部分下的用户也会被授予提升的权限。为了调用与线路抽象相关的方法客户端随后必须通过发送Initialize请求来初始化线路应用程序实例。异步事件处理电话本质上是事件驱动的来电、状态更改和媒体事件可能独立于客户端请求而发生。由于MSRPC遵循同步请求-响应模型MS-TRP协议实现了自己的机制用于将异步事件从电话服务传递给连接的客户端。事件传递模型在初始的ClientAttach调用期间协商并根据客户端是本地还是远程而有所不同。对于本地客户端异步事件使用共享的同步对象传递。客户端在ClientAttach期间提供其进程标识符lProcessID并接收事件对象的句柄。当事件数据可用时电话服务发出此事件的信号提示客户端通过发出GetAsyncEvents请求来检索待处理的数据。当TAPI服务器模式启用时协议提供两种替代机制来传递异步事件推送和拉取。所选模型由提供给ClientAttach的参数决定。在推送模型中客户端将pszDomainUser参数留空并在pszMachine参数中提供以引号分隔的RPC字符串绑定例如CLIENT-PC-NAMEncacn_ip_tcp31337。电话服务建立到端点的反向RPC连接绑定到remotesp接口并在异步事件发生时调用RemoteSPEventProc方法。在拉取模型中客户端在会话初始化期间在pszDomainUser参数中指定一个邮件槽名称。电话服务定期向此邮件槽发送DWORD大小的数据报指示事件可用于检索。然后客户端应使用GetAsyncEvents获取相应的事件数据。在所有情况下服务器使用客户端在Initialize数据包中提供的InitContext字段值将事件与特定的线路应用程序关联。该值被视为不透明的4字节标识符并由服务器作为事件通知的一部分回显给应用程序。邮件槽的把戏邮件槽是一种遗留的Windows IPC机制设计用于传输小的单向消息。邮件槽写入器向命名端点发送数据报而接收者被动地读取传入的消息。从客户端角度来看邮件槽使用标准的Win32文件API访问如CreateFile、WriteFile和CloseHandle。邮件槽使用特殊路径语法寻址形式如下\\COMPUTERNAME\MAILSLOT\MailslotName从客户端的角度来看生成的句柄表现得像一个只写文件。通过网络邮件槽消息使用NetBIOS-over-UDP数据报传输或者曾经传输过——自Windows 11 24H2以来远程邮件槽已被禁用。由于通信严格是单向的发送方不会收到远程邮件槽存在或消息正在被处理的确认。如前所述电话服务使用拉取异步事件模型通过定期向客户端提供的邮件槽名称发送数据报来通知远程客户端有关待处理的事件。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远程调用该数据包是已记录的tapiRequestMakeCallAPI的后端实现。当客户端通过未记录的LRegisterRequestRecipient处理器Req_Func 61注册或取消注册为请求接收者时该处理器支持lineRegisterRequestRecipientAPI最高优先级线路应用程序会重新计算。相关逻辑如下所示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 apps 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, LSoftware\\Microsoft\\Windows\\CurrentVersion\\Telephony\\HandoffPriorities, 0, 0x20019, hKey) ) { // Load the value from the specified registry key GetPriorityList(hKey, LRequestMakeCall, ppszPriorityList); RegCloseKey(hKey); } RegCloseKey(phkResult); } LeaveCriticalSection(gPriorityListCritSec); return RpcRevertToSelf(); }具体来说服务读取客户端HKCU配置单元下的以下键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设置InitContext为所需的DWORD值pszModuleName为DIALER.EXE或其他高优先级条目使用LRegisterRequestRecipientReq_Func 61,dwRequestMode LINEREQUESTMODE_MAKECALL,bEnable 1将线路应用程序注册为请求接收者。通过提交TRequestMakeCall数据包Req_Func 121触发事件。使用GetAsyncEventsReq_Func 0出队事件完成写入。取消注册请求接收者LRegisterRequestRecipient,bEnable 0。使用ShutdownReq_Func 86关闭线路应用程序。重复此序列允许攻击者向电话服务可写的任意预先存在的文件中写入任意数据。从文件写入到RCE在这个阶段漏洞利用需要一个NETWORK SERVICE可写的现有文件。一个特别明显的候选文件是前面提到的C:\Windows\TAPI\tsec.ini。在服务器模式下运行电话服务的系统上该文件始终存在且服务账户可写。该文件除了其他配置设置外还定义了电话服务将哪些用户视为管理员。通过在[TapiAdministrators]下添加一个条目例如[TapiAdministrators]\r\nDOMAIN\\attacker1远程无权限的域用户可以授予自己在电话服务中的管理权限。在此修改后通过ClientAttach建立新会话会导致客户端上下文设置了管理权限标志。有了电话服务的管理访问权限额外的攻击面变得可用。一个特别强大的原语通过GetUIDllName请求暴露该请求在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_PROVIDERID并指定攻击者控制的DLL路径的GetUIDllName请求我们可以让电话服务加载该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[] Lcmd.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客户端提供了负载已执行的明确信号。披露与修复时间线2025年11月6日– 漏洞报告给微软。2025年12月22日– 微软确认该问题为安全漏洞。2025年12月23日– 根据微软漏洞赏金计划获得5000美元赏金。2025年12月29日– 分配CVE-2026-20931。2026年1月13日– 修复作为2026年1月补丁星期二更新的一部分发布。2026年1月19日– 本文发布。此漏洞是根据协调漏洞披露实践披露的。微软的公告可在2026年1月安全更新指南中的CVE-2026-20931下找到。结论这项研究表明即使很少使用的遗留Windows子系统仍然可以暴露复杂而强大的攻击面。探索TAPI的结果比我预期的要有趣得多——这提醒我们最有价值的研究往往隐藏在平台中容易被忽视的部分。最后值得注意的是这里描述的漏洞仅影响配置为服务器模式的TAPI系统——这是一种相对不常见的设置用于集中式电话基础设施这极大地限制了实际暴露范围。pXE4VDOqwlf/p8ApaIB8OCxgzALcVOdg2L9/hndJQNyiFBvCEz/KALTGRbGq86VL7t8a6/Kf4QiNsvvHrxnDiW3OdSmDENZvq8g0WAuLT0xzhp79Pqs18qZFNS2WjN更多精彩内容 请关注我的个人公众号 公众号办公AI智能小助手对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号网络安全技术点滴分享