Zoom meetings were default protected by a 6 digit numeric password, meaning 1 million maximum passwords. I discovered a vulnerability in the Zoom web client that allowed checking if a password is correct for a meeting, due to broken CSRF and no rate limiting.
This enabled an attacker to attempt all 1 million passwords in a matter of minutes and gain access to other people’s private (password protected) Zoom meetings.
This also raises the troubling question as to whether others were potentially already using this vulnerability to listen in to other people’s calls (e.g. the UK Cabinet Meeting!).
I reported the issue to Zoom, who quickly took the web client offline to fix the problem. They seem to have mitigated it by both requiring a user logs in to join meetings in the web client, and updating default meeting passwords to be non-numeric and longer. Therefore this attack no longer works.
On March 31st, Boris Johnson tweeted about chairing the first ever digital cabinet meeting. I was amongst many who noticed that the screenshot included the Zoom Meeting ID. Twitter was alive with people saying they were trying to join, but Zoom protects meetings with a password by default (which was pointed out when the Government defended using Zoom).
Having also tried to join, I thought I would see if I could crack the password for private Zoom meetings. Over the next couple of days, I spent time reverse engineering the endpoints for the web client Zoom provide, and found I was able to iterate over all possible default passwords to discover the password for a given private meeting.
After trying to join the Cabinet Meeting, I poked about in the Zoom app and noticed the default passwords being 6 digits and numeric, meaning 1 million maximum passwords.
A fairly standard principle of password security is to rate limit password attempts, to prevent an attacker from iterating over a list of candidate passwords and trying them all. I assumed that Zoom would be doing this, but decided to double check.
I decided to target Zoom’s web client, but my findings apply to meetings initiated and conducted via all versions of the app too.
Meeting Login Flow
When a user creates a new meeting, Zoom auto generates a link for people to join, in the form (dummy data below):
It contains both the meeting ID and the auto generated password. I believe this password is a hashed version of the 6 digit numeric password, but I also found that swapping it out for the 6 digit numeric version was acceptable to the web client endpoints, so we could ignore the hashed version and concentrate on the numeric version.
If you remove the
pwd parameter then visit the web client join link (
https://zoom.us/wc/join/618086352) then you will see a login screen:
This seemed to fire off an XHR GET request then take you to another page.
Breaking down the flow behind the scenes
There were several things going on as you move through this flow:
- When you first open any web client page, without an existing cookie, a cookie is set which, amongst other bits, contains a GUID. This seems to be your anonymous user ID.
- If you fill in the user/pass form but haven’t completed the privacy agreement you are redirected to it. Completing it was a simple GET request to a given endpoint, which contains your GUID. There was a CSRF HTTP header sent during this step, but if you omitted it then the request still seemed to just work fine anyway.
- The redirect would take you to a new page, which seems to know server side whether your GUID has previously entered the correct password. i.e. The previous step stored state server side marking whether you got the password correct.
The failure on the CSRF token made it even easier to abuse than it would be otherwise, but fixing that wouldn’t provide much protection against this attack.
This process was a little convoluted to automate, which is maybe why this endpoint had not been scrutinised in detail before. There are some details I’ve skipped over, such as parameters that need to be saved from one request to another, but they are not important to understanding the main issue.
The important thing to note about the above process is that there was no rate limit on repeated password attempts (each comprising of 2 HTTP requests – one to submit the password, and follow up request to check if it was accepted by the server). However, the speed is limited by how quickly you can make HTTP requests, which have a natural latency which would make cracking a password a slow process; the server side state means you have to wait for the first request to complete before you can send the second.
However, we should note that the state was stored against the provided GUID, and you can ask the server for as many of those as you want by sending HTTP requests with no cookie. This means we could request a batch of GUIDs and then chunk the 1 million possible passwords up between them and run multiple requests in parallel.
I put together some (fairly clunky) Python that requests a batch of GUIDs then spawns multiple threads so they can run requests in parallel. An initial test running from my home machine with 100 threads:
Passwords tried: 43164
took 28m 52s 392ms
We can see we are checking about 25 passwords a second, and discovered the password (in this example I knew the password so had bounded my search). I ran a similar test from a machine in AWS and checked 91k passwords in 25 minutes.
With improved threading, and distributing across 4-5 cloud servers you could check the entire password space within a few minutes. This would be fairly simple to do, but I resisted as I had demonstrated the process and wanted to be cautious not to interrupt Zoom’s service (I did do some short higher rate tests and never got throttled or blocked).
Note also that the expected time to find a password would be shorter, as you would not normally need to search the entire list of possible passwords.
Also note that recurring meetings, including ‘Personal Meeting IDs (PMIs)’ always use the same password, so once it is cracked you have ongoing access.
The initial version of my attack could only be run once a meeting started, but I later found that the DOM for un-started indicated whether the password was correct vs incorrect, meaning you could crack scheduled meetings too.
Zoom Password Issues
Firstly, note that whilst it doesn’t seem possible to change the 6 digit numeric password for spontaneous meetings, it is possible to override it for scheduled meetings, but is an explicit step to change the default password provided. I checked about 20 Zoom meeting invites I’ve received in the past, from various people, and found they all used the default 6 digit password.
If you do override the password and produce a longer alphanumeric password, then a 6 digit numeric password may be produced anyway for phone users. This password is not accepted, at least on the endpoint I was trying for the web client. I’m not sure if this is true for other endpoints – I didn’t check.
Also note that if the password was to be updated to alphanumeric, I estimate you could still run across a password list of, say, the top 10 million passwords in less than an hour.
In other testing, I found that Zoom has a maximum password length of 10 characters, and whilst it accepts non-ASCII characters (such as ü, €, á) it converts them all to ? after you save the password.
Could someone have eavesdropped on the UK Cabinet Meeting?
Lastly, I noted in Boris Johnson’s screenshot, that there is a user called simply ‘iPhone’ (see bottom right) that is muted with the camera off:
It got me wondering whether this flaw has previously been found — if I could discover it then it seems plausible that others could too, which makes this bug particularly worrisome.
The high level recommendations I passed on to Zoom for fixing this were:
- Rate limit GUIDs to a reasonable number of password attempts (e.g 10 [different] failed attempts in an hour for a given meeting)
- Rate limit IP addresses, irrespective of GUID, for password attempts (irrespective of meeting ID)
- Rate limit or trigger a warning should a given meeting pass a set failure rate for failed password attempts
- Fix the CSRF on the Privacy Terms form, so it is harder to automate attacks.
- Increase the length of the default password.
As far as I can tell (Zoom hasn’t given me any insight into what they did to mitigate the issue), it seems Zoom has made a couple of changes:
- Started enforcing sign-in for users joining meetings via the web client; it is unclear if this is a permanent change or not (it is a problem for some users as I understand).
- Updated default passwords to be alphanumeric instead. This seems to be in some phased rollout as I’m still sometimes seeing numeric only passwords.
I reported the issue to Zoom directly, and they quickly took the whole web client offline for a few days whilst they triaged the issue, it came up again a few days later.
I’m aware Zoom have been under a lot of scrutiny for their security practices given their sudden spike in usage brought about by the COVID-19 pandemic. From my interactions with the team, they seemed to care about the security of the platform, and their users and they seemed appreciative of the report.
Zoom run a private, invite only, bug bounty program, which is a fairly common practice for lots of organisations. I was invited to submit this bug to the bug bounty program, but I asked to wait as I was interested in the new bug bounty program they were working on. I wondered if the new program rules would guarantee consent for disclosure, given I felt this was a bug of public interest. Zoom agreed I could submit the bug under the new program when it was launched.
Zoom have since released the results of their 90 day security sprint, and commitment #4 on that includes updates to their bug bounty program. Hopefully that is coming soon, but I didn’t want to wait in disclosing the bug (they had agreed to disclosure), given it has been fixed for a while.
I did submit a couple of other small bugs via the private program on HackerOne, and received bounties for those. Thanks Zoom team! 🙂
It was surprising to me that there was a lack of rate limiting on the central mechanism of the platform, which combined with a poor default password system and faulty CSRF meant that meetings were really not secure.
However, Zoom’s response was fast, and they quickly addressed the rate limiting issue. Zoom meetings also got a default password upgrade, which is great.
Zoom’s ease of use and video conferencing quality have made it a hugely valuable tool for millions of people over the last few months, during what is a tough time. Many (most?) are using it entirely for free. That is a great thing, and I’m grateful Zoom exists. Thanks Zoom team!
- 1st April – I reported the issue to Zoom, with a working Python POC. I sent this via their generic support form, and via email.
- 2nd April – I followed up with a draft of this post as additional explanation, and a better commented version of the Python code. I tweeted at Zoom to ask about a status, and in DMs with them passed on the ticket number.
- 2nd April – Heard from their team they were looking (this was about 24 hours following my report), then received a follow up from Zoom’s CISO.
- 2nd April – Noted that the Zoom Web Client was offline, returning a 403. This also affected the web SDK.
- 9th April – Heard from the Zoom team that this was mitigated.
- 16th April – Heard they were working on updated bug bounty program.
- 15th June – Requested update on BB program. No reply.
- 8th July – Asked again if I could submit this for bounty. No reply. (Point of clarity here – the bug is fixed, and they have new issues to deal with so this isn’t exactly a priority for them. I could have chosen to file the bug for a bounty at the time, but didn’t, and wasn’t promised anything if I waited).
- 29th July – Disclosure.
Update edit: A few people have asked me or remarked about the lack of bounty. To be clear, I never actually submitted this bug via their bounty program (but was invited to do so), as was holding out for their new program (see post), and fell down the cracks a bit. Zoom didn’t decide against awarding a bounty – I never submitted for one, and disclosed here instead.