This blog discusses a command injection vulnerability in a built-in macOS Perl script that could allow for LPE under certain conditions. I reported this vulnerability to Apple and they did not deem this a security issue, so the most recent version of macOS (Tahoe 26.2) is currently still vulnerable.

Stumbling Upon Injection

I actually laid the ground work (unintentionally) for finidng this vulnerability while researching how macOS handles special characters in newly created usernames, and only actually discovered the vulnerability while researching the ARDAgent.app.

I had created a local macOS user named myu$(ls)ser a couple months ago and had forgotten to delete it. While researching the ARDAgent.app I was running the Perl script located at /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart when I noticed the below error being outputted everytime I ran the script, no matter what flags I gave it.

sh: ls: No such file or directory
<dscl_cmd> DS Error: -14136 (eDSRecordNotFound)

That’s when I started to dig a little deeper into the kickstart script to figure out what was causing that error.

Understanding the kickstart Script

The kickstart script is used for managing and configuring Remote Management options in macOS. It has a lot of different configuration options, including enabling/disabling Remote Management, configuring VNC access, installing software, and setting access/permissions levels for specific users for remote access. To control remote access permissions for specific users, the kickstart script leverages the /usr/bin/dscl command to modfiy naprivs of specific or all users depending on the given configuration flags. Before modifying the permissions, the kickstart script first sets up the dscl command by setting the DSLocal database for all local users:

my $DSLocalDB = "${TargetDisk}var/db/dslocal/nodes/Default";
die "Can't locate DSLocal database: $DSLocalDB\n" . Usage1() unless -e $DSLocalDB;

$serviceCmd = "/usr/bin/dscl -f '$DSLocalDB' localonly";
$databasePath = "/Local/Target/Users";

The kickstart script then uses the set DSLocal database to read all local users and store them in an array. It does this by executing the $serviceCmd dscl command and grepping all users with uid > 500:

## Find all the active local users of the system (except common built-in users)

my $FoundUsers = "";
$FoundUsers     = [map {(m{(\S+)\s+})[0]} `$serviceCmd -list $databasePath`];

my $FoundUsersHash  = {map {($_ => $_)} @$FoundUsers};
my $AllUsers        = [grep {!m{^(root|nobody|daemon|unknown|smmsp|www|mysql|sshd|lp|sendmail|postfix|eppc|qtss|cyrus|mailman)$ }x} @$FoundUsers ];
$AllUsers           = [grep {(`$serviceCmd -read "$databasePath/$_" uid` =~ m{(\d+)})[0] > 500} @$AllUsers];

Identifying the Injection

That last line where $serviceCmd is executed is where the vulnerability lies:

$AllUsers = [grep {(`$serviceCmd -read "$databasePath/$_" uid` =~ m{(\d+)})[0] > 500} @$AllUsers];

The "$databasePath/$_" uses " instead of ' which makes it vulnerable to command injection, as any bash syntax within $databasePath or $_ will be treated as bash instead of as an escaped string. The $databasePath is statically set in the kickstart script and can’t be controlled, so no opportunity of command injection there. But $_ is a different story.

$_ represents the local user name retrieved from /var/db/dslocal/nodes/Default/users. If we take a look at that directory on my machine, we’ll see the local user I created months prior:

-rw-------    1 root  wheel  108790 Jan  6 09:38 myu$(ls)ser.plist

When the kickstart script gets to this user, the command that gets executed will look like:

$AllUsers = [grep {(`$serviceCmd -read "$databasePath/myu$(ls)ser.plist" uid` =~ m{(\d+)})[0] > 500} @$AllUsers];

The $(ls) in my created username will be treated as bash and executed, confirming command injection!

This also isn’t the only line in the kickstart script that is vulnerable to command injection (lines 750, 819, and 838 are too!), but this is the one that will get executed no matter what flags you pass to the kickstart script upon execution. You could give it the -h help flag or no flag at all and the above line is guaranteed to execute.

Making the Injection More Useful

Ok we confirmed command-injection, but that still doesn’t explain the error message. ls is a command that exists in all unix operating systems, especially macOS, so why did we get the sh: ls: No such file or directory error whenever we run the kickstart script?

Well it’s because the kickstart script actually clears the PATH environment variable before it does anything else:

## Make sure perl will run us setuid by untainting PATH.
$ENV{PATH} = '';

So any command that isn’t referenced using its full path (e.g. /bin/ls) will return a No such file or directory error. But macOS (and all unix-based OS’s) restrict the use of / characters in usernames, so how can we get a more useful command injection that actually does something?

Well we can use environment variables in the usernames instead. Below is a simple proof of concept that creates a local user with an environment variable in the username that contains a bash command. We could also place a bash script file in the environment variable to execute more complex commands/scripts if we wanted.

sudo dscl . -create /Users/'myu$(${POC})ser'

export POC="/usr/bin/touch /tmp/poc.txt"

sudo -E /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart -h

ls -la /tmp/poc.txt

Something else I should mention is that the kickstart script will only run as root as shown in the below line, which means this command injection will always been run as root too.

## Don't do anything if not running as root.
if ($> != 0) {die "$0 must be run as root or sudo ($>).\n";}

You’ll notice in the proof of concept above that sudo privileges are required to create a local user, which is a limitation of exploiting this vulnerability. However, there are legitimate scenarios where an attacker can escalate privileges from a limited sudo-context to full root. For example, if the attacker has access to a local user with the below rule in their /etc/sudoers file, it could be used to create the command injection payload.

johndoe ALL=(root) /usr/bin/dscl

I think this scenario is possible, specifically for management-type service accounts, but if anyone else has other ideas for legitimate attack scenarios, or ways to create a local user without sudo, send me a DM on Twitter/X!

Conclusion

I reported this command injection vulnerability to Apple and they decided it wasn’t a security issue due to the sudo requirement to create a local user, despite the fact that the kickstart script is still vulnerable to command injection, and would be as simple to fix as replacing " with '.

Either way, I thought it was an interesting vulnerability worth sharing. I’ll also keep that myu$(ls)ser on my system in case it accidentally identifies more command-injection vulnerabilities!