Among other uses, PAN-OS uses the device master key to encrypt secrets in the config. Most of the time, a malicious administrator can simply decrypt the passwords using the AES-256-CBC key '8103850245b9b48f0428c5b74e2615528103850245b9b48f0428c5b74e261552', but occasionally administrators will actually remember to change the default master key. Luckily, Palo Alto Networks made sure to provide an alternative way to borrow plaintext secrets from a firewall in situations like this. They kindly gave us a nice user-friendly padding oracle by logging invalid padding.
In /usr/local/sbin/cryptod, the function which is responsible for decrypting v1 (default) secrets is pan_cryptod_decrypt_version1. It takes data encoded in a specific format, and it decrypts and verifies the hash of the data. The format is:
base64(version)||base64(sha1(plaintext))||base64(ciphertext)
During decryption of messages, the return value of EVP_DecryptFinal_ex is checked. If an error is detected, a log message may be printed depending on logging level, and the function returns immediately. According to OpenSSL documentation, EVP_DecryptFinal_ex "will return an error code if padding is enabled and the final block is not correctly formatted". If logging is set to debug, this is immediately enough to launch a padding oracle attack. However, in the default logging configuration, this message will not be printed. Luckily, the difference in execution provides more differences soon after. If the sha1 of the plaintext is invalid, an error message is logged. Because a failure in EVP_DecryptFinal_ex causes pan_cryptod_decrypt_version1 to return early, only a message with valid padding will reach the hash check. As a result, as long as we set the hash to a junk value, we can use the integrity check failure as our oracle to launch this attack.As a side note, since the sha1 verification of the plaintext happens only after decryption is successful, there may also be a timing-based padding oracle. I was unable to observe a statistically significant variation in timing after 100,000 requests made over LAN so it's unlikley that this will be too usable for a real-world attack, but it's still a theoretical possibility and may be exploitable in some configurations.
PKCS#7 is a method for padding a message to a known byte alignment. This is used for AES since AES operates on 16-byte blocks. If the length of your message is not divisible by 16 bytes, you will need to add extra data to the message until it is a multiple of 16 bytes long. With PKCS7 padding, the padding byte will have the value of the number of padding bytes which were added. To pad the message "DA SLOP PIT!!!", which is 14 bytes long, you would add 2 bytes of '\x02' resulting in the padded message "DA SLOP PIT!!!\x02\x02". If the message is already exactly 16 bytes long, then PKCS7 requires adding an additional block, which for AES would contain 16 bytes of '\x10'. This is a deterministic method which allows a message to be deterministically padded and allows the decrypting side the ability to validate the padding.
The simplest method of encrypting data with AES would be to simply encrypt each block of data with the key. This has the drawback that identical plaintexts will produce identical ciphertexts, and an adversary can gain information about the structure of the plaintext. Many alternative modes of operations have been developed to get around this, with one of the most common variants being cipher block chaining. In the CBC mode of operation, the plaintext of each block is xored with the ciphertext of the previous block before being encrypted. Since the first block of data has no previous ciphertext, an initialization vector is generated and used instead of the previous ciphertext. Ideally, this IV should be randomly generated, but the Palo Alto Networks realized that generating unique IVs would be too much effort so they just hardcoded the IV to all-zeroes instead.
The weakness of AES-CBC is that, since the ciphertext of block n-1 is xored with the plaintext of block n, flipping a bit in block n-1 results in flipping a bit in the plaintext of block n. Combined with PKCS7 and an oracle which will tell us whether a message is padded correctly, we can perform a chosen ciphertext attack to recover the plaintext of an arbitrary encrypted message.
This attack will operate on two blocks of data. We will be flipping bits in the first block b0, and using that to decrypt the second block b1.
The algorithm we will use is as follows:
1. brute force a valid 1-byte padding by iterating through all possible values of b0[15] until the padding oracle informs us a valid padding is found.
2. at this point, we know b0[15]^decr(b1)[15]==\x01. rearranging this gives b0[15]^\x01==decr(b1)[15], meaning we have recovered the last byte of plaintext.
3. set decr(b1)[15] to \x02 by modifying b1[15], and repeat steps 1 and 2, except searching for a padding of \x02 and operating on byte 14, then \x03 and 13, and so on.
This is a relatively straightforward algorithm, but unfortunately it looks a bit messy when translated into code. The core of the code looks something like this:
def leakBlock(b0, b1):
pt='\0'*16
for i in range(15,-1, -1):
for j in range(255,-1,-1): # 1
a=xor_block(pt, b0) # 2
b=set_pad(a,i) # 3
ct=xor_byte(b, i, j)+b1 # 4
makeRequest(ct)
if checkLog():
pt=xor_byte(pt, i, j^(16-i)) # 4
print(pt.encode('hex'))
break
else:
print("no valid padding found... error")
return pt
1: For the initial byte, xor with 0 will always be guaranteed to produce a valid padding if the message is padded correctly. Because of this, we want to see if any other value produces a correct padding, and only break and assume a guess of \0 is correct if all other values fail to produce a valid padding. In this case, the padding was already set to \x01.
2: xor_block performs a bitwise xor on the entire 16-byte blocks pt and b0
3: update the known values to the next correct value of padding (eg when we know a value which will produce \x02\x02 for the last 2 bytes, this generates a value which will set them to \x03\03)
4: xors the i'th byte of the block with j
This will require at most 256*16=4096 queries per block. I am using paramiko to log in and parse logs, and I'm using requests to make the actual requests. I also use the XML API for making requests, although the sslmgr endpoint could also be used if globalprotect is enabled.
My full script for performing this attack can be found on github. This vulnerability was initially found live on twitter. I have also reported this vulnerability to Palo Alto networks at some point before publishing this Post.
Thanks for coming to SpaceHey, the hot new Web Site for Cyber Threat Intelligence for Ghosts. Please remember that only true scene queens and goths are permitted on this Web Site, so if you're some "info sec" "professional", please go back to Linked In Dot Com or wherever it is you do your silly little Professional Posting. If you are a scene queen and/or goth, please like comment and subscribe for more vulnerabilities and writeups like this one.