Last active
October 29, 2021 04:22
-
-
Save not-an-aardvark/769c94a17ba4e1d6aa3ca5529dc9f4e6 to your computer and use it in GitHub Desktop.
See https://blog.teddykatz.com/2019/11/23/json-padding-oracles.html for more details.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use strict"; | |
const crypto = require("crypto"); | |
const fetch = require("node-fetch"); | |
const BLOCK_SIZE = 16; | |
const PADDING_ORACLE_PAYLOAD = '","user":"admin"}'; | |
const padToBlockSize = text => { | |
const unpadded = Buffer.from(text, "binary"); | |
const paddingNeeded = BLOCK_SIZE - unpadded.length % BLOCK_SIZE; | |
return Buffer.concat([ | |
unpadded, | |
Buffer.alloc(paddingNeeded).fill(paddingNeeded) | |
]); | |
}; | |
const splitIntoBlocks = paddedText => { | |
let arr = []; | |
for (let i = 0; i < paddedText.length; i += BLOCK_SIZE) { | |
arr.push(paddedText.slice(i, i + BLOCK_SIZE)); | |
} | |
return arr; | |
}; | |
const hasValidPadding = async cipherText => { | |
const response = await fetch( | |
"http://localhost:9000/flag?token=" + cipherText.toString("hex") | |
); | |
if (response.ok) { | |
return true; | |
} | |
const responseBody = await response.text(); | |
if ( | |
responseBody.includes( | |
"digital envelope routines:EVP_DecryptFinal_ex:bad decrypt" | |
) | |
) { | |
return false; | |
} | |
if (/Unexpected token . in JSON/s.test(responseBody)) { | |
return true; | |
} | |
throw new Error(responseBody); | |
}; | |
const runPaddingOracle = async () => { | |
const cipherTexts = [crypto.randomBytes(BLOCK_SIZE)]; | |
const plainTextBlocks = splitIntoBlocks( | |
padToBlockSize(PADDING_ORACLE_PAYLOAD) | |
); | |
for (let i = plainTextBlocks.length - 1; i >= 0; i--) { | |
const twoBlockString = Buffer.concat([ | |
Buffer.alloc(BLOCK_SIZE).fill(0), | |
cipherTexts[0] | |
]); | |
for ( | |
let expectedPaddingAmount = 1; | |
expectedPaddingAmount <= BLOCK_SIZE; | |
expectedPaddingAmount++ | |
) { | |
for (let byte = 0; byte < 256; byte++) { | |
twoBlockString[BLOCK_SIZE - expectedPaddingAmount] = byte; | |
if (await hasValidPadding(twoBlockString)) { | |
for ( | |
let j = BLOCK_SIZE - expectedPaddingAmount; | |
j < BLOCK_SIZE; | |
j++ | |
) { | |
twoBlockString[j] ^= expectedPaddingAmount; | |
if (expectedPaddingAmount < BLOCK_SIZE) { | |
twoBlockString[j] ^= expectedPaddingAmount + 1; | |
} | |
} | |
break; | |
} | |
} | |
} | |
cipherTexts.unshift(plainTextBlocks[i]); | |
for (let j = 0; j < BLOCK_SIZE; j++) { | |
cipherTexts[0][j] ^= twoBlockString[j]; | |
} | |
} | |
return cipherTexts; | |
}; | |
(async () => { | |
let flagResponse; | |
do { | |
const homepageBody = await fetch("http://localhost:9000/").then(res => | |
res.text() | |
); | |
const anonymousToken = homepageBody.match(/(?<token>[0-9a-f]{96})/).groups | |
.token; | |
const firstTwoBlocks = Buffer.from(anonymousToken, "hex").slice(0, 32); | |
const constructedToken = Buffer.concat([ | |
firstTwoBlocks, | |
...(await runPaddingOracle()) | |
]); | |
flagResponse = await fetch( | |
"http://localhost:9000/flag?token=" + constructedToken.toString("hex") | |
); | |
} while (!flagResponse.ok); | |
console.log(await flagResponse.text()); | |
})(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use strict"; | |
const fs = require("fs"); | |
const crypto = require("crypto"); | |
const http = require("http"); | |
const KEY = fs.readFileSync("key.txt"); | |
const FLAG = fs.readFileSync("flag.txt", "utf8"); | |
function generateToken(username) { | |
const payload = { time: Date.now(), user: username }; | |
const cipher = crypto.createCipher("AES-192-CBC", KEY); | |
let token = cipher.update(JSON.stringify(payload), "binary", "hex"); | |
token += cipher.final("hex"); | |
return token; | |
} | |
function validateToken(token, username) { | |
if (!token) { | |
throw new Error("no token provided"); | |
} | |
const decipher = crypto.createDecipher("AES-192-CBC", KEY); | |
let plaintext = decipher.update(token, "hex", "binary"); | |
plaintext += decipher.final("binary"); | |
const payload = JSON.parse(plaintext); | |
if (Date.now() - payload.time > 1000 * 60 * 60 * 24 * 30) { | |
throw new Error("token > 30 days old"); | |
} | |
if (payload.user !== username) { | |
throw new Error("wrong user"); | |
} | |
} | |
http | |
.createServer((req, res) => { | |
const url = new URL(req.url, "http://localhost:9000"); | |
try { | |
switch (url.pathname) { | |
case "/": | |
res.writeHead(200, { "Content-Type": "text/plain" }); | |
res.end( | |
`Welcome, anonymous. Your token is ${generateToken("anonymous")}.` | |
); | |
break; | |
case "/flag": | |
validateToken(url.searchParams.get("token"), "admin"); | |
res.writeHead(200, { "Content-Type": "text/plain" }); | |
res.end(FLAG); | |
break; | |
default: | |
res.writeHead(404, { "Content-Type": "text/plain" }); | |
res.end("Not found"); | |
} | |
} catch (err) { | |
res.writeHead(500, { "Content-Type": "text/plain" }); | |
res.end(err.stack); | |
} | |
}) | |
.listen(9000); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment