Skip to content

Instantly share code, notes, and snippets.

@not-an-aardvark
Last active October 29, 2021 04:22
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save not-an-aardvark/769c94a17ba4e1d6aa3ca5529dc9f4e6 to your computer and use it in GitHub Desktop.
Save not-an-aardvark/769c94a17ba4e1d6aa3ca5529dc9f4e6 to your computer and use it in GitHub Desktop.
"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());
})();
"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