Privilege Escalation with cPanel API Calls
Overview
This section contains information about a temporary escalation of a cPanel user's privileges so that an API call can execute code as
root. This document also provides a sample plugin that contains examples of the concepts described within the article.
cPanel handles privilege escalation by providing a
setuid wrapper that a cPanel user can execute. While providing root privileges, the wrapper also ensures that the user can only run permitted code.
Our
example package provides all of the relevant code and information for building a privilege escalation system. You should
never use this package on a production system. Read the
README file you view and use this package. The
README file contains important information regarding the distribution and reuse of the code we have provided.
Key components
- The
admin script — This script contains the code which the cPanel user can execute as root.
- A wrapper binary — This binary is built from
/usr/local/cpanel/src/wrap/wrap.c. The binary handles 3 tasks:
- It is responsible for a user's privilege escalation.
- It ensures that the parent process which executes the binary is a cPanel process.
- It passes data between the
admin script and the process that makes the API call.
- The
Cpanel::AdminBin module — This Perl module is located at /usr/local/cpanel/Cpanel/AdminBin.pm and calls the wrapper binary and the data it returns in a useful format.
Naming conventions
The name of your
admin binary and wrapper script must follow a naming convention. You must prefix
admin and
wrap with an identifier.
If you were to create an
admin binary and wrapper script called
example, you would name the files
exampleadmin and
examplewrap.
Cpanel::AdminBin
This Perl module passes information between the wrapper scripts and the system processes that runs the code.
For this tutorial, we will focus on 2 functions. However, there are more functions you can use within the module (
/usr/local/cpanel/Cpanel/AdminBin.pm).
The
adminrun() function is used to return strings from the admin binary:
adminrun($name, $cmd, @args )
| Parameter |
Description |
| $name |
The identifier prefixed to the admin script's filename. |
| $cmd |
The command to pass to the wrapper. |
| @args |
A list of arguments for the command. |
For example, if you want to create a wrapper script named
file and a function named
READ that takes path argument via
adminrun(), it would resemble the following:
my $file_contents = Cpanel::AdminBin::adminrun('file', 'READ', $OPTS{'path'});
This executes
/usr/local/cpanel/bin/filewrap as
setuid root and writes
READ $path to it via STDIN. The filewrap binary executes
/usr/local/cpanel/bin/fileadmin and writes
$uid READ $path to STDIN.
The
adminfetchnocache() function is used for returning data structures from the admin script:
adminfetchnocache($name, $cachefile, $cmd, $format, @args)
| Variable |
Description |
| $name |
The identifier prefixed to the admin script's filename. |
| $cachefile |
The location of the cache file to use. Because we are using a nocache version in this method, we will pass '' here. |
| $format |
The format used for communicating between the admin script and calling code. We strongly recommend using storage here. |
| $cmd |
The command to pass to the wrapper. |
| @args |
A list of arguments for the command. |
For example, if you want to implement the ability to retrieve a data structure representing a directory as
root via
adminfetchnocache, it would resemble the following:
my $dir_listing_hr = Cpanel::AdminBin::adminfetchnocache('file', '', 'LS', 'Storable', $opts{'path'});
For version 11.36: If you wanted to implement the ability to retrieve a data structure that represents a directory as
root via
adminfetchnocache, it would resemble the following:
my $dir_listing_hr = Cpanel::AdminBin::adminfetchnocache('file', '', 'LS', '', $opts{'path'});
You can find examples of how to use this code in the
Test.pm file, provided in the example plugin.
The admin script
This script contains the code that will run under the
root user. The script takes data from STDIN, prints data to STDOUT, and is executed by the wrapper binary. This script must follow the naming convention mentioned in the
naming conventions section. You must place this script in
/usr/local/cpanel/bin/ with executable permissions.
Data written to STDIN must follow the following format:
$uid $cmd $args
| Variable |
Description |
| $uid |
The user ID (UID) of the user calling the wrapper script. This value is automatically prepended by the admin binary. |
| $cmd |
The command to execute |
| $args |
Any arguments to pass to the command |
You can see the logic behind this script in the
testadmin file provided in the sample code.
The testadmin script
Our sample package, included alongside this tutorial, contains the
testadmin script. This script contains the actual logic that executes as
root. Most of the code contained within this script is boilerplate.
You will mostly be concerned with the command hash that contains subroutines that will execute as
root. For example:
my %commands = (
'LS' => sub {
# pull in the values that were in @args
my ($dir) = @_;
# Sanitize our input
if ( !defined $dir || $dir eq '' ) {
print "Directory not defined\n";
exit;
}
if ( !-d $dir ) {
print "provided directory does not exist\n";
exit;
}
my @files;
# perform the action
opendir( my $dir_dh, $dir ) || die "Can't open directory: $dir";
#build our data structure
foreach my $file ( readdir $dir_dh ) {
push @files, $file;
}
closedir($dir_dh);
# print out in Storable format
Storable::nstore_fd( \@files, \*STDOUT );
},
For version 11.36:
my %commands = (
'LS' => sub {
# pull in the values that were in @args
my ($dir) = @_;
# Sanitize our input
if ( !defined $dir || $dir eq '' ) {
print "Directory not defined\n";
exit;
}
if ( !-d $dir ) {
print "provided directory does not exist\n";
exit;
}
my @files;
# perform the action
opendir( my $dir_dh, $dir ) || die "Can't open directory: $dir";
#build our data structure
foreach my $file ( readdir $dir_dh ) {
push @files, $file;
}
closedir($dir_dh);
# print out in YAML format
Cpanel::YAML::Dump(\@files);
},
Warning: You should heavily sanitize and check
all of the input for these scripts. Any vulnerability inside of these scripts is a root code execution vulnerability.
For version 11.34 and earlier
The most important piece of this code is the last line. It prints a Storable file to STDOUT, allowing you to pass a data structure back to the subroutine that initiated the call. When using this file, you must call it via the
adminfetch or
adminfetchnocache AdminBin functions.
Storable::nstore_fd( \@files, \*STDOUT );
For version 11.36
Version 11.36 prints a YAML file to STDOUT, allowing you to pass a data structure back to the subroutine that initiated the call. When you use this file, you must call it via the
adminfetch or
adminfetchnocache AdminBin functions
Cpanel::YAML::Dump(\@files);
This bit of code is used to return the data structure to the AdminBin module, which in turn will pass it back to your code. It prints the provided data structure (
@files) to STDOUT in the Perl Storable format.
Version 11.36 prints the provided data structure (
@files) to STDOUT in the YAML format.
For both version 11.34 and earlier, and version 11.36
To make your plugin compatible with both cPanel & WHM version 11.34 and earlier
and cPanel & WHM version 11.36, use the following
if structure:
use Cpanel::Version::Tiny ();
use Cpanel::Version::Compare ();
use Cpanel::YAML ();
if (Cpanel::Version::Compare::compare_major_release($Cpanel::Version::Tiny::VERSION,'<','11.36')) {
Storable::nstore_fd( \@files, \*STDOUT );
} else {
print Cpanel::YAML::Dump(\@files);
}
Note: Anything sent to STDOUT is sent back to the script.
The wrapper binary
The wrapper binary handles escalation privileges. It is a
setuid binary placed in
/usr/local/cpanel/bin/.
You can find the source code for cPanel's
wrap.c in
/usr/local/cpanel/src/wrap/wrap.c. You can modify and distribute this code for use with cPanel systems under a few conditions:
- It is distributed for non-commercial use only.
- If distributed for commercial use, prior written approval must be given by cPanel, Inc (contact copyright@cpanel.net for details).
- An uncompiled version of this code is publicly available.
- It is understood that cPanel, Inc will not provide support for modified versions of this code.
- All copyright notices are left intact in the modified version.
- Contact information for the maintainer of the modified version is contained within the source.
When modifying this binary for use with cPanel systems, you will want to change the
exec_list array to point to your
adminscript and use a unique identifier. For example:
struct executable_properties exec_list [1] = {
{ "testwrap", "/usr/local/cpanel/bin/testadmin", "root", "wheel", 1, 0 }
};
After you make the appropriate changes the code above should resemble the following:
struct executable_properties exec_list [1] = {
{ "myappwrap", "/usr/local/cpanel/bin/myappadmin", "root", "wheel", 1, 0 }
};
You will also need to change the
PROG_NAME variable in the Makefile. You can find the Makefile in the package distributed alongside this documentation.
Security concerns
This section contains information about what you should and should not allow when allowing users to execute privileged code. A poor implementation of the
cpwrap system can lead to
root privilege vulnerabilities.
You should take care to:
- Only execute code that must run as
root.
- Make sure all of the input passed to this system is validated.
- Sanitize the environment of the
admin script.
You should
not:
- Manipulate files and directories under a user's control as
root.
- Execute actions on unvalidated input. This includes user-controlled files, environment variables, incoming parameters, etc.
Environment sanitation
You will need to sanitize the
admin script's environment. You should set the environment variable to limit paths from which libraries are loaded. This sanitizes @INC to prevent users from loading arbitrary libraries via paths they can control.
For example:
BEGIN {
unshift @INC, '/usr/local/cpanel';
@INC = grep( !/(^\.|\.\.|\/\.+)/, @INC );
}
This block of code adds
/usr/local/cpanel to the environment, then removes entries that don't match standard Perl library paths.