mirror of
https://github.com/gchq/CyberChef.git
synced 2025-06-14 10:14:53 -04:00
Merge e77cdaee11
into c57556f49f
This commit is contained in:
commit
efe521619c
2 changed files with 159 additions and 51 deletions
|
@ -20,7 +20,7 @@ class PHPDeserialize extends Operation {
|
||||||
|
|
||||||
this.name = "PHP Deserialize";
|
this.name = "PHP Deserialize";
|
||||||
this.module = "Default";
|
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.infoURL = "http://www.phpinternalsbook.com/classes_objects/serialization.html";
|
||||||
this.inputType = "string";
|
this.inputType = "string";
|
||||||
this.outputType = "string";
|
this.outputType = "string";
|
||||||
|
@ -39,6 +39,8 @@ class PHPDeserialize extends Operation {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
run(input, args) {
|
run(input, args) {
|
||||||
|
const refStore = [];
|
||||||
|
const inputPart = input.split("");
|
||||||
/**
|
/**
|
||||||
* Recursive method for deserializing.
|
* Recursive method for deserializing.
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
|
@ -60,7 +62,6 @@ class PHPDeserialize extends Operation {
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read characters from the input until `until` is found.
|
* Read characters from the input until `until` is found.
|
||||||
* @param until
|
* @param until
|
||||||
|
@ -70,28 +71,57 @@ class PHPDeserialize extends Operation {
|
||||||
let result = "";
|
let result = "";
|
||||||
for (;;) {
|
for (;;) {
|
||||||
const char = read(1);
|
const char = read(1);
|
||||||
if (char === until) {
|
if (char === until) break;
|
||||||
break;
|
result += char;
|
||||||
} else {
|
|
||||||
result += char;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read characters from the input that must be equal to `expect`
|
* Read characters from the input that must be equal to `expect`
|
||||||
* @param expect
|
* @param expectStr
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function expect(expect) {
|
function expect(expectStr) {
|
||||||
const result = read(expect.length);
|
const result = read(expectStr.length);
|
||||||
if (result !== expect) {
|
if (result !== expectStr) {
|
||||||
throw new OperationError("Unexpected input found");
|
throw new OperationError(`Expected "${expectStr}", but got "${result}"`);
|
||||||
}
|
}
|
||||||
return 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.
|
* Helper function to handle deserialized arrays.
|
||||||
|
@ -100,61 +130,113 @@ class PHPDeserialize extends Operation {
|
||||||
function handleArray() {
|
function handleArray() {
|
||||||
const items = parseInt(readUntil(":"), 10) * 2;
|
const items = parseInt(readUntil(":"), 10) * 2;
|
||||||
expect("{");
|
expect("{");
|
||||||
const result = [];
|
const result = {};
|
||||||
let isKey = true;
|
for (let idx = 0; idx < items; idx += 2) {
|
||||||
let lastItem = null;
|
const keyInfo = handleInput();
|
||||||
for (let idx = 0; idx < items; idx++) {
|
const valueInfo = handleInput();
|
||||||
const item = handleInput();
|
let key = keyInfo.value;
|
||||||
if (isKey) {
|
if (keyInfo.keyType === "i") key = parseInt(key, 10);
|
||||||
lastItem = item;
|
result[key] = valueInfo.value;
|
||||||
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);
|
|
||||||
}
|
|
||||||
isKey = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
expect("}");
|
expect("}");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const kind = read(1).toLowerCase();
|
const kind = read(1).toLowerCase();
|
||||||
|
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case "n":
|
case "n":
|
||||||
expect(";");
|
expect(";");
|
||||||
return "null";
|
return record({ value: null, keyType: kind });
|
||||||
case "i":
|
|
||||||
case "d":
|
case "i": {
|
||||||
|
expect(":");
|
||||||
|
const data = readUntil(";");
|
||||||
|
return record({ value: parseInt(data, 10), keyType: kind });
|
||||||
|
}
|
||||||
|
|
||||||
|
case "d": {
|
||||||
|
expect(":");
|
||||||
|
const data = readUntil(";");
|
||||||
|
return record({ value: parseFloat(data), keyType: kind });
|
||||||
|
}
|
||||||
|
|
||||||
case "b": {
|
case "b": {
|
||||||
expect(":");
|
expect(":");
|
||||||
const data = readUntil(";");
|
const data = readUntil(";");
|
||||||
if (kind === "b") {
|
return record({ value: data !== 0, keyType: kind });
|
||||||
return (parseInt(data, 10) !== 0);
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "a":
|
case "a":
|
||||||
expect(":");
|
expect(":");
|
||||||
return "{" + handleArray() + "}";
|
return record({ value: handleArray(), keyType: kind });
|
||||||
|
|
||||||
case "s": {
|
case "s": {
|
||||||
expect(":");
|
expect(":");
|
||||||
const length = readUntil(":");
|
const lengthRaw = readUntil(":").trim();
|
||||||
|
const length = parseInt(lengthRaw, 10);
|
||||||
expect("\"");
|
expect("\"");
|
||||||
const value = read(length);
|
|
||||||
expect('";');
|
// Read until the next quote-semicolon
|
||||||
if (args[0]) {
|
let str = "";
|
||||||
return '"' + value.replace(/"/g, '\\"') + '"'; // lgtm [js/incomplete-sanitization]
|
while (true) {
|
||||||
} else {
|
const next = read(1);
|
||||||
return '"' + value + '"';
|
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({ value: str, keyType: kind });
|
||||||
|
}
|
||||||
|
|
||||||
|
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 valueRaw = handleInput();
|
||||||
|
let key = keyRaw.value;
|
||||||
|
if (typeof key === "string" && key.startsWith('"') && key.endsWith('"')) {
|
||||||
|
key = key.slice(1, -1);
|
||||||
|
}
|
||||||
|
key = normalizeKey(key);
|
||||||
|
obj[key] = valueRaw.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect("}");
|
||||||
|
return record({ value: obj, keyType: kind });
|
||||||
|
}
|
||||||
|
|
||||||
|
case "r": {
|
||||||
|
expect(":");
|
||||||
|
const refIndex = parseInt(readUntil(";"), 10);
|
||||||
|
if (refIndex >= refStore.length || refIndex < 0) {
|
||||||
|
throw new OperationError(`Invalid reference index: ${refIndex}`);
|
||||||
|
}
|
||||||
|
const refValue = refStore[refIndex];
|
||||||
|
if (typeof refValue === "object" && refValue !== null && "value" in refValue && "keyType" in refValue) {
|
||||||
|
return refValue;
|
||||||
|
}
|
||||||
|
return record({ value: refValue, keyType: kind });
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -162,10 +244,25 @@ class PHPDeserialize extends Operation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputPart = input.split("");
|
/**
|
||||||
return handleInput();
|
* Helper function to make invalid json output (legacy support)
|
||||||
}
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
function stringifyWithIntegerKeys(obj) {
|
||||||
|
const entries = Object.entries(obj).map(([key, value]) => {
|
||||||
|
const jsonKey = Number.isInteger(+key) ? key : JSON.stringify(key);
|
||||||
|
const jsonValue = JSON.stringify(value);
|
||||||
|
return `${jsonKey}:${jsonValue}`;
|
||||||
|
});
|
||||||
|
return `{${entries.join(',')}}`; // eslint-disable-line quotes
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args[0]) {
|
||||||
|
return JSON.stringify(handleInput().value);
|
||||||
|
} else {
|
||||||
|
return stringifyWithIntegerKeys(handleInput().value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PHPDeserialize;
|
export default PHPDeserialize;
|
||||||
|
|
|
@ -46,7 +46,7 @@ TestRegister.addTests([
|
||||||
{
|
{
|
||||||
name: "PHP Deserialize array (JSON)",
|
name: "PHP Deserialize array (JSON)",
|
||||||
input: "a:2:{s:1:\"a\";i:10;i:0;a:1:{s:2:\"ab\";b:1;}}",
|
input: "a:2:{s:1:\"a\";i:10;i:0;a:1:{s:2:\"ab\";b:1;}}",
|
||||||
expectedOutput: "{\"a\": 10,\"0\": {\"ab\": true}}",
|
expectedOutput: '{"0":{"ab":true},"a":10}',
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "PHP Deserialize",
|
op: "PHP Deserialize",
|
||||||
|
@ -57,7 +57,7 @@ TestRegister.addTests([
|
||||||
{
|
{
|
||||||
name: "PHP Deserialize array (non-JSON)",
|
name: "PHP Deserialize array (non-JSON)",
|
||||||
input: "a:2:{s:1:\"a\";i:10;i:0;a:1:{s:2:\"ab\";b:1;}}",
|
input: "a:2:{s:1:\"a\";i:10;i:0;a:1:{s:2:\"ab\";b:1;}}",
|
||||||
expectedOutput: "{\"a\": 10,0: {\"ab\": true}}",
|
expectedOutput: '{0:{"ab":true},"a":10}',
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "PHP Deserialize",
|
op: "PHP Deserialize",
|
||||||
|
@ -65,4 +65,15 @@ TestRegister.addTests([
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "PHP Deserialize array with object and reference (JSON)",
|
||||||
|
input: 'a:1:{s:6:"navbar";O:18:"APP\\View\\Menu\\Item":3:{s:4:"name";s:16:"Secondary Navbar";s:8:"children";a:1:{s:9:"View Cart";O:18:"APP\\View\\Menu\\Item":2:{s:4:"name";s:9:"View Cart";s:6:"parent";r:2;}}s:6:"parent";N;}}', // eslint-disable-line no-useless-escape
|
||||||
|
expectedOutput: `{"navbar":{"__className":"APP\\\\View\\\\Menu\\\\Item","name":"Secondary Navbar","children":{"View Cart":{"__className":"APP\\\\View\\\\Menu\\\\Item","name":"View Cart","parent":"Secondary Navbar"}},"parent":null}}`,
|
||||||
|
recipeConfig: [
|
||||||
|
{
|
||||||
|
op: "PHP Deserialize",
|
||||||
|
args: [true],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
]);
|
]);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue