mirror of
https://github.com/gchq/CyberChef.git
synced 2025-06-14 10:14:53 -04:00
refactor to allow objects and references in phpserialised data
This commit is contained in:
parent
411f78d27a
commit
208df066a0
1 changed files with 117 additions and 33 deletions
|
@ -20,7 +20,7 @@ class PHPDeserialize extends Operation {
|
|||
|
||||
this.name = "PHP Deserialize";
|
||||
this.module = "Default";
|
||||
this.description = "Deserializes PHP serialized data, outputting keyed arrays as JSON.<br><br>This function does not support <code>object</code> tags.<br><br>Example:<br><code>a:2:{s:1:"a";i:10;i:0;a:1:{s:2:"ab";b:1;}}</code><br>becomes<br><code>{"a": 10,0: {"ab": true}}</code><br><br><u>Output valid JSON:</u> JSON doesn't support integers as keys, whereas PHP serialization does. Enabling this will cast these integers to strings. This will also escape backslashes.";
|
||||
this.description = "Deserializes PHP serialized data, outputting keyed arrays as JSON.<br><br>Example:<br><code>a:2:{s:1:"a";i:10;i:0;a:1:{s:2:"ab";b:1;}}</code><br>becomes<br><code>{"a": 10,0: {"ab": true}}</code><br><br><u>Output valid JSON:</u> JSON doesn't support integers as keys, whereas PHP serialization does. Enabling this will cast these integers to strings. This will also escape backslashes.";
|
||||
this.infoURL = "http://www.phpinternalsbook.com/classes_objects/serialization.html";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
|
@ -39,6 +39,8 @@ class PHPDeserialize extends Operation {
|
|||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
const refStore = [];
|
||||
const inputPart = input.split("");
|
||||
/**
|
||||
* Recursive method for deserializing.
|
||||
* @returns {*}
|
||||
|
@ -60,7 +62,6 @@ class PHPDeserialize extends Operation {
|
|||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read characters from the input until `until` is found.
|
||||
* @param until
|
||||
|
@ -70,14 +71,10 @@ class PHPDeserialize extends Operation {
|
|||
let result = "";
|
||||
for (;;) {
|
||||
const char = read(1);
|
||||
if (char === until) {
|
||||
break;
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
if (char === until) break;
|
||||
result += char;
|
||||
}
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -85,13 +82,46 @@ class PHPDeserialize extends Operation {
|
|||
* @param expect
|
||||
* @returns {string}
|
||||
*/
|
||||
function expect(expect) {
|
||||
const result = read(expect.length);
|
||||
if (result !== expect) {
|
||||
throw new OperationError("Unexpected input found");
|
||||
function expect(expectStr) {
|
||||
const result = read(expectStr.length);
|
||||
if (result !== expectStr) {
|
||||
throw new OperationError(`Expected "${expectStr}", but got "${result}"`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Records a value by pushing it into the reference store and returns it.
|
||||
* @param {any} value - The value to be recorded.
|
||||
* @returns {any} - The recorded value.
|
||||
*/
|
||||
function record(value) {
|
||||
refStore.push(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the key by converting private and protected keys to standard formats.
|
||||
* @param {string} key - The key to be normalized.
|
||||
* @returns {string} - The normalized key.
|
||||
*/
|
||||
function normalizeKey(key) {
|
||||
if (typeof key !== "string") return key;
|
||||
|
||||
// Match private: "\0ClassName\0prop"
|
||||
const privateMatch = key.match(/^\u0000(.+)\u0000(.+)$/);
|
||||
if (privateMatch) {
|
||||
const [_, className, prop] = privateMatch; // eslint-disable-line no-unused-vars
|
||||
return `private:${prop}`;
|
||||
}
|
||||
|
||||
// Match protected: "\0*\0prop"
|
||||
const protectedMatch = key.match(/^\u0000\*\u0000(.+)$/);
|
||||
if (protectedMatch) {
|
||||
return `protected:${protectedMatch[1]}`;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to handle deserialized arrays.
|
||||
|
@ -100,7 +130,7 @@ class PHPDeserialize extends Operation {
|
|||
function handleArray() {
|
||||
const items = parseInt(readUntil(":"), 10) * 2;
|
||||
expect("{");
|
||||
const result = [];
|
||||
const result = {};
|
||||
let isKey = true;
|
||||
let lastItem = null;
|
||||
for (let idx = 0; idx < items; idx++) {
|
||||
|
@ -109,12 +139,11 @@ class PHPDeserialize extends Operation {
|
|||
lastItem = item;
|
||||
isKey = false;
|
||||
} else {
|
||||
const numberCheck = lastItem.match(/[0-9]+/);
|
||||
if (args[0] && numberCheck && numberCheck[0].length === lastItem.length) {
|
||||
result.push('"' + lastItem + '": ' + item);
|
||||
} else {
|
||||
result.push(lastItem + ": " + item);
|
||||
let key = lastItem;
|
||||
if (args[0] && typeof key === "number") {
|
||||
key = key.toString();
|
||||
}
|
||||
result[key] = item;
|
||||
isKey = true;
|
||||
}
|
||||
}
|
||||
|
@ -122,39 +151,96 @@ class PHPDeserialize extends Operation {
|
|||
return result;
|
||||
}
|
||||
|
||||
|
||||
const kind = read(1).toLowerCase();
|
||||
|
||||
switch (kind) {
|
||||
case "n":
|
||||
expect(";");
|
||||
return "null";
|
||||
return record(null);
|
||||
|
||||
case "i":
|
||||
case "d":
|
||||
case "b": {
|
||||
expect(":");
|
||||
const data = readUntil(";");
|
||||
if (kind === "b") {
|
||||
return (parseInt(data, 10) !== 0);
|
||||
return record(parseInt(data, 10) !== 0);
|
||||
}
|
||||
return data;
|
||||
if (kind === "i") {
|
||||
return record(parseInt(data, 10));
|
||||
}
|
||||
if (kind === "d") {
|
||||
return record(parseFloat(data));
|
||||
}
|
||||
return record(data);
|
||||
}
|
||||
|
||||
case "a":
|
||||
expect(":");
|
||||
return "{" + handleArray() + "}";
|
||||
return record(handleArray());
|
||||
|
||||
case "s": {
|
||||
expect(":");
|
||||
const length = readUntil(":");
|
||||
const lengthRaw = readUntil(":").trim();
|
||||
const length = parseInt(lengthRaw, 10);
|
||||
expect("\"");
|
||||
const value = read(length);
|
||||
expect('";');
|
||||
if (args[0]) {
|
||||
return '"' + value.replace(/"/g, '\\"') + '"'; // lgtm [js/incomplete-sanitization]
|
||||
} else {
|
||||
return '"' + value + '"';
|
||||
|
||||
// Read until the next quote-semicolon
|
||||
let str = "";
|
||||
while (true) {
|
||||
const next = read(1);
|
||||
if (next === '"' && inputPart[0] === ";") {
|
||||
inputPart.shift(); // Consume the ;
|
||||
break;
|
||||
}
|
||||
str += next;
|
||||
}
|
||||
|
||||
const actualByteLength = new TextEncoder().encode(str).length;
|
||||
if (actualByteLength !== length) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`Length mismatch: declared ${length}, got ${actualByteLength} — proceeding anyway`);
|
||||
}
|
||||
|
||||
return record(str);
|
||||
}
|
||||
|
||||
case "o": {
|
||||
expect(":");
|
||||
const classNameLength = parseInt(readUntil(":"), 10);
|
||||
expect("\"");
|
||||
const className = read(classNameLength);
|
||||
expect("\"");
|
||||
expect(":");
|
||||
const propertyCount = parseInt(readUntil(":"), 10);
|
||||
expect("{");
|
||||
|
||||
const obj = {
|
||||
__className: className
|
||||
};
|
||||
|
||||
for (let i = 0; i < propertyCount; i++) {
|
||||
const keyRaw = handleInput();
|
||||
const value = handleInput();
|
||||
let key = keyRaw;
|
||||
if (typeof keyRaw === "string" && keyRaw.startsWith('"') && keyRaw.endsWith('"')) {
|
||||
key = keyRaw.slice(1, -1);
|
||||
}
|
||||
key = normalizeKey(key);
|
||||
obj[key] = value;
|
||||
}
|
||||
|
||||
expect("}");
|
||||
return record(obj);
|
||||
}
|
||||
|
||||
case "r": {
|
||||
expect(":");
|
||||
const refIndex = parseInt(readUntil(";"), 10);
|
||||
if (refIndex >= refStore.length || refIndex < 0) {
|
||||
throw new OperationError(`Invalid reference index: ${refIndex}`);
|
||||
}
|
||||
return refStore[refIndex];
|
||||
}
|
||||
|
||||
default:
|
||||
|
@ -162,10 +248,8 @@ class PHPDeserialize extends Operation {
|
|||
}
|
||||
}
|
||||
|
||||
const inputPart = input.split("");
|
||||
return handleInput();
|
||||
return JSON.stringify(handleInput());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default PHPDeserialize;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue