Skip to content

Instantly share code, notes, and snippets.

@Wh1terat
Last active May 30, 2023 04:36
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Wh1terat/f78416e4c681becb5bdf0a646aa37566 to your computer and use it in GitHub Desktop.
Save Wh1terat/f78416e4c681becb5bdf0a646aa37566 to your computer and use it in GitHub Desktop.
Incapsula JS Deobfuscator (obfuscator.io) - JS
#!/usr/bin/env node
var fs = require('fs');
var esprima = require('esprima');
var escodegen = require('escodegen');
var estraverse = require('estraverse');
var debug = true;
var rename = true;
var stringrotatefunc = `
(function (array, times) {
var whileFunction = function (times) {
while (--times) {
array['push'](array['shift']());
}
};
var selfDefendingFunc = function () {
var object = {
'data': {
'key': 'cookie',
'value': 'timeout'
},
'setCookie': function (options, name, value, document) {
document = document || {};
var updatedCookie = name + '=' + value;
var i = 0;
for (var i = 0, len = options['length']; i < len; i++) {
var propName = options[i];
updatedCookie += '; ' + propName;
var propValue = options[propName];
options['push'](propValue);
len = options['length'];
if (propValue !== true) {
updatedCookie += '=' + propValue;
}
}
document['cookie'] = updatedCookie;
},
'removeCookie': function () {
return 'dev';
},
'getCookie': function (document, name) {
document = document || function (value) {
return value;
};
var matches = document(new RegExp('(?:^|; )' + name['replace'](/([\.$?*|{}\(\)\[\]\\\/+^])/g, '$1') + '=([^;\]*)'));
var func = function (param1, param2) {
param1(++param2);
};
func(whileFunction, times);
return matches ? decodeURIComponent(matches[1]) : undefined;
}
};
var test1 = function () {
var regExp = new RegExp('\\\\w+ *\\\\(\\\\) *{\\\\w+ *[\\'|"].+[\\'|"];? *}');
return regExp['test'](object['removeCookie']['toString']());
};
object['updateCookie'] = test1;
var cookie = '';
var result = object['updateCookie']();
if (!result) {
object['setCookie'](['*'], 'counter', 1);
} else if (result) {
cookie = object['getCookie'](null, 'counter');
} else {
object['removeCookie']();
}
};
selfDefendingFunc();
}(stringArray, 123));
`
var callwrapper = `
var stringArrayCallsWrapper = function (index, key) {
index = index - 0;
var value = stringArray[index];
if (stringArrayCallsWrapper['initialized'] === undefined) {
(function () {
var getGlobal = Function('return (function() ' + '{}.constructor("return this")( )' + ');');
var that = getGlobal();
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
that['atob'] || (that['atob'] = function (input) {
var str = String(input)['replace'](/=+$/, '');
for (var bc = 0, bs, buffer, idx = 0, output = '';
buffer = str['charAt'](idx++); ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer, bc++ % 4) ? output += String['fromCharCode'](255 & bs >> (-2 * bc & 6)) : 0) {
buffer = chars['indexOf'](buffer);
}
return output;
});
}());
var rc4 = function (str, key) {
var s = [], j = 0, x, res = '', newStr = '';
str = atob(str);
for (var k = 0, length = str['length']; k < length; k++) {
newStr += '%' + ('00' + str['charCodeAt'](k)['toString'](16))['slice'](-2);
}
str = decodeURIComponent(newStr);
for (var i = 0; i < 256; i++) {
s[i] = i;
}
for (i = 0; i < 256; i++) {
j = (j + s[i] + key['charCodeAt'](i % key['length'])) % 256;
x = s[i];
s[i] = s[j];
s[j] = x;
}
i = 0;
j = 0;
for (var y = 0; y < str['length']; y++) {
i = (i + 1) % 256;
j = (j + s[i]) % 256;
x = s[i];
s[i] = s[j];
s[j] = x;
res += String['fromCharCode'](str['charCodeAt'](y) ^ s[(s[i] + s[j]) % 256]);
}
return res;
};
stringArrayCallsWrapper['rc4'] = rc4;
stringArrayCallsWrapper['data'] = {};
stringArrayCallsWrapper['initialized'] = !![];
}
var cachedValue = stringArrayCallsWrapper['data'][index];
if (cachedValue === undefined) {
if (stringArrayCallsWrapper['once'] === undefined) {
var StatesClass = function (rc4Bytes) {
this['rc4Bytes'] = rc4Bytes;
this['states'] = [
1,
0,
0
];
this['newState'] = function () {
return 'newState';
};
this['firstState'] = '\\\\w+ *\\\\(\\\\) *{\\\\w+ *';
this['secondState'] = '[\\'|"].+[\\'|"];? *}';
};
StatesClass['prototype']['checkState'] = function () {
var regExp = new RegExp(this['firstState'] + this['secondState']);
return this['runState'](regExp['test'](this['newState']['toString']()) ? --this['states'][1] : --this['states'][0]);
};
StatesClass['prototype']['runState'] = function (stateResult) {
if (!Boolean(~stateResult)) {
return stateResult;
}
return this['getState'](this['rc4Bytes']);
};
StatesClass['prototype']['getState'] = function (rc4Bytes) {
for (var i = 0, len = this['states']['length']; i < len; i++) {
this['states']['push'](Math['round'](Math['random']()));
len = this['states']['length'];
}
return rc4Bytes(this['states'][0]);
};
new StatesClass(stringArrayCallsWrapper)['checkState']();
stringArrayCallsWrapper['once'] = !![];
}
value = stringArrayCallsWrapper['rc4'](value, key);
stringArrayCallsWrapper['data'][index] = value;
} else {
value = cachedValue;
}
return value;
};
`
var singlenode = `
var singleNodeCallControllerFunction = function () {
var firstCall = !![];
return function (context, fn) {
var rfn = firstCall ? function () {
if (fn) {
var res = fn['apply'](context, arguments);
fn = null;
return res;
}
} : function () {
};
firstCall = ![];
return rfn;
};
}();
`
var debugprotection = `
function debugProtectionFunction() {
function debuggerProtection(counter) {
if (('' + counter / counter)['length'] !== 1 || counter % 20 === 0) {
(function () {
}['constructor']('debugger')());
} else {
(function () {
}['constructor']('debugger')());
}
return debuggerProtection(++counter);
}
try {
return debuggerProtection(0);
} catch (y) {
}
}
`
var debugInterval = `
function debugProtectionFunctionInterval() {
if (new windowfoo['Date']()['getTime']() - initTime > 500) {
debugProtectionFunction();
}
}
`
var unicodeprotection = `
var selfDefendingFunction = singleNodeCallControllerFunction(this, function () {
var func1 = function () {
return 'dev';
}, func2 = function () {
return 'window';
};
var test1 = function () {
var regExp = new RegExp('\\\\w+ *\\\\(\\\\) *{\\\\w+ *[\\'|"].+[\\'|"];? *}');
return !regExp['test'](func1['toString']());
};
var test2 = function () {
var regExp = new RegExp('(\\\\[x|u](\\\\w){2,4})+');
return regExp['test'](func2['toString']());
};
var recursiveFunc1 = function (string) {
var i = ~-1 >> 1 + 255 % 0;
if (string['indexOf']('i' === i)) {
recursiveFunc2(string);
}
};
var recursiveFunc2 = function (string) {
var i = ~-4 >> 1 + 255 % 0;
if (string['indexOf']((!![] + '')[3]) !== i) {
recursiveFunc1(string);
}
};
if (!test1()) {
if (!test2()) {
recursiveFunc1('indеxOf');
} else {
recursiveFunc1('indexOf');
}
} else {
recursiveFunc1('indеxOf');
}
});
selfDefendingFunction();
`
function rc4 (str, key) {
var s = [], j = 0, x, res = '', newStr = '';
str = Buffer.from(str, 'base64').toString('utf8');
for (var k = 0, length = str.length; k < length; k++) {
newStr += '%' + ('00' + str.charCodeAt(k).toString(16)).slice(-2);
}
str = unescape(newStr);
for (var i = 0; i < 256; i++) {
s[i] = i;
}
for (i = 0; i < 256; i++) {
j = (j + s[i] + key.charCodeAt(i % key.length)) % 256;
x = s[i];
s[i] = s[j];
s[j] = x;
}
i = 0;
j = 0;
for (var y = 0; y < str.length; y++) {
i = (i + 1) % 256;
j = (j + s[i]) % 256;
x = s[i];
s[i] = s[j];
s[j] = x;
res += String.fromCharCode(str.charCodeAt(y) ^ s[(s[i] + s[j]) % 256]);
}
return res;
}
function debug_log(){
if (debug) console.log.apply(null, arguments);
}
function compare_nodes(node, pattern) {
if (Array.isArray(pattern)) {
if (node.length !== pattern.length) return false;
return node && pattern.every(function(val, index) {
return compare_nodes(node[index], val);
});
} else if (typeof pattern !== "object" || pattern == null) {
return node === pattern;
} else {
return node && Object.keys(pattern).every(function(key) {
// Allow for loose matching
if (['id', 'name','value','raw'].indexOf(key) > -1 ) return true;
return compare_nodes(node[key], pattern[key]);
});
}
};
var args = process.argv.slice(2);
fs.readFile(args[0], 'utf8', function(err, contents) {
var initial = esprima.parseScript(contents);
var ast = esprima.parseScript(contents);
estraverse.traverse(initial, {
enter: function(node,parent){
if (node.type == 'VariableDeclarator' && node.init.value.length > 100)
{
ast = esprima.parseScript(Buffer.from(node.init.value, 'hex').toString());
}
}
})
// Write out intermediate step
var code = escodegen.generate(ast);
fs.writeFileSync(`stage1-${args[0]}`, code, function (err) {
if (err) return console.log(err);
});
fs.writeFileSync(`ast-${args[0]}`, ast, function (err) {
if (err) return console.log(err);
});
// Find string array rotate function
var stringarrays = [];
estraverse.replace(ast, {
enter: function (node, parent) {
if (compare_nodes(node, esprima.parseScript(stringrotatefunc).body[0])) {
debug_log('Found StringRotate func!');
var arrname = node.expression.arguments[0].name;
var arrshift = node.expression.arguments[1].value;
estraverse.replace(ast, {
enter: function (node, parent) {
if (node.type == 'VariableDeclaration' && node.declarations[0].id.name == arrname){
while(arrshift--)node.declarations[0].init.elements.push(node.declarations[0].init.elements.shift());
stringarrays.push({
name: arrname,
elements: node.declarations[0].init.elements
})
this.remove();
}
}
});
// huh? Removing the rotate function seems to remove callwrapper ?!
//this.remove();
}
}
});
debug_log(stringarrays);
// Find string array call wrapper
var callwrappers = [];
estraverse.replace(ast, {
enter: function (node, parent) {
if (compare_nodes(node, esprima.parseScript(callwrapper).body[0])) {
debug_log('Found CallWrapper func!')
callwrappers.push({
name: node.declarations[0].id.name,
stringarray: node.declarations[0].init.body.body[1].declarations[0].init.object.name
});
this.remove();
}
}
});
debug_log(callwrappers);
// Fix string array calls
for (callwrapper of callwrappers){
var stringarray = stringarrays.find(x => x.name === callwrapper.stringarray);
estraverse.replace(ast, {
enter: function (node, parent){
if (node.type == 'CallExpression' && node.callee.name == callwrapper.name) {
if (node.arguments[1].type == 'Literal') {
key = node.arguments[1].value;
} else if (node.arguments[1].type == 'BinaryExpression') {
var left = node.arguments[1].left.name;
var right = node.arguments[1].right.name;
var tmpkname,tmpkey,tmpkoff,tmpkskip = null;
estraverse.traverse(ast, {
enter: function (node, parent){
if (node.type =='ForStatement' && node.body.body[0].type == 'ExpressionStatement') {
if (node.body.body[0].expression.left.name == left){
tmpkname = node.body.body[0].expression.right.object.name;
tmpkoff = node.test.right.value - 1;
estraverse.traverse(parent, {
enter: function(node){
if (node.type == 'VariableDeclaration' && node.declarations[0].id.name == tmpkname){
tmpkey = node.declarations[0].init.value;
this.break;
}
}
});
} else if (node.body.body[0].expression.left.name == right){
tmpkskip = node.init.declarations[0].init.value;
this.break;
}
}
}
});
debug_log('Found Funky Thing!')
key = tmpkey.substr(0, tmpkoff) + tmpkey.substr(tmpkskip)
} else {
debug_log('ERR: Bad String Wrapper:');
debug_log(node);
return false
}
idx = parseInt(node.arguments[0].value, 16);
raw = stringarray.elements[idx].value;
value = rc4(raw, key);
if (debug){
debug_log(`Index: ${idx}, Raw:${raw}, Key:${key}, Value:${value}`);
}
return {
type: 'Literal',
value: value,
raw: value
}
}
}
});
}
// Write out intermediate step
var code = escodegen.generate(ast);
fs.writeFileSync(`stage2-${args[0]}`, code, function (err) {
if (err) return console.log(err);
});
// Fix control flow (switchyness)
var switchreg = new RegExp("(\\d+\\|)+\\d+");
estraverse.replace(ast, {
enter: function(node,parent){
try {
if (node.type =='VariableDeclaration' &&
node.declarations[0].init.callee.property.value == 'split' &&
switchreg.test(node.declarations[0].init.callee.object.value))
{
var name = node.declarations[0].id.name;
var order = node.declarations[0].init.callee.object.value.split('|');
debug_log(order);
estraverse.replace(parent, {
enter: function(node,parent){
if (node.type == 'WhileStatement' && node.body.body[0].discriminant.object.name == name){
for (idx of order){
node.body.body[0].cases[idx].consequent.forEach(function(consequent) {
if (consequent.type != 'ContinueStatement'){
parent.body.push(consequent);
}
})
}
this.remove();
}
}
})
this.remove();
}
} catch (ignore) {}
}
})
// Write out intermediate step
var code = escodegen.generate(ast);
fs.writeFileSync(`stage3-${args[0]}`, code, function (err) {
if (err) return console.log(err);
});
// Fix control flow (indirection)
estraverse.replace(ast, {
enter: function(node,parent){
try {
if(node.declarations[0].init.properties.every(function(prop){
if (prop.value.type == 'FunctionExpression' && prop.value.body.body[0].type == 'ReturnStatement') return true
})) {
for (func of node.declarations[0].init.properties){
foo = node;
estraverse.replace(ast, {
enter: function(node, parent){
try {
if (node.type == 'CallExpression' && node.callee.object.name == foo.declarations[0].id.name && node.callee.property.value == func.key.value){
bar = Object.assign({},func.value.body.body[0].argument);
if (bar.type == 'BinaryExpression'){
bar.left = node.arguments[0];
bar.right = node.arguments[1];
return bar;
} else if (bar.type == 'CallExpression'){
bar.callee.name = node.arguments[0].name;
bar.arguments = node.arguments.slice(1);
return bar;
}
}
} catch (ignore) {}
}
});
}
this.remove();
}
} catch (ignore) {}
}
});
// Write out intermediate step
var code = escodegen.generate(ast);
fs.writeFileSync(`stage4-${args[0]}`, code, function (err) {
if (err) return console.log(err);
});
// Fix up loose ends
estraverse.replace(ast, {
enter: function (node, parent) {
// Remove junk
if (compare_nodes(node,esprima.parse(singlenode).body[0])){
debug_log('Removing singlenode');
this.remove();
}
if (compare_nodes(node,esprima.parse(debugprotection).body[0])){
debug_log('Removing debugprotection');
this.remove();
}
if (compare_nodes(node,esprima.parse(debugInterval).body[0])){
debug_log('Removing debugInterval(and calls to it)');
name = node.id.name;
estraverse.replace(ast, {
enter: function (node, parent) {
if (node.type == 'ExpressionStatement' && node.expression.type == 'CallExpression' && node.expression.callee.name == name) {
this.remove();
} else if (node.type == 'VariableDeclarator' && node.init && node.init.type == 'CallExpression' && node.init.callee.name == name){
newnode = Object.assign({}, node);
newnode.init = {
type: 'Literal',
value: 1
}
return newnode;
}
}
});
this.remove();
}
if (compare_nodes(node,esprima.parse(unicodeprotection).body[0])){
debug_log('Removing unicodeprotection')
this.remove();
}
if (compare_nodes(node,esprima.parse(stringrotatefunc).body[0])){
debug_log('Removing stringrotatefunc')
this.remove();
}
if (rename){
// Rename idents
// Not scope aware but meh
var ecma_reserved = ['break','case','catch','class','const','continue','debugger','default','delete','do','else','export',
'extends','finally','for','function','if','import','in','instanceof','new','return','super','switch',
'this','throw','try','typeof','var','void','while','with','yield','enum','implements','interface','let',
'package','private','protected','await','abstract','boolean','byte','char','double','final','float',
'goto','int','long','native','short']
var idents = []
estraverse.replace(ast, {
enter: function(node,parent){
if (node.type == 'Identifier' && String(node.name).startsWith('_0x')){
if(idents.find(x => x.oldname === node.name)){
node.name = idents.find(x => x.oldname === node.name).newname;
} else {
newname = (function(){
while(true){
//var chars = "ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
//var string_length = 3;
var chars = "abcdefghiklmnopqrstuvwxyz";
var string_length = 4;
var randomstring = '';
for (var i=0; i<string_length; i++) {
var rnum = Math.floor(Math.random() * chars.length);
randomstring += chars.substring(rnum,rnum+1);
}
if (ecma_reserved.indexOf(randomstring) < 0 && !idents.find(x => x.oldname === randomstring) && !idents.find(x => x.newname === randomstring)){
return randomstring;
}
}
})();
idents.push({
oldname: node.name,
newname: newname
})
debug_log(`Renaming Literal: ${node.name} to: ${newname}`);
node.name = newname;
}
}
}
})
}
// Change calls from foo['bar']() to foo.bar()
if (node.type == 'MemberExpression' && node.computed == true && typeof node.property.value != "undefined" && isNaN(node.property.value)) {
debug_log('Fixing calls')
return {
type: 'MemberExpression',
computed: false,
object: node.object,
property: {
type: 'Identifier',
name: node.property.value
}
}
}
// Fix true/false statements
if (node.type == 'UnaryExpression' && node.operator == '!'){
if (node.argument.type == 'UnaryExpression' && node.argument.operator == '!'){
if (node.argument.argument.type == 'ArrayExpression' && node.argument.argument.elements.length == 0){
debug_log('Fixing true/false statement')
return {
type: 'Literal',
value: true
}
}
} else if (node.argument.type == 'ArrayExpression' && node.argument.elements.length == 0){
debug_log('Fixing true/false statement')
return {
type: 'Literal',
value: false
}
}
}
}
});
//if (debug) fs.writeFileSync('test2.ast', JSON.stringify(ast, null, 4));
var code = escodegen.generate(ast);
fs.writeFileSync(`deob-${args[0]}`, code, function (err) {
if (err) return console.log(err);
});
});
@angkk2u
Copy link

