- 2020-12-14 - Disclosed to Cisco PSIRT
- 2021-02-09 - Incident manager assigned
- 2021-03-05 - Cisco confirmed RCE/Drive-by-download findings, confirmed not validating certs on remote is 'expected behaviour'
- 2021-07-07 - Cisco published without having gotten to delivering a fix.
- 2022-08-11 - Rapid7 / Jake Baines publish followon research - apparently the original fix for this was incomplete, and they took it a bunch further. They do credit, but fail to invite me to present at Black Hat USA/DEF CON with them ;-) https://forum.defcon.org/node/241939 and https://www.rapid7.com/blog/post/2022/08/11/rapid7-discovered-vulnerabilities-in-cisco-asa-asdm-and-firepower-services-software/
https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-asdm-rce-gqjShXW
To follow along, unpack the ASDM IDM Launcher 1.9 msi with 7zip/cabextract, decompile with jd-gui.
mkdir unpacked ; cd unpacked
7z x ../dm-launcher.msi
cabextract Data1.cab
java -jar jd-gui-1.6.6.jar asdm_launcher.jar
java -jar jd-gui-1.6.6.jar jploader.jar
In the obfuscated com.cisco.launcher.w class, method a() does the evil:
HttpsURLConnection.setDefaultSSLSocketFactory(sSLContext.getSocketFactory());
n.a();
HostnameVerifier hostnameVerifier = new HostnameVerifier() {
public boolean verify(String param1String, SSLSession param1SSLSession) {
return true;
}
};
HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier);
It appears to be a logical error as n.a() does appear to try and setup a somewhat sane verifier, before they override with the poor no-op verifier as default... this error permits MITM of devices which ordinarily present a valid/trusted certificate, in addition to allowing connection to any TLS-enabled, attacker-controlled host with impunity.
The vendor indicated this is by design, but accepts that it might not be the wisest choice in 2021...
it has been confirmed that this lack of validation has always been present on ASDM and it is an expected behaviour. ASDM does accept self-signed certificates from the ASA as a way to allow customers without CA signed certificates to manage their devices. If customers decide to add a CA signed certificate to either the ASA or to ASDM, it's is possible and supported. It is true that this design choice may be perceived as a flaw but this is something the product team has repeatedly discussed with the customer base. For the future, I've opened an enhancement request for ASDM in order to re-evaluate this choice, and to set the ASDM default behaviour to be a mandatory certificate validation, rather than the other way around
After downloading the /admin/login_banner
and /admin/version.prop
from the target 'device' - the Launcher application attempts to retrieve /admin/pdm.sgz
.
This is an encapsulated form of Java-Class delivery with no code-signing at all. The content is trusted and executed in the context of the victim's Launcher process. All that is required to deliver arbitrary code to the victim is an understanding of the SGZ format, which is reasonably trivial, and to send a class PDMApplet
that can be cast to SgzApplet
. See PDMApplet.java and serve.py for sample, non-evil, PoC exploit.
Further the user-entered credentials are passed verbatim to this code's start()
method...
All the social engineering required is a simple 'Hey IT Dude, please help me configure this ASA at IP...'
Video PoC at https://youtu.be/fAQNMzCwvF8
By spoofing launcher.version = 2.9
in the returned version.prop
we can direct the launcher to open a web-browser to the following URL 'https://myasa/admin/dm-launcher.dmg' where myasa is the faked/MITM'd device. The browser will complain wildly about the cert in this case, but I'd hazard that enough would also click-thru.
Especially since we can spoof login_banner
to say something along the lines of 'Mandatory Security Update Available'...
Header+Chunk+(Chunk+...)+EndChunk
17 byte header - of which 16 byte fingerprint and 1 byte = version + 100. Fingerprint seems to be used for nothing more than local cache of downloads.
Chunk header of 5 bytes
- 4 byte int32 length (LSB first, length not including chunk header)
- 1 byte compression indicator (ref: "SgzReader: chunk dataLen")
- ChunkData
Compression Indicator 0 = raw, 2=cBzip2, 3=lzma, default=Gzip
End marker (which must be present as the outer container has no length) is chunk header with len<0 (e.g. len=0xFFFFFFFF comp=0x00) and no data.
Inside the chunk-data:
- Seems like we have 2 byte len('jarname')
- 'jarname' string
- 4 byte 'jar' length
- 'jar' data.
while (read(inputStream, arrayOfByte1, 0, 2) == 2) {
int m = get2(arrayOfByte1, 0);
while (arrayOfByte1.length < m)
arrayOfByte1 = new byte[2 * arrayOfByte1.length];
if (read(inputStream, arrayOfByte1, 0, m) != m)
break;
String str = new String(arrayOfByte1, 0, m);
if (read(inputStream, arrayOfByte1, 0, 4) != 4)
break;
int n = (int)get4(arrayOfByte1, 0);
StoredJarEntry storedJarEntry = new StoredJarEntry();
if (n >= 0) {
storedJarEntry._data = new byte[n];
if (read(inputStream, storedJarEntry._data, 0, n) != n)
break;
}
int i1 = (j - lengthLimitedInputStream1.available()) * 100 / j;
storeEntry(str, storedJarEntry);
Loader.updateStatus(4, i1);
}
In fact - this is somewhat misleading, each 'JarEntry' is actually the individual class. so you need to send MyClass.class MyOtherClass.class in separate chunks.
A Python implementation of the Sgz stream packing code can be found in serve.py's buildSgz()
TODO There may also be a special env.properties
entry in here which is parsed as a Java Properties
object for $reasons-unknown