Skip to content

Instantly share code, notes, and snippets.

@greim
Last active September 18, 2022 06:44
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save greim/35b806f05e393b14c1e4 to your computer and use it in GitHub Desktop.
Save greim/35b806f05e393b14c1e4 to your computer and use it in GitHub Desktop.
HTTPS MITM Proxy Proof of Concept
import https from 'https'
import http from 'http'
import url from 'url'
import adapt from 'ugly-adapter' // callback => promise adapter; need to npm install this
import tls from 'tls'
import net from 'net'
import fs from 'fs'
import os from 'os'
import path from 'path'
import childProcess from 'child_process'
require('babel/polyfill')
// This is a proof of concept for a MITM https proxy. It requires creating your
// own root certificate authority as described here:
// https://github.com/coolaj86/node-ssl-root-cas/wiki/Painless-Self-Signed-Certificates-in-node.js
// You must then set your http client to trust it, or none of this will work.
// This was built as part of researching possibilities for my http debugging proxy
// project: https://github.com/greim/hoxy
// To run this: `babel https-mitm-proxy-poc.js | node`
// ------------------------------------------
// Start the proxy, passing in paths to your root key and CA files.
startProxy({
port: 8080,
rootKey: `${__dirname}/my-private-root-ca.key.pem`,
rootCert: `${__dirname}/my-private-root-ca.crt.pem`,
}).catch(err => {
console.error(err.stack)
process.exit(1)
})
// ------------------------------------------
// This is the main function that starts the proxy running.
async function startProxy({ port, rootKey, rootCert }) {
// ------------------------------------------
// A few things we'll use later.
let read = adapt.part(fs.readFile)
, key = await read(rootKey)
, cert = await read(rootCert)
, SNICallback = createSNICallback(rootKey, rootCert)
, tlsSocketPath = await ensureSocket('foobar.sock')
, connectOpts = { path: tlsSocketPath }
, cxnEstablished = 'HTTP/1.1 200 Connection Established\r\nProxy-agent: foobar\r\n\r\n'
// ------------------------------------------
// Create the main proxy. Make your client use this for http AND https
// proxying. SSL termination happens before this is reached, which is why
// this isn't an https server. Importantly, this server will "un-terminate"
// SSL if the incoming request was https, so the remote server won't know
// the connection is being MITM'd.
let proxy = http.createServer((fromClient, toClient) => {
let purl = url.parse(fromClient.url)
, h = purl.protocol === 'https:' ? https : http // un-terminate
let toServer = h.request({
hostname: purl.hostname,
port: purl.port,
method: fromClient.method,
path: purl.path,
headers: fromClient.headers,
}, fromServer => {
toClient.writeHead(fromServer.statusCode, fromServer.headers)
fromServer.pipe(toClient)
})
fromClient.pipe(toServer)
}).listen(port)
console.log(`proxy listening on ${port}`)
// ------------------------------------------
// On this event, clients are asking to be tunneled out to a remote host.
// Instead, we'll connect them our own https server which impersonates the
// remote host. The client will establish a tls connection on this socket,
// but we'll let the impersonation server handle that and just forward it
// using net.connect().
proxy.on('connect', (request, clientSocket, head) => {
let serverSocket = net.connect(connectOpts, () => {
clientSocket.write(cxnEstablished) // tell client "we'll do it"
serverSocket.write(head)
clientSocket
.pipe(serverSocket)
.pipe(clientSocket)
})
})
// ------------------------------------------
// Create the SSL impersonation server. The above 'connect' event is sending
// traffic here. This terminates SSL and forwards cleartext to the main proxy
// server, created above. 'key' and 'cert' aren't very important here; the
// key bits happen in 'SNICallback' described below.
https.createServer({ key, cert, SNICallback }, (fromClient, toClient) => {
let shp = 'https://' + fromClient.headers.host
, fullUrl = shp + fromClient.url
let toServer = http.request({
host: 'localhost',
port: port,
method: fromClient.method,
path: fullUrl,
headers: fromClient.headers,
}, fromServer => {
toClient.writeHead(fromServer.statusCode, fromServer.headers)
fromServer.pipe(toClient)
})
fromClient.pipe(toServer)
}).listen(tlsSocketPath)
console.log(`https server listening on ${tlsSocketPath}`)
}
// ------------------------------------------
// Not really relevant, jut a convenience preventing having to manually
// delete socket files.
async function ensureSocket(socket) {
let socketPath = path.normalize(`${os.tmpDir()}/${socket}`)
try { await adapt(fs.unlink, socketPath) }
catch(ex) { }
return socketPath
}
// ------------------------------------------
// Creates a callback that plugs into the https impersonation server. The
// callback uses the provided root CA and key to create on-the-fly certs for
// each host. It uses caching to prevent having to re-generate lots of certs
// and slow things down.
function createSNICallback(rootKey, rootCert) {
let cache = new Map()
, exec = adapt.part(childProcess.exec)
, read = adapt.part(fs.readFile)
async function getCtx(serverName) {
let ctx = cache.get(serverName)
if (!ctx) {
console.log(`creating cert for ${serverName}`)
ctx = await genCtx(serverName)
cache.set(serverName, ctx)
}
return ctx
}
async function genCtx(serverName) {
let tmpDir = os.tmpDir()
, now = Date.now()
, rand = (Math.random()+'').replace(/\D/g, '')
, slug = `${now}-${rand}`
, serverKey = `${tmpDir}/${slug}.key.pem`
, serverCsr = `${tmpDir}/${slug}.csr.pem`
, serverCrt = `${tmpDir}/${slug}.crt.pem`
, genKeyCmd = `openssl genrsa -out ${serverKey} 2048`
, genCsrCmd = `openssl req -new -key ${serverKey} -out ${serverCsr} -subj "/C=US/ST=Utah/L=Provo/O=ACME Tech Inc/CN=${serverName}"`
, genCrtCmd = `openssl x509 -req -in ${serverCsr} -CA ${rootCert} -CAkey ${rootKey} -CAcreateserial -out ${serverCrt} -days 500`
await exec(genKeyCmd)
await exec(genCsrCmd)
await exec(genCrtCmd)
let key = await read(serverKey)
, cert = await read(serverCrt)
, ctx = tls.createSecureContext({ key, cert })
return ctx
}
// return a SNICallback
return (serverName, cb) => {
getCtx(serverName).then(ctx => cb(null, ctx), err => {
console.error(err.stack)
//process.exit(1)
cb(err)
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment