168 lines
10 KiB
Markdown
168 lines
10 KiB
Markdown
# Juniper Host Checker Linux MITM RCE
|
|
## CVEs
|
|
|
|
- No certificate Validation - [CVE-2020-11580](https://nvd.nist.gov/vuln/detail/CVE-2020-11580)
|
|
- Command Injection - [CVE-2020-11581](https://nvd.nist.gov/vuln/detail/CVE-2020-11581)
|
|
- DNS Rebindig - [CVE-2020-11582](https://nvd.nist.gov/vuln/detail/CVE-2020-11582)
|
|
|
|
Link to Juniper official advisory [SA44426](https://kb.pulsesecure.net/articles/Pulse_Security_Advisories/SA44426)
|
|
|
|
## Intro
|
|
The Host Checker is a client side component that the [Pulse Connect Secure](https://www.pulsesecure.net/products/pulse-connect-secure/) appliance 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 work 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 (or it can be done via DNS Rebinding), 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
|
|
The client implement a custom protocol in order to talk to the server. For further reference, the [open source client](https://raw.githubusercontent.com/russdill/juniper-vpn-py/master/tncc.py) has reverse engineered and implemented the same protocol. The file ```tncc.jar``` is not obfuscated in any way and the originalk source code can be obtained with almost any Java decompiler.
|
|
|
|
### Certificate
|
|
Below are some extracts of code from the classes that handle the connection with the Pulse Connect Secure appliance.
|
|
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).
|
|
The following code updates the DSPREAUTH cookie when sending periodic updates to the server. Periodic updates may or may not be required depending on the policy configuration.
|
|
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
|
|
When a client is found to be non compliant, remediation instructions have to be shown to the user in order to give him a chance to fix his problems.
|
|
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.
|