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 document contains information about a temporary escalation of a cPanel user's privileges so that an API call can execute code as the root user. This document also provides a sample plugin that contains examples of this process.

cPanel manages privilege escalation via a setuid wrapper that a cPanel user can execute. While this wrapper provides root privileges, the wrapper also ensures that the user can only run permitted code.

Before cPanel & WHM version 11.38, this system required users to construct admin scripts and directly manipulate data constructs that passed in and out of the wrapper. In 11.38, cPanel introduced a pluggable wrapper system that allows users to write simpler and more secure functions that can run with temporary elevated privileges.

Security concerns

The information below describes what system administrators should and should not do when they allow users to execute privileged code. A poor implementation of this system can lead to root privilege vulnerabilities.

System administrators should:

  • Only execute code that must run as root.
  • Certify that the input that code passes to this system is validated.
  • Sanitize the environment of the admin script.

System administrators should not:

  • Manipulate, as root, files and directories that are under a user's control.
  • Execute actions on input that has not been validated. These actions include the execution of user-controlled files, environment variables, and incoming parameters.

Environment sanitation

The script's environment will need to be sanitized. You should set the environment variable to limit the paths from where libraries are loaded. This action sanitizes the @INC array, and users will not have the ability to load arbitrary libraries via the paths that they can control.

For example, the following block of code adds the /usr/local/cpanel file to the environment, then removes entries that do not match standard Perl library paths.


BEGIN {
    unshift @INC, '/usr/local/cpanel';
    @INC = grep( !/(^\.|\.\.|\/\.+)/, @INC );
}

Version 11.36 and Previous Versions

The downloadable example package provides all of the relevant code and information that is required to build a privilege escalation system. Read the README file in the downloadable example package to view and use this package. The README file contains important information about the distribution and how to reuse the provided code.

ALERT! Warning: System administrators should never use this package on a production system.

Key components of the privilege escalation system

  • The admin script — This script contains the code that the user can execute as root.
  • A wrapper binary — This binary is built from /usr/local/cpanel/src/wrap/wrap.c. This binary handles 3 tasks:
    • The binary is responsible for a user's privilege escalation.
    • The binary ensures that the parent process which executes the binary is a cPanel process.
    • The binary 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 that the wrapper returns in a useful format.

Naming conventions

The name of the admin binary and wrapper script follow a strict naming convention. System administrators must prefix admin and wrap with a unique identifier.

For example, if a user creates an admin binary and a wrapper script called example, then the files would be named exampleadmin and examplewrap.

Cpanel::AdminBin

This Perl module passes information between the wrapper scripts and the system processes that run the code.

In this example, users should focus on two functions. However, there are more functions that users can use within the module located at /usr/local/cpanel/Cpanel/AdminBin.pm

The adminrun() function returns strings from the admin binary. To call adminrun(), execute the following:

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, to create a wrapper script named file and a function named READ that takes path argument via adminrun(), the module will resemble the following:


my $file_contents = Cpanel::AdminBin::adminrun('file', 'READ', $OPTS{'path'});

This module executes /usr/local/cpanel/bin/filewrap as setuid root and writes READ $path to it through STDIN. The filewrap binary executes /usr/local/cpanel/bin/fileadmin and writes $uid READ $path to STDIN.

The adminfetchnocache() function returns data structures from the admin script. To call this function, execute the following:

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 cPanel uses a nocache version in this method, users will pass '' here.
$format The format used to communicate between the admin script and calling code. We strongly recommend that users use storage here.
$cmd The command to pass to the wrapper.
@args A list of arguments for the command.

For example, in 11.38, to implement the ability to retrieve a data structure that represents a directory as root via adminfetchnocache, the function will resemble the following:


my $dir_listing_hr = Cpanel::AdminBin::adminfetchnocache('file', '', 'LS', 'Storable', $opts{'path'});

In 11.36, to implement the ability to retrieve a data structure that represents a directory as root via adminfetchnocache, the function will resemble the following:


my $dir_listing_hr = Cpanel::AdminBin::adminfetchnocache('file', '', 'LS', '', $opts{'path'});

note Note: There are examples of how to use this code in the Test.pm file that the example plugin provides.

The admin script

This script contains the code that will run under the root user. The script takes data from STDIN and prints data to STDOUT and then the wrapper binary executes the data. This script must follow the naming convention mentioned in the naming conventions section. This script must be placed in /usr/local/cpanel/bin/ file with executable permissions.

Data written to STDIN must use the following format: $uid $cmd $args