angkk2u commented Feb 5, 2020

It is not work for me.

I met the error below message.

if (node.type == 'VariableDeclarator' && node.init.value.length > 100)
^
TypeError: Cannot read property 'length' of undefined

Do you have any idea?

@Wh1terat
Copy link
Author

Wh1terat commented Feb 5, 2020

@angkk2u:
This is only for Incapsula - who use a forked version of obfuscator.io.
I've just tested an incapsula site and it's still working.

Can you attach the JS you're trying to work with ?

@xOnlyFadi
Copy link

how to use this script it confused me

@Wh1terat
Copy link
Author

how use this js?

If you don't understand it then you have no business using it.

@sugappa
Copy link

sugappa commented Dec 31, 2020

not worked for me `PS C:\Users\sugar\Desktop\1> node deob.js
C:\Users\sugar\Desktop\1\deob.js:304
if (node.type == 'VariableDeclarator' && node.init.value.length > 100)
^

TypeError: Cannot read property 'length' of undefined
at Controller.enter (C:\Users\sugar\Desktop\1\deob.js:304:70)
at Controller.__execute (C:\Users\sugar\Desktop\1\node_modules\�[4mestraverse�[24m\estraverse.js:332:31)
at Controller.traverse (C:\Users\sugar\Desktop\1\node_modules\�[4mestraverse�[24m\estraverse.js:445:28)
at Object.traverse (C:\Users\sugar\Desktop\1\node_modules\�[4mestraverse�[24m\estraverse.js:666:27)
at C:\Users\sugar\Desktop\1\deob.js:302:16
�[90m at FSReqCallback.readFileAfterClose [as oncomplete] (internal/fs/read_file_context.js:63:3)�[39m
PS C:\Users\sugar\Desktop\1>`
js: https://pastebin.com/raw/adsM4kbL
pls help me mate

