Page MenuHomePhabricator

CVE-2023-22912: CheckUser TokenManager insecurely uses AES-CTR encryption with repeated nonce, allowing an adversary to decrypt
Closed, ResolvedPublicSecurity

Description

CheckUser has this JWT token scheme. In it, there is an HS256 JWT token. The payload of this token is a base64 encrypted blob that is encrypted in either AES-256-ctr or AES-256-cbc depending on how php is compiled (Probably safe to assume its always CTR mode).

The nonce for this encryption is randomly generated, and then stored in the user's session and used for all subsequent encryptions/decryptions of the session.

This is very insecure as CTR mode requires that nonces are only ever used once.

If you have two ciphertexts C₁ and C₂ encrypted with AES-CTR and the same nonce then C₁ ⊕ C₂ = P₁⊕ P₂. So you now have the XOR of two plaintexts. Given that much of the plaintexts are predictible (i.e. they are of the form {"reason":"REASON HERE","targets":[Target1"],"offset":"20220420100831|49"} ) and not byte aligned, it is probably very easy to decrypt the ciphertext if you have two of them in the average case.

For example:

Consider the following two JWT tokens (You can use https://jwt.io/ to extract the payload):

  1. eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NjA0NDU1MTAsImRhdGEiOiJaSENZMWJabUVNUEZUMUJcL0RTalRLdngzNlgyR0ZlNjJ4MFRuRTRFXC9aeEZjamtEQ2xhdVUifQ.g7lu81qgItJ5iVg0p0FSImRQJTyXxGz410QB5Jz7bfg
  2. eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NjA0NDU1NDksImRhdGEiOiJaSENZMWJabUVNUEZUMUJcL0RTalRPdko1djJpVkFPeW53QlhcL2N2aGZOMVFFekJhS2g5allKVXFkIn0.GyF6a0j3lP6sDo9eSwQsusmS7MYxttyWKnP3KvdRiYc

The XOR of their payload (in hex) is (You can use https://gchq.github.io/CyberChef/ or other tools to calculate this):
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 10 0e 0e 56 15 13 15 02 11 07 51 18 61 79 60 50 45 58 42 56 48 12 73 4c 41 3a 05

We notice they start to diverge 16 bytes in. If we assume that the reason field was different lengths (in this example they share a prefix), then around that point, one of them should have a known value of ","targets":[ where one plaintext is still in the reason field, but the other has moved on to the next json value. XORing ","targets":[" starting at byte 16 we get [1]:

2","targets":[

So we've now decrypted that one of the ciphertexts had a reason field that was the prefix of the other one, but with a 2 at the end.

We can also do this in the other direction, XORing 2","targets":[" and we get ","targets":["B, revealing that one of the targets username starts with B. We can try ","targets":["B."]}, ","targets":["B.."]} and so on, until we get a "]} in order to determine the length of the username (Or in this case, we run out of bytes) - so we know . We can then just try every username starting with B until we find something that decrypts to a valid username (Or set, we might not get 1, but it should narrow things down considerably). With more than two ciphertexts this process will become easier, and a script could be written to do it automatically. We can assume that the user is probably active in the wiki being checked (otherwise what is the point of checkusering them). On mediawiki.org for example, Special:ActiveUsers only lists 40 active users whose username starts with B. If we put in ","targets":["Bawolff"]} in as a guess, we get a decryption of 2","targets":["127.0.0.1. ( Full result in cyberchef)


Anyways, in conclusion, it is very insecure to reuse nonces with AES-CTR encryption.

If we really need to use AES-CTR, the nonce should be regenerated on every encryption and stored with the ciphertext, not on the server.

However, I would question whether this is the best approach. It seems like just using POST instead of GET would solve all the problems this code is trying to solve and be much simpler.

If encryption is really needed, since we are already using JWT, perhaps use encrypted JWTs instead (using a library to do it) so as to avoid us rolling our own encryption.

Given that we set a referrer policy of origin-when-cross-origin (And browsers now have good defaults if not set), the worst case here is probably someone reading log files, so not a very big impact.

Event Timeline

It seems like the background context is T239680

Mstyles triaged this task as Medium priority.Aug 16 2022, 4:34 PM
Mstyles added a project: Anti-Harassment.
Mstyles moved this task from Incoming to In Progress on the Security-Team board.
Mstyles changed Risk Rating from N/A to Medium.Sep 6 2022, 4:09 PM

Easiest fix would be something like

However, I would question whether this is the best approach. It seems like just using POST instead of GET would solve all the problems this code is trying to solve and be much simpler.

Is there a reason you didn't go in this direction?

However, I would question whether this is the best approach. It seems like just using POST instead of GET would solve all the problems this code is trying to solve and be much simpler.

Is there a reason you didn't go in this direction?

When I started looking more deeply into the code, it looked like it would be difficult to do that well still using Pager subclasses. Probably something that could be worked around, but it seemed like it would involve writing a lot of additional code.

This is also made use of by Special:CheckUser, but it uses this value only in POST requests.

@Niharika do you think someone from your team could take a look at this patch and give it a thumbs up? We just want to ensure this patch is okay with the team before merging it

@Bawolff this patch did not merge, even with a 3 way merge. I will be investigating further but wanted to let you know

Rebased version of patch if its helpful.

Thanks, @Bawolff. We'll try to get your rebased patch deployed within a week or two this time.

@Bawolff the rebased patched worked perfectly, thank you! this is now deployed in production on php-1.40.0-wmf.8

sbassett changed the task status from Open to In Progress.Nov 9 2022, 5:28 PM
sbassett moved this task from Security Patch To Deploy to Watching on the Security-Team board.
sbassett removed a project: Patch-For-Review.
Mstyles changed the visibility from "Custom Policy" to "Public (No Login Required)".Jan 12 2023, 3:05 AM
Mstyles changed the edit policy from "Custom Policy" to "All Users".

Change 879074 merged by jenkins-bot:

[mediawiki/extensions/CheckUser@master] SECURITY: Always use a unique nonce/IV for AES-CTR encryption.

https://gerrit.wikimedia.org/r/879074

Change 879534 had a related patch set uploaded (by Zabe; author: Brian Wolff):

[mediawiki/extensions/CheckUser@REL1_39] SECURITY: Always use a unique nonce/IV for AES-CTR encryption.

https://gerrit.wikimedia.org/r/879534

Change 879534 merged by jenkins-bot:

[mediawiki/extensions/CheckUser@REL1_39] SECURITY: Always use a unique nonce/IV for AES-CTR encryption.

https://gerrit.wikimedia.org/r/879534

Change 879566 had a related patch set uploaded (by Zabe; author: Brian Wolff):

[mediawiki/extensions/CheckUser@REL1_38] SECURITY: Always use a unique nonce/IV for AES-CTR encryption.

https://gerrit.wikimedia.org/r/879566

Change 879568 had a related patch set uploaded (by Zabe; author: Brian Wolff):

[mediawiki/extensions/CheckUser@REL1_35] SECURITY: Always use a unique nonce/IV for AES-CTR encryption.

https://gerrit.wikimedia.org/r/879568

Change 879566 merged by jenkins-bot:

[mediawiki/extensions/CheckUser@REL1_38] SECURITY: Always use a unique nonce/IV for AES-CTR encryption.

https://gerrit.wikimedia.org/r/879566

Change 879568 merged by jenkins-bot:

[mediawiki/extensions/CheckUser@REL1_35] SECURITY: Always use a unique nonce/IV for AES-CTR encryption.

https://gerrit.wikimedia.org/r/879568

Mstyles claimed this task.
mmartorana renamed this task from CheckUser TokenManager insecurely uses AES-CTR encryption with repeated nonce, allowing an adversary to decrypt to CVE-2023-22912: CheckUser TokenManager insecurely uses AES-CTR encryption with repeated nonce, allowing an adversary to decrypt.Jan 12 2023, 6:24 PM