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:
# `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NjA0NDU1MTAsImRhdGEiOiJaSENZMWJabUVNUEZUMUJcL0RTalRLdngzNlgyR0ZlNjJ4MFRuRTRFXC9aeEZjamtEQ2xhdVUifQ.g7lu81qgItJ5iVg0p0FSImRQJTyXxGz410QB5Jz7bfg`
# `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NjA0NDU1NDksImRhdGEiOiJaSENZMWJabUVNUEZUMUJcL0RTalRPdko1djJpVkFPeW53QlhcL2N2aGZOMVFFekJhS2g5allKVXFkIn0.GyF6a0j3lP6sDo9eSwQsusmS7MYxttyWKnP3KvdRiYc`
The XOR of their payload (in hex) is:
`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:
`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). 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. Perhaps one can narrow down by who is recently being sus. If we don't trust that Bawolff user, and put `","targets":["Bawolff"]}` in as a guess, we get a decryption of `2","targets":["127.0.0.1`.
-----
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.