Software Development Kit

cPanel & WHM's API [+] cPanel & WHM's API [-]


Modules and Plugins [+] Modules and Plugins [-]


cPanel & WHM Hooks [+] cPanel & WHM Hooks [-]


cPAddons (Site Software) [+] cPAddons (Site Software) [-]


System Administration [+] System Administration [-]


Developer Software [+] Developer Software [-]


Back to All Documentation

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

  1. The admin script — This script contains the code which the cPanel user can execute as root.
  2. 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.
  3. 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);
	},

ALERT! 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 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:

  1. It is distributed for non-commercial use only.
  2. If distributed for commercial use, prior written approval must be given by cPanel, Inc (contact copyright@cpanel.net for details).
  3. An uncompiled version of this code is publicly available.
  4. It is understood that cPanel, Inc will not provide support for modified versions of this code.
  5. All copyright notices are left intact in the modified version.
  6. 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:

  1. Only execute code that must run as root.
  2. Make sure all of the input passed to this system is validated.
  3. Sanitize the environment of the admin script.

You should not:

  1. Manipulate files and directories under a user's control as root.
  2. 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.

Topic revision: r14 - 03 Apr 2013 - 14:55:26 - Main.LaurenceSimon
DeveloperResources.PrivilegeEscalation moved from Sandbox.PrivilegeEscalation on 21 Oct 2010 - 17:30 by Main.JustinSchaefer