Debugging Windows: New Users, Powershell, and TLS
A little context
I wanted to work on this PR in Infection Monkey. Basically, a very simple feature - make the Monkey create a new user, and try to create an HTTPS request as that new user.
ToC
- A little context
- ToC
- How I wanted to do it
- The problems
- The first problem:
The application failed to initialize properly (0xc0000124)
- The second problem:
The response content cannot be parsed because the Internet Explorer engine is not available, or Internet Explorer's first-launch configuration is not complete.
- The third problem: Unsupported TLS version
- The first problem:
- Conclusions
More context
👩🔧 If you’re here for the technical stuff only you can skip this section.
The Infection Monkey is getting a big upgrade right now (Jan 2020) relating to the Zero Trust territory. One of Zero Trust’s pillars is People (i.e. User Identity) - meaning how User Identity is secured in your network. The test we wanted to implement in the Monkey to check out that pillar was supposed to imitate the following Attack scenario:
- Attacker gets into the machine.
- Attacker creates a new local user.
- Attacker does actions as that new user, including communicating with the internet.
This test checks how much a network adheres to the People pillar of Zero Trust, since if you’re enforcing that part of you network security correctly - a totally new, unknown user SHOULDN’T be able to access the internet at all.
How I wanted to do it
I already had a POC of the following flow working and in the Repo:
- Create a new user with the
net user
command. - Log on as the new user (see on Github):
import win32security
import win32con
try:
# Logon as new user: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-logonusera
self.logon_handle = win32security.LogonUser(
self.username,
".", # Use current domain.
self.password,
win32con.LOGON32_LOGON_INTERACTIVE, # Logon type - interactive (normal user). Need this to open ping
# using a shell.
win32con.LOGON32_PROVIDER_DEFAULT) # Which logon provider to use - whatever Windows offers.
except Exception as err:
raise NewUserError("Can't logon as {}. Error: {}".format(self.username, str(err)))
- Run
PING.exe
as that user usingCreateProcessAsUser
, withself.logon_handle
(see on GitHub):
def run_as(self, command):
# Importing these only on windows, as they won't exist on linux.
import win32con
import win32process
import win32api
import win32event
exit_code = -1
process_handle = None
thread_handle = None
try:
# Open process as that user:
# https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessasusera
process_handle, thread_handle, _, _ = win32process.CreateProcessAsUser(
self.get_logon_handle(), # A handle to the primary token that represents a user.
None, # The name of the module to be executed.
command, # The command line to be executed.
None, # Process attributes
None, # Thread attributes
True, # Should inherit handles
win32con.NORMAL_PRIORITY_CLASS, # The priority class and the creation of the process.
None, # An environment block for the new process. If this parameter is NULL, the new process
# uses the environment of the calling process.
None, # CWD. If this parameter is NULL, the new process will have the same current drive and
# directory as the calling process.
win32process.STARTUPINFO() # STARTUPINFO structure.
# https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa
)
logger.debug(
"Waiting for process to finish. Timeout: {}ms".format(WAIT_TIMEOUT_IN_MILLISECONDS))
# Ignoring return code, as we'll use `GetExitCode` to determine the state of the process later.
_ = win32event.WaitForSingleObject( # Waits until the specified object is signaled, or time-out.
process_handle, # Ping process handle
WAIT_TIMEOUT_IN_MILLISECONDS # Timeout in milliseconds
)
exit_code = win32process.GetExitCodeProcess(process_handle)
finally:
try:
if process_handle is not None:
win32api.CloseHandle(process_handle)
if thread_handle is not None:
win32api.CloseHandle(thread_handle)
except Exception as err:
logger.error("Close handle error: " + str(err))
return exit_code
One Googling session later to look for the proper way to send HTTP requests in Windows; and I find that to do that, since we don’t have curl
, I’ll need to use powershell
. All I wanted to do was change PING.exe
to powershell.exe -command "Invoke-WebRequest https://infectionmonkey.com/"
. Sounds simple, doesn’t it?
The problems
The first problem: The application failed to initialize properly (0xc0000124)
Like any good developer, I went looking for this error code. I also improved my logging of the error code. The problem seemed to be a DLL init problem.
What does that mean? I used some procmon
to try to figure out what the problem was, but that proved not super useful. My hunch was that since powershell
is a modern app, it has more dependencies and requirements that ping
simply doesn’t, so after a lot of time wasted on trying to pinpoint the issue, I decided to go with a “try until it works” approach.
Based on this StackOverflow thread, I realized I should use CreateProcessWithLogonW
instead of CreateProcessAsUser
. CreateProcessWithLogonW
managed to create an environment where Powershell.exe
could run, whereas LogonUser
+ CreateProcessAsUser
didn’t.
This is because CreateProcessWithLogonW
uses an RPC call to ncalrpc:[SECLOGON]
in svchost
(SeclCreateProcessWithLogonW
from seclogon.dll
called). Internally it does CreateProcessAsUser
eventually, but it logs on in a different way and ALSO creates a new session + window. This was enough for powershell
to run; the actual issue of the environment that powershell
needed to run was solved.
This was VERY hard and cryptic to figure out, and took the better part of a day. I was very happy to be done with it; then I got another Error code. 0x1. Hey, at least it’s a normal number this time 😑
The second problem: The response content cannot be parsed because the Internet Explorer engine is not available, or Internet Explorer's first-launch configuration is not complete.
The second bug was with the actual command itself. After creating a new user, Invoke-WebRequest
won’t work. How did I figure this out? I created a debug breakpoint and manually opened a powershell as the new user, just after it was created.
We tried running echo hello
- it worked. So we tried running Invoke-WebRequest
and it failed:
This is because Invoke-WebRequest
internally uses Internet Explorer’s engine to parse the response. Since the user is totally new, “Internet Explorer’s first-launch configuration is not complete” and therefore the Internet Explorer engine is not available.
The fix for that was to add the -UseBasicParsing
flag. At first, I thought it was deprecated, but I looked at the wrong documentation since I was testing with an old powershell
version. In the newer powershell
versions this shouldn’t be an issue at all since PS6 doesn’t rely on Windows.
The third problem: Unsupported TLS version
After I was done testing the feature on my machine, I moved to testing the feature on a specific Windows server; where, to no one’s surprise, the feature didn’t work correctly. This happened since I was testing with https://infectionmonkey.com
which ONLY supported TLS 1.2
, but the powershell
commandlet I was using used TLS 1.0
by default.
To overcome this I manually decided which security protocol powershell
was going to use, by adding the [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
command to the powershell
script. That set the TLS version of the request and fixed the issue. See the pull request.
This problem also brought to my attention the fact that https://infectionmonkey.com
doesn’t support old TLS versions - but we decided that’s OK 🔐😀
Conclusions
- Windows will never cease to surprise me, but not all surprises are good.
- If you’re using Windows, try to use the highest-level API you can use. Usually that gives the best results.
- Talking to people is still the #1 method of debugging. 100% of the fixes in this process were inspired by my coworkers.
- I can fix anything given enough time. So can you 🗻