Variable Description
$uid The user ID (UID) of the user who calls the wrapper script. The admin binary automatically prefixes this value.
$cmd The command to execute.
$args Any arguments to pass to the command.

Users can see the logic behind this script in the testadmin file provided in the sample code.

The testadmin script

The downloadable example package contains the testadmin script. This script contains the actual logic that executes as root. Most of the code contained within this script is standardized text.

note Note: The command hash contains the subroutines that will execute as root.

For example, in 11.34:


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) || die "Can't close directory: $dir";
        # print out in Storable format
        Storable::nstore_fd( \@files, \*STDOUT );
	},

In 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) || die "Can't close directory: $dir";
        # print out in YAML format
        print 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

You should note the last line of this code. This line prints a Storable file to STDOUT which allows users to pass a data structure back to the subroutine that initiated the call. When users use this file, they call the file via the adminfetch or adminfetchnocache AdminBin functions.


Storable::nstore_fd( \@files, \*STDOUT );

For version 11.36

In 11.36, the last line of the code prints a YAML file to STDOUT which allows users to pass a data structure back to the subroutine that initiated the call. When a user uses this file, the user must call the file via the adminfetch or adminfetchnocache AdminBin functions


# print out in YAML format
print Cpanel::YAML::Dump(\@files);

This piece of example code returns the data structure to the AdminBin module, which in turn will pass the data structure back to user's code. The last line of the code prints the provided data structure (@files) to STDOUT in the YAML format.

For versions 11.36 and earlier

To make the plugin compatible with cPanel & WHM version 11.36 and earlier, use the following if structure:

use Cpanel::Version::Tiny    ();
use Cpanel::Version::Compare ();
 
my @files = qw(example file list);
 
if ( Cpanel::Version::Compare::compare_major_release( $Cpanel::Version::Tiny::VERSION, '<', '11.36' ) ) {
    require Storable;
    Storable::nstore_fd( \@files, \*STDOUT );
}
else {
    require Cpanel::YAML;
    print Cpanel::YAML::Dump( \@files );
}

note Note: Information sent to STDOUT is sent back to the script.

The wrapper binary

The wrapper binary handles escalation privileges. It is a setuid binary located in the /usr/local/cpanel/bin/ directory.

The source code for cPanel's wrap.c is located in /usr/local/cpanel/src/wrap/wrap.c. Users can modify and distribute this code for use with cPanel systems if they meet the following conditions:

  • The code must be distributed for non-commercial use only.
    • If the code is to be distributed for commercial use, the author must have written approval from cPanel, Inc. Contact copyright@cpanel.net for details.
  • An uncompiled version of this code is publicly available.
  • Users understand that cPanel, Inc will not provide support for modified versions of this code.
  • All copyright notices are left intact in the modified version.
  • The contact information for the maintainer of the modified version is contained within the source.

When you modify this binary for use with their cPanel systems, you need to change the exec_list array to point to their 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 will 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. Users can find the Makefile in the downloadable example package.

Version 11.38 and newer

The pluggable wrap system is designed to simplify the process to construct AdminBin applications to be run securely at elevated privilege, and to help secure them. This system executes some of the tasks that were previously the responsibility of the developer.

How to create an AdminBin application

The code that you wish to execute in a privileged mode will be located in the /usr/local/cpanel/bin/admin/ directory. Build a new namespace in that directory, and then place the AdminBin applications into that namespace. The namespace and the directory name are exactly the same. The Cpanel namespace already exists, but you should create a new namespace. Create the corresponding directory with the following:

mkdir /usr/local/cpanel/bin/admin/MyNamespace

ALERT! Warning: We strongly recommend that you do not put your AdminBin application inside the Cpanel namespace. We may add new applications to our namespace that may cause conflicts with any that you put in there.

An AdminBin application is composed of two files: a configuration file (For example, MyExample.conf.), and the application itself (MyExample). The AdminBin application may also be in any language that you are comfortable with (For example, Perl, PHP, or Ruby.). However, you must shebang the AdminBin application properly or compile it.

The configuration file

The configuration file will resemble the following:

mode=simple
allowed_parents=/usr/local/cpanel/cpanel

note Note: The configuration file must be named MyExample.conf, where MyExample is the name of the module.