@Wh1terat
Copy link
Author

Wh1terat commented Jan 4, 2021

@sugappa:
That looks like it's obfuscator.io rather than incapsula specifically - whilst incapsula use obfuscator.io, they use a particular version and set of options and they also wrap the code in an extra layer.

So you can remove that initial unwrap:

    //var initial = esprima.parseScript(contents);
    var ast = esprima.parseScript(contents);
    /*estraverse.traverse(initial, {
        enter: function(node,parent){
            if (node.type == 'VariableDeclarator' && node.init.value.length > 100)
            {   
                ast = esprima.parseScript(Buffer.from(node.init.value, 'hex').toString());
            }
        }
    })*/

So whilst the guts of the script would work, you'd need to work out which options were used when the script was encoded.
Also you'd need to work out which version of obfuscator.io was used and make sure the templates match that. (e.g stringrotatefunc,callwrapper,etc).

@nullableVoidPtr
Copy link

What exactly is the difference between obfuscator.io and Incapsula? I have written a proper deobfuscator for the former so I am curious.

@Wh1terat
Copy link
Author

@nullableVoidPtr:
Incapsula is just a bot protection product that uses dynamically generated JS wrapped in obfuscator.io, the js has a token used to generate a cookie assuming the browser passes the bot tests.

I had a personal project to scrape some content from a site protected by Incapsula and then went down the rabbit hole of this, more of a learning exercise of navigating ast than anything.

