commit 8ba4c651fb38cef643bfa9d6fef66c38d2a31a74 Author: Giulio Date: Tue Jan 21 10:08:51 2020 +0100 Writeup draft diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..9bbd1cf --- /dev/null +++ b/Readme.md @@ -0,0 +1,154 @@ +# Juniper Host Checker Linux MITM RCE +## Intro +The Host Checker is a client side component that some Pulse Secure appliances may require in order to connect to the VPN. The Host Checker requests a policy from the server and perform basic checks on the client accordingly. Checks may include MAC Addresses, running process (ie: checking for an antivirus) and some others. While on Windows the plugin is an ActiveX component, in Linux, Solaris and OSX it is a Java Applet. +Of course client checks can always be bypassed, and an open source (yet not well documented) implementation [do exist](https://raw.githubusercontent.com/russdill/juniper-vpn-py/master/tncc.py). +## Sumamry +Probably in order to still works with misconfigured instances, the Host Cheker does not check neither the validity of the server certificate nor its hostname. The server can set a malicious cookie, which can be used to exploit a command injection when the user is found not compliant. Note that a malicious server can force a user to be non compliant. +## Code +### Certificate +In `net.juniper.tnc.client.HttpNAR.HttpNAR`: +``` + private void trustAllCerts() throws Exception { + final TrustManager[] tm = { new X509TrustManager() { + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + @Override + public void checkClientTrusted(final X509Certificate[] array, final String s) { + } + + @Override + public void checkServerTrusted(final X509Certificate[] array, final String s) { + } + } }; + final SSLContext instance = SSLContext.getInstance(NARUtil.getSSLProtocol()); + instance.init(null, tm, new SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(instance.getSocketFactory()); + } + + private void allowHostnameMismatch() { + HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { + @Override + public boolean verify(final String s, final SSLSession sslSession) { + return true; + } + }); + } +``` +Both function gets executed when initializing the connection to a server. From the same class as above: +``` + public void initialize(final String[] array) throws Exception { + [..parametr parsing..] + if (this.mHomeDir.length() == 0) { + this.mHomeDir = System.getProperty("user.home"); + } + if (HttpNAR.gLoggingEnabled) { + [..omitted log instructions..] + } + if (!this.isPlatformSupported()) { + NARUtil.logError("HttpNAR: unsupported operating system " + NARUtil.getOSName() + "; stopping..."); + throw new Exception("Unsupported operating system"); + } + this.mAppSupportDir = this.getPlatformSupportDir(this.mHomeDir); + [..proxy usage..] + + (this.mTncClient = new TNCClient()).initialize(this); + this.loadbundledIMC(); + this.mHandshakeRequestor.start(); + this.trustAllCerts(); + this.allowHostnameMismatch(); + } +``` +### Cookie +In order for the Host Checker to work two cookies are needed, `DSPREAUTH` and `DSSIGNIN`. +They can be either set by the server or from sending commands to a socket listening to all interfaces (but accepting connections only from localhost). +From `net.juniper.tnc.client.HttpNAR.HttpConnection`: +``` + public int sendUpdate(final byte[] array, final ByteArrayOutputStream byteArrayOutputStream, final boolean b) throws Exception { + NARUtil.logInfo("TNCoHTTP: sending update = "); + NARUtil.logData(array); + final String byteArrayToBase64 = Base64.byteArrayToBase64(array); + final String string = "https://" + this.mIveHost + "/dana-na/hc/tnchcupdate.cgi"; + NARUtil.logInfo("TNCoHTTP: opening connection to " + string); + final HttpsURLConnection httpsURLConnection = (HttpsURLConnection)new URL(string).openConnection(); + httpsURLConnection.setDoOutput(true); + httpsURLConnection.setDoInput(true); + httpsURLConnection.setRequestProperty("Cookie", "DSPREAUTH=" + this.mPreauthCookie + ";DSSIGNIN=" + this.mSignIn); + if (this.mUserAgent.length() > 0) { + httpsURLConnection.setRequestProperty("User-Agent", this.mUserAgent); + } + NARUtil.setConnectTimeout(httpsURLConnection, 2000); + httpsURLConnection.connect(); + final PrintWriter printWriter = new PrintWriter(httpsURLConnection.getOutputStream()); + [..adding parameters..] + if (b) { + printWriter.print("firsttime=1;"); + } + printWriter.close(); + String headerFieldKey; + for (int n = 1; (headerFieldKey = httpsURLConnection.getHeaderFieldKey(n)) != null; ++n) { + final String headerField = httpsURLConnection.getHeaderField(n); + if (headerFieldKey.equalsIgnoreCase("Set-Cookie") && headerField.startsWith("DSPREAUTH=")) { + final int index = headerField.indexOf(59); + this.mPreauthCookie = headerField.substring(10, index); + this.mPreauthOpts = headerField.substring(index); + NARUtil.logInfo("TNCoHTTP: received response DSPREAUTH = " + this.mPreauthCookie + this.mPreauthOpts); + } + } +``` + +### Command injection +In `net.juniper.tnc.client.HttpNAR.TNCHandshake`: +``` + public void doCustomRemediateInstructions() { + final StringBuffer sb = new StringBuffer(); + sb.append("https://").append(this.mIcURL); + sb.append("/dana-na/auth/rdpreauth.cgi?DSPREAUTH=" + this.mCookie); + final String string = sb.toString(); + NARUtil.logInfo("TncHandshake: launching browser for " + string); + NARUtil.execCommand((NARUtil.isMacOSX() ? "open -a Safari " : ((NARUtil.isLinux() || NARUtil.isSolaris()) ? "firefox " : "")) + string); + NARUtil.logInfo("TncHandshake: browser launched"); + } +``` +From `net.juniper.tnc.client.HttpNAR.NARUtil`: +``` + public static String execCommand(final String s) { + String execCommand = null; + if (NARUtil.PlatformUtil != null) { + execCommand = NARUtil.PlatformUtil.execCommand(s); + } + return execCommand; + } +``` +From `net.juniper.tnc.client.NARPlatform.linux.LinuxNARPlatform`: +``` + @Override + public String execCommand(final String s) { + String output = null; + try { + final Process exec = Runtime.getRuntime().exec(s); + final CommandOutputThread commandOutputThread = new CommandOutputThread(exec.getInputStream()); + final CommandOutputThread commandOutputThread2 = new CommandOutputThread(exec.getErrorStream()); + commandOutputThread.start(); + commandOutputThread2.start(); + final int wait = exec.waitFor(); + if (wait != 0) { + this.logError("Command " + s + " failed; return = " + wait + "; error output = " + commandOutputThread2.getOutput()); + } + output = commandOutputThread.getOutput(); + } + catch (Exception ex) { + this.logException(ex); + } + return output; + } +``` +As we can see, the `NARUtil.execCommand()` function is just a wrapper around `Runtime.getRuntime().exec()`. +## Full Chain +An attacker who is in a position where he can perform a Man in the Middle attack may spoof the server and send a malicious cookie along with an impossible to comply policy. An example cookie could be any method of command injection on linux (ex: `; sleep 20;`, `$(sleep 20)`, `\nsleep 20`, etc.). The client will then fail to comply with the policy and execute the command with the appended value when trying to show remediation instructions. +### DNS Rebinding (Bonus) +The Host Checker is controlled using a command socket. By default, when the Host Checker is started, it opens a socket using `ServerSocket(0)` which will automatically choose a port to listen on all interfaces. The selected port will then be writte to `~/.pulse_secure/narport.txt`. +The code prevents sending commands froma non local host but apart from that doesn't have any other authentication mechanism. An attacker may [brute force the port using JavaScript](https://portswigger.net/research/exposing-intranets-with-reliable-browser-based-port-scanning) or if in the same network directly ports can since it is listening on all interfaces. Once the ports is known, a DNS Rebinding attack can be done and commands can be sent to the socket. While this does not imply a command execution per se, one of the supported commands is `setcookie` which sets the cookie used for the command injection descripted in the paragraph above. +Note: the command socket expect a command to start at the beginnig of the first line but will try to parse up to 25 invalid commands before exiting, so a GET or a POST requests should work. diff --git a/tncc.jar b/tncc.jar new file mode 100644 index 0000000..6e07dba Binary files /dev/null and b/tncc.jar differ