There are two possible configuration entries in the configuration file:

  • mode — There are two possible values for mode: simple and full.
    • If this entry is left blank, the default value is simple. In simple mode, a user's arguments to the module must be passed as a scalar value (integer, string, or undef), and will not be serialized.
    • If full is used, users may pass complex data structures that will be serialized with Cpanel::AdminBin::Serializer (currently in JSON).
  • allowed_parents — This entry is a comma-separated list of binaries that are allowed to call these routines.
    • If the file /var/cpanel/skipparentcheck exists, allowed_parents is ignored.
    • We recommend that you do not create /var/cpanel/skipparentcheck on production systems. skipparentcheck is intended for development sandboxes and other spaces where security is not as sensitive.
    • allowed_parents should only include compiled binaries. If you run your code from an interpreted file, do not include this entry.

The AdminBin application

An AdminBin application in simple mode should resemble the following:


#!/usr/local/cpanel/3rdparty/bin/perl

use strict; 
use Cpanel::AdminBin::Serializer ();
use Cpanel::Logger               ();
use Cpanel::PwCache              ();

my $stdin = <STDIN>;
chomp $stdin;
my ($uid,$function,$data) = split (/ /,$stdin,3);
# sanitize the input; in this case, only alphanumeric, underscore, space, period, and exclamation are allowed
$data =~ s/![\w \.\!]//g;   

# make a note in the logs!
my $user = (Cpanel::PwCache::getpwuid($uid))[0];
my $logger = Cpanel::Logger->new();
$logger -> warn("Myexample called by user $user with function: $function");

if ($function eq 'ECHO') {
        print $data;
        exit(0);
}

elsif ($function eq 'MIRROR') {
        print scalar reverse($data);
        exit(0);
}

elsif ($function eq 'BOUNCY') {
        print _bouncy($data);
        exit(0);
}

elsif ($function eq 'HASHIFY') {
        print ".\n" . Cpanel::AdminBin::Serializer::Dump ({ 'ourdata' =>  $data} );
        exit(0);
}

else {
        print "Invalid function specified to MyExample adminbin function";
        exit(1);
}

1;

sub _bouncy {
        my $data_in = shift;
        my $data_out = q{};
        for my $i (0..length($data_in)-1) {
                if ($i % 2) {
                        $data_out .= substr( $data_in,$i,1); }
                else {
                        $data_out .= uc(substr( $data_in,$i,1)); }}
        return $data_out;
}

note Note: The file must have the same name as the corresponding config file, without the .conf, and must be executable for the system to locate it.

ALERT! Important: The application must sanitize its own inputs. The system will not sanitize them.

To invoke your AdminBin application, use the Cpanel::Wrap::send_cpwrapd_request call when you run as the user. This will pass your arguments and parameters into the application via STDIN. Any subroutines that you write here will be private to the AdminBin application. User level code that calls Cpanel::Wrap::send_cpwrapd_request will not have access to these subroutines because AdminBin applications will execute in a separate process.

The AdminBin application receives a pseudo-function name as the second argument. You should determine the code path to execute in your AdminBin based on the pseudo-function name that is passed to your AdminBin application. The simplest way to implement this is an IF block that executes various code paths based on the pseudo-function name that has been passed to the AdminBin application.

The parameter order passed to Cpanel::Wrap::send_cpwrapd_request is constant, but the behavior of some parameters will vary. The behavior depends on the configuration mode:

Parameter simple mode full mode
uid (not a parameter, passed in automatically) Passed as the first item on STDIN. Passed in as ARGV[0].
function (the pseudo-function call) Passed on STDIN as the second item. Passed on STDIN as the first item.
data Passed on STDIN as the third item. You may only use scalars in simple mode. Passed on STDIN as the second item. If the data is a reference to a structure, it will be serialized by Cpanel::AdminBin::Serializer as JSON and passed in. You must deserialize it, if needed. If data is a scalar, it is passed in verbatim.
action (optional) Specifies the behavior of the output:
run — (default) Returns the data directly as a string.
fetch — Serializes the output into a JSON structure for return to the caller
stream — Streams the result. This will cause the AdminBin server to send the output from the AdminBin directly to the filehandle that was passed in the stream key to the Cpanel::Wrap::send_cpawrapd_request call.
note Note: If the AdminBin application module return starts with .\n, the action will automatically be switched to fetch.
env A hashref of keys and values to set in the environment before the AdminBin application executes.
note Note: Only keys that are in Cpanel:: Wrap:: Config::ALLOWED_ENV will be permitted. At this time, the key values that are permitted are REMOTE_PASSWORD, CPRESELLER, CPRESELLERSESSION, CPRESELLERSESSIONKEY, WHM50 and cp_security_token. All other values are ignored.
note Note: You can also simply pass Cpanel::Wrap::Config::safe_hashref_of_allowed_env.
module The filename of your AdminBin application.
* For example, MyExample in /usr/local/cpanel/bin/MyNamespace/MyExample
namespace The directory of the AdminBin application inside of /usr/local/cpanel/bin/admin/
* For example, MyNamespace in /usr/local/cpanel/bin/MyNamespace/MyExample
stream A filehandle to which the output of the AdminBin application will be streamed.
ALERT! Warning: Only use this parameter if your action is set to stream.
version This should always be set to Cpanel::AdminBin::Serializer::VERSION