Just seen your project - wish it had existed when I started looking at this!

@nullableVoidPtr
Copy link

Thank you for the prompt reply.

Just seen your project - wish it had existed when I started looking at this!

No problem! Though I do feel it can still be more robust in some areas.

@m-abu
Copy link

m-abu commented Jul 29, 2021

I am totally new to this but I gave this a try and it seems to be partially working for me .. I managed to workaround the options as you mentioned and got the 4 stages. However, I am not able to see or construct the actual URL that I should call eventually to bypass Incapsula but maybe I am missing something.

@Wh1terat
Copy link
Author

@m-abu:
Incapsula's role is to generate a cookie, not a final URL.

There's some stuff that's changed with regard to extracting the elements needed to generate that cookie - a few releases ago incapsula changed some stuff to do with that - more obfuscation.

@alexnrj
Copy link

alexnrj commented Nov 30, 2021

Hello @Wh1terat I'm using your code to analyze incapsula resources SWJIYLWA. Using three samples for two of them the code throw error generating stage2. Is that normal? This one of files that doesn't work https://pastebin.com/Z25E03z7

@egerdnc
Copy link

egerdnc commented Dec 16, 2021

Hi @Wh1terat i saw your reply to "sugappa" and commented those lines you just mentioned but right after this
the file has been created but also throw an error

Some help would be really useful ❤️

C:\deobf>node deobfuscate.js server.js
node:internal/fs/utils:879
  throw new ERR_INVALID_ARG_TYPE(
  ^

TypeError [ERR_INVALID_ARG_TYPE]: The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received an instance of Script
←[90m    at Object.writeFileSync (node:fs:2146:5)←[39m
    at C:\deobf\deobfuscate.js:324:8
←[90m    at FSReqCallback.readFileAfterClose [as oncomplete] (node:internal/fs/read_file_context:68:3)←[39m {
  code: ←[32m'ERR_INVALID_ARG_TYPE'←[39m
}

Media:
https://prnt.sc/23749n1

Code:
https://www.toptal.com/developers/hastebin/deloqobuyi.md

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment