NTLMSSP decryption works only for the first RPC call then fails for the others following
Summary
I observed that during a DCE/RPC exchange, the NTLMSSP decryption feature works well for the first RPC call (request & response are properly decrypted), but then it fails for all the following calls (decrypted data is garbage).
This is a bug that I initially discovered when working on my SharkFest EU 22 talk, you can see it live: https://youtu.be/5O1tKxEa1iY?t=2100
I'm currently investigating the issue, but I'm reporting it already
Steps to reproduce
It can be reproduced using the following Python3 script, which uses the impacket library. It runs fine on a Linux system, against a Windows target. Admin privileges on the remote Windows target are required.
from impacket.dcerpc.v5 import transport, rpcrt, epm, tsch
from impacket.dcerpc.v5.dtypes import NULL
from impacket import ntlm
import random
import string
username = "FIXME"
password = "FIXME"
domain = "FIXME.lan"
remoteHost = "FIXME_IP"
xml = """<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<Triggers>
<CalendarTrigger>
<StartBoundary>2015-07-15T20:35:13.2757294</StartBoundary>
<Enabled>true</Enabled>
<ScheduleByDay>
<DaysInterval>1</DaysInterval>
</ScheduleByDay>
</CalendarTrigger>
</Triggers>
<Principals>
<Principal id="LocalSystem">
<UserId>S-1-5-18</UserId>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>true</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>true</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>P3D</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="LocalSystem">
<Exec>
<Command>%s</Command>
<Arguments>%s</Arguments>
</Exec>
</Actions>
</Task>
"""
stringBinding = epm.hept_map(remoteHost, tsch.MSRPC_UUID_TSCHS, protocol = 'ncacn_ip_tcp')
print(stringBinding)
rpctransport = transport.DCERPCTransportFactory(stringBinding)
# ntlm.USE_NTLMv2 = False
rpctransport.set_credentials(username, password, domain)
dce = rpctransport.get_dce_rpc()
dce.set_auth_level(rpcrt.RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
dce.set_auth_type(rpcrt.RPC_C_AUTHN_WINNT)
dce.connect()
dce.bind(tsch.MSRPC_UUID_TSCHS)
tmpName = ''.join([random.choice(string.ascii_letters) for _ in range(8)])
print('Creating task \\%s' % tmpName)
tsch.hSchRpcRegisterTask(dce, '\\%s' % ("firsttask" + tmpName), xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE)
print('Creating task \\%s' % tmpName)
tsch.hSchRpcRegisterTask(dce, '\\%s' % ("secondtask" + tmpName), xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE)
resp = tsch.hSchRpcEnumTasks(dce, "\\")
resp.dump()
It connects to the task scheduler RPC server, creates a first scheduled task, then a second one with the same XML content.
To anticipate some questions:
- this use-case is a bit useless, but it's the minimization of the bug reproduction steps that I've managed to achieve
- you could think that Impacket is actually generating bad data, but it isn't the case since the second task is properly created on the target. Also, I initially found the bug when analyzing other RPC captures which were generated by normal Windows systems (so not a 3rd-party library). You can create a similar example using schtasks.exe in Windows.
What is the current bug behavior?
The first call is properly decrypted (UTF-16 encoded so just look carefully and ignore all the null-bytes):
Whereas the second call (and all the others after) is not:
What is the expected correct behavior?
I'd expect to have all calls in the RPC exchange to be properly decrypted.
Sample capture file
Here are a couple samples:
- create_two_tasks_then_enum_RPC_C_AUTHN_LEVEL_CONNECT_NTLMv2.pcapng: uses RPC_C_AUTHN_LEVEL_CONNECT so it isn't encrypted. Just so you can see what the decrypted data should look like. No bug here of course.
-
create_two_tasks_then_enum_RPC_C_AUTHN_LEVEL_PKT_PRIVACY_NTLMv1_ESS.pcapng: uses RPC_C_AUTHN_LEVEL_PKT_PRIVACY so it's encrypted. By using
ntlm.USE_NTLMv2 = False
I forced it to NTLMv1 so we can see it's affected too. Bug is visible. - create_two_tasks_then_enum_RPC_C_AUTHN_LEVEL_PKT_PRIVACY_NTLMv2.pcapng: uses RPC_C_AUTHN_LEVEL_PKT_PRIVACY so it's encrypted. It uses NTLMv2 NTLMv2 which is affected too. Bug is visible.
For all these files, the password of my user is: clem
. You must configure it like this (or using the keytab method):
Relevant logs and/or screenshots
See above
Build information
Version 4.0.5 (v4.0.5-0-ge556162d8da3).
Compiled (64-bit) using Microsoft Visual Studio 2022 (VC++ 14.32, build 31332),
with GLib 2.72.3, with PCRE2, with zlib 1.2.12, with Qt 5.15.2, with libpcap,
with Lua 5.2.4, with GnuTLS 3.6.3 and PKCS #11 support, with Gcrypt 1.10.1, with
Kerberos (MIT), with MaxMind, with nghttp2 1.46.0, with brotli, with LZ4, with
Zstandard, with Snappy, with libxml2 2.9.14, with libsmi 0.4.8, with
QtMultimedia, with automatic updates using WinSparkle 0.5.7, with AirPcap, with
SpeexDSP (using bundled resampler), with Minizip, with binary plugins.
Running on 64-bit Windows 10 (22H2), build 19045, with Intel(R) Core(TM)
i7-4700MQ CPU @ 2.40GHz (with SSE4.2), with 16303 MB of physical memory, with
GLib 2.72.3, with PCRE2 10.40 2022-04-14, with Qt 5.15.2, with Npcap version
1.71, based on libpcap version 1.10.2-PRE-GIT, with c-ares 1.18.1, with GnuTLS
3.6.3, with Gcrypt 1.10.1, with nghttp2 1.46.0, with brotli 1.0.9, with LZ4
1.9.3, with Zstandard 1.5.2, without AirPcap, with light display mode, without
HiDPI, with LC_TYPE=French_France.utf8, binary plugins supported.