diff --git a/src/core/config/Categories.js b/src/core/config/Categories.js index f04b5fd9..3bc672b7 100755 --- a/src/core/config/Categories.js +++ b/src/core/config/Categories.js @@ -288,6 +288,7 @@ const Categories = [ "XPath expression", "JPath expression", "CSS selector", + "PHP Deserialize", "Microsoft Script Decoder", "Strip HTML tags", "Diff", diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index 9caa4f91..43072558 100755 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -26,6 +26,7 @@ import JS from "../operations/JS.js"; import MAC from "../operations/MAC.js"; import MorseCode from "../operations/MorseCode.js"; import NetBIOS from "../operations/NetBIOS.js"; +import PHP from "../operations/PHP.js"; import PublicKey from "../operations/PublicKey.js"; import Punycode from "../operations/Punycode.js"; import Rotate from "../operations/Rotate.js"; @@ -3845,6 +3846,19 @@ const OperationConfig = { } ] }, + "PHP Deserialize": { + module: "Default", + description: "Deserializes PHP serialized data, outputting keyed arrays as JSON.

This function does not support object tags.

Example:
a:2:{s:1:"a";i:10;i:0;a:1:{s:2:"ab";b:1;}}
becomes
{"a": 10,0: {"ab": true}}

Output valid JSON: JSON doesn't support integers as keys, whereas PHP serialization does. Enabling this will cast these integers to strings. This will also escape backslashes.", + inputType: "string", + outputType: "string", + args: [ + { + name: "Output valid JSON", + type: "boolean", + value: PHP.OUTPUT_VALID_JSON + } + ] + }, }; diff --git a/src/core/config/modules/Default.js b/src/core/config/modules/Default.js index 682db223..dec015a5 100644 --- a/src/core/config/modules/Default.js +++ b/src/core/config/modules/Default.js @@ -20,6 +20,7 @@ import NetBIOS from "../../operations/NetBIOS.js"; import Numberwang from "../../operations/Numberwang.js"; import OS from "../../operations/OS.js"; import OTP from "../../operations/OTP.js"; +import PHP from "../../operations/PHP.js"; import QuotedPrintable from "../../operations/QuotedPrintable.js"; import Rotate from "../../operations/Rotate.js"; import SeqUtils from "../../operations/SeqUtils.js"; @@ -28,7 +29,6 @@ import Tidy from "../../operations/Tidy.js"; import Unicode from "../../operations/Unicode.js"; import UUID from "../../operations/UUID.js"; - /** * Default module. * @@ -155,6 +155,7 @@ OpModules.Default = { "Conditional Jump": FlowControl.runCondJump, "Return": FlowControl.runReturn, "Comment": FlowControl.runComment, + "PHP Deserialize": PHP.runDeserialize, /* diff --git a/src/core/operations/PHP.js b/src/core/operations/PHP.js new file mode 100644 index 00000000..e4bb0b5b --- /dev/null +++ b/src/core/operations/PHP.js @@ -0,0 +1,160 @@ +/** + * PHP operations. + * + * @author Jarmo van Lenthe [github.com/jarmovanlenthe] + * @copyright Jarmo van Lenthe + * @license Apache-2.0 + * + * @namespace + */ +const PHP = { + + /** + * @constant + * @default + */ + OUTPUT_VALID_JSON: true, + + /** + * PHP Deserialize operation. + * + * This Javascript implementation is based on the Python implementation by + * Armin Ronacher (2016), who released it under the 3-Clause BSD license. + * See: https://github.com/mitsuhiko/phpserialize/ + * + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + runDeserialize: function (input, args) { + /** + * Recursive method for deserializing. + * @returns {*} + */ + function handleInput() { + /** + * Read `length` characters from the input, shifting them out the input. + * @param length + * @returns {string} + */ + function read(length) { + let result = ""; + for (let idx = 0; idx < length; idx++) { + let char = inputPart.shift(); + if (char === undefined) { + throw "End of input reached before end of script"; + } + result += char; + } + return result; + } + + /** + * Read characters from the input until `until` is found. + * @param until + * @returns {string} + */ + function readUntil(until) { + let result = ""; + for (;;) { + let char = read(1); + if (char === until) { + break; + } else { + result += char; + } + } + return result; + + } + + /** + * Read characters from the input that must be equal to `expect` + * @param expect + * @returns {string} + */ + function expect(expect) { + let result = read(expect.length); + if (result !== expect) { + throw "Unexpected input found"; + } + return result; + } + + /** + * Helper function to handle deserialized arrays. + * @returns {Array} + */ + function handleArray() { + let items = parseInt(readUntil(":"), 10) * 2; + expect("{"); + let result = []; + let isKey = true; + let lastItem = null; + for (let idx = 0; idx < items; idx++) { + let item = handleInput(); + if (isKey) { + lastItem = item; + isKey = false; + } else { + let 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("}"); + return result; + } + + + let kind = read(1).toLowerCase(); + + switch (kind) { + case "n": + expect(";"); + return ""; + + case "i": + case "d": + case "b": { + expect(":"); + let data = readUntil(";"); + if (kind === "b") { + return (parseInt(data, 10) !== 0); + } + return data; + } + + case "a": + expect(":"); + return "{" + handleArray() + "}"; + + case "s": { + expect(":"); + let length = readUntil(":"); + expect("\""); + let value = read(length); + expect("\";"); + if (args[0]) { + return "\"" + value.replace(/"/g, "\\\"") + "\""; + } else { + return "\"" + value + "\""; + } + } + + default: + throw "Unknown type: " + kind; + } + } + + let inputPart = input.split(""); + return handleInput(); + } + +}; + +export default PHP; diff --git a/test/index.js b/test/index.js index 773a5b14..748e1103 100644 --- a/test/index.js +++ b/test/index.js @@ -25,6 +25,7 @@ import "./tests/operations/Hash.js"; import "./tests/operations/Image.js"; import "./tests/operations/MorseCode.js"; import "./tests/operations/MS.js"; +import "./tests/operations/PHP.js"; import "./tests/operations/StrUtils.js"; import "./tests/operations/SeqUtils.js"; diff --git a/test/tests/operations/PHP.js b/test/tests/operations/PHP.js new file mode 100644 index 00000000..a42ee430 --- /dev/null +++ b/test/tests/operations/PHP.js @@ -0,0 +1,68 @@ +/** + * PHP tests. + * + * @author Jarmo van Lenthe + * + * @copyright Crown Copyright 2017 + * @license Apache-2.0 + */ + +import TestRegister from "../../TestRegister.js"; + +TestRegister.addTests([ + { + name: "PHP Deserialize empty array", + input: "a:0:{}", + expectedOutput: "{}", + recipeConfig: [ + { + op: "PHP Deserialize", + args: [true], + }, + ], + }, + { + name: "PHP Deserialize integer", + input: "i:10;", + expectedOutput: "10", + recipeConfig: [ + { + op: "PHP Deserialize", + args: [true], + }, + ], + }, + { + name: "PHP Deserialize string", + input: "s:17:\"PHP Serialization\";", + expectedOutput: "\"PHP Serialization\"", + recipeConfig: [ + { + op: "PHP Deserialize", + args: [true], + }, + ], + }, + { + name: "PHP Deserialize array (JSON)", + input: "a:2:{s:1:\"a\";i:10;i:0;a:1:{s:2:\"ab\";b:1;}}", + expectedOutput: "{\"a\": 10,\"0\": {\"ab\": true}}", + recipeConfig: [ + { + op: "PHP Deserialize", + args: [true], + }, + ], + }, + { + name: "PHP Deserialize array (non-JSON)", + input: "a:2:{s:1:\"a\";i:10;i:0;a:1:{s:2:\"ab\";b:1;}}", + expectedOutput: "{\"a\": 10,0: {\"ab\": true}}", + recipeConfig: [ + { + op: "PHP Deserialize", + args: [false], + }, + ], + }, +]);