Returns from the AdminBin application are sent via STDOUT and will be processed through the AdminBin server and returned via Cpanel::Wrap::send_cpwrapd_request.

  • The returns may be either a structure (For example, in Perl, a hash.) or an integer, string, or undef.
  • If the AdminBin application is supposed to return a structure, it will need to serialize the output with either Cpanel::AdminBin::Serializer::Dump or JSON.

Serialized output must be prefixed with a period and a newline (.\n) before you send the serialized data. When the AdminBin server sees a period and a newline, it will automatically treat the rest of the data it received as serialized and process it with Cpanel::AdminBin::Serializer::Load.

How to call an AdminBin application

The following LiveAPI example demonstrates how to use this functionality within a Perl script.


#!/usr/local/cpanel/3rdparty/bin/perl
BEGIN {
    unshift @INC, '/usr/local/cpanel';
}

use Cpanel::LiveAPI ();
use Data::Dumper    ();
use Cpanel::Wrap    ();

sub do_MyExample_stuff {
    my $thing_to_do = shift;
    my $string_to_mess_with = shift;
 
    my $result = Cpanel::Wrap::send_cpwrapd_request(
        'namespace' => 'MyNamespace',
        'module'    => 'MyExample',
        'function'  => $thing_to_do,
        'data'      => $string_to_mess_with
    );

    if ( $result->{'error'} ) {
        return "Error code $result->{'exit_code'} returned: $result->{'data'}";
    }
    elsif ( ref ( $result->{'data'} ) ) {
        return Data::Dumper::Dumper($result->{'data'});
    }
    elsif ( defined( $result->{'data'}) ) {
        return $result->{'data'};
    }
    return 'cpwrapd request failed: ' . $result->{'statusmsg'};
}

my $cpanel = Cpanel::LiveAPI->new();

print "Content-type: text/html\r\n\r\n";

print "<pre>";

print "ECHO test:\n".do_MyExample_stuff("ECHO","Hello, World!")."\n\n";
print "MIRROR test:\n".do_MyExample_stuff("MIRROR","Hello, World!")."\n\n";
print "BOUNCY test:\n".do_MyExample_stuff("BOUNCY","Hello, World!")."\n\n";
print "HASHIFY test:\n".do_MyExample_stuff("HASHIFY","Hello, World!")."\n\n";
print "WRONG test:\n".do_MyExample_stuff("WRONG","Hello, World!")."\n\n";
 
print "test complete!\n";
$cpanel->end();

Example output from Cpanel::Wrap::send_cpwrap_request

Cpanel::Wrap::send_cpwrapd_request will return a hashref similar to the below:

Successful request:

{
         'statusmsg' => 'Ran AdminBin MyNamespace/MyExample/HASHIFY',
         'version' => '2.3',
         'status' => 1,
         'mode' => 'simple',
         'data' => {
                     'ourdata' => 'Hello, World!'
                   },
         'exit_code' => 0,
         'timeout' => 0,
         'action' => 'fetch',
         'error' => 0
};

Unsuccessful request:

{
          'statusmsg' => 'Ran adminbin MyNamespace/MyExample/WRONG',
          'version' => '2.3',
          'status' => 1,
          'mode' => 'simple',
          'data' => 'Invalid function specified to MyExample adminbin function',
          'exit_code' => 256,
          'timeout' => 0,
          'action' => 'run',
          'error' => 1
}

Downloadable examples for version 11.38

  • PrivEscExample.tar.gz: This file is the example for simple mode.
  • cron.tar.gz: This file is an elaborate example that enables a user to edit and update cron entries.

You may also wish to view the github history on how CloudFlare updated their cPanel plugin to use the new system.

Topic revision: r20 - 31 Jul 2013 - 19:04:14 - Main.SarahHaney
DeveloperResources.PrivilegeEscalation moved from Sandbox.PrivilegeEscalation on 21 Oct 2010 - 17:30 by Main.JustinSchaefer