中间人攻击(Man-in-the-MiddleAttack,简称“MITM攻击”)是一种“间接”的入侵攻击,这种攻击模式是通过各种技术手段将受入侵者控制的一台计算机虚拟放置在网络连接中的两台通信计算机之间,这台计算机就称为'中间人',——解释来自于百度百科
我们常用的Fiddler和Charles,就是基于该原理实现的。
我们可以基于MITM做以下内容
- 请求劫持
- 伪造请求
- 过滤数据
- 调试接口
所以我们终极目标就是伪造一个代理服务器,用于建立一个可以同时与客户端和服务端进行通信的网络服务(欺骗客户端和服务端存放的中间人)
这个中间人充当的就是一个卧底的角色,所有请求和响应都经过他手,而服务器和客户端都不能感知到他的威胁
HTTP是明文传输的,所以在这里实现中间人代理就会非常简单
const http = require('http');
const url = require('url');
let httpMitmProxy = new http.Server();
// 启动端口
let port = 6789;
httpMitmProxy.listen(port, () => {
console.log(`HTTP中间人代理启动成功,端口:${port}`);
});
// 代理接收客户端的转发请求
httpMitmProxy.on('request', (req, res) => {
// 解析客户端请求
var urlObject = url.parse(req.url);
let options = {
protocol: 'http:',
hostname: req.headers.host.split(':')[0],
method: req.method,
port: req.headers.host.split(':')[1] || 80,
path: urlObject.path,
headers: req.headers
};
console.log(`请求方式:${options.method},请求地址:${options.protocol}//${options.hostname}:${options.port}${options.path}`);
// 根据客户端请求,向真正的目标服务器发起请求。
let realReq = http.request(options, (realRes) => {
// 设置客户端响应的http头部
Object.keys(realRes.headers).forEach(function (key) {
res.setHeader(key, realRes.headers[key]);
});
// 设置客户端响应状态码
res.writeHead(realRes.statusCode);
// 通过pipe的方式把真正的服务器响应内容转发给客户端
realRes.pipe(res);
});
// 通过pipe的方式把客户端请求内容转发给目标服务器
req.pipe(realReq);
realReq.on('error', (e) => {
console.error(e);
})
})具体源代码请看这里
回想上一节的http代理,我们是基于应用层的http协议实现的代理功能。由于http是明文传输,代理可以解析出客户端的真实的请求报文,并且拿着该请求报文"代表"客户端向真正的服务器发起请求。那么https是否仍然可以通过这样的方式进行代理?
先简单说下SSL/TLS,SSL/TLS协议是为了解决这三大风险而设计的
- 所有信息都是加密传播,第三方无法窃听。
- 具有校验机制,一旦被篡改,通信双方会立刻发现。
- 配备身份证书,防止身份被冒充。
基于SSL/TLS设计的第一点,可知代理是无法像解析单纯的http协议那样解析https的报文,从而也无法像代理http那样代理https。在实际网络中,每个网络请求都会经过各种个样的网络节点,代理也是其中很常见的一种。
https请求如何通过这些http代理节点连接到目标服务器?为了解决这类问题,http tunnel(隧道)技术就派上了用场。
| HTTP | HTTPS | |||
|---|---|---|---|---|
| http | 应用层 | |||
| 应用层 | HTTP | TCL/SSL | 安全层 | |
| TCP | --传输层--> | TCP | 传输层 | |
| IP | --网络层--> | IP | 网络层 | |
| 网络接口 | --数据链路层--> | 网络接口 | 数据链路层 | |
| 物理层 |
HTTPS的分层是在传输层之上建立了安全层,所有的HTTP请求都在安全层上传输。既然无法通过像代理一般HTTP请求的方式在应用层代理HTTPS请求,那么我们就退而求其次为在传输层为客户端和服务器建立起TCP连接。这种方式就像为客户端和服务器之间打通了一条TCP连接的隧道,作为HTTP代理对隧道里传输的内容一概不予理会,只负责传输。
- 第一步:客户端像http代理发起CONNECT请求。
CONNECT abc.com:443 HTTP/1.1-
第二步:http代理接收到CONNECT请求后与abc.com的433端口建立tcp连接。
-
第三步:与abc.com的433端口建立tcp连接成功,通知客户端。
HTTP/1.1 200 Connection Establishedconst http = require('http');
const url = require('url');
const net = require('net');
let httpTunnel = new http.Server();
// 启动端口
let port = 6789;
httpTunnel.listen(port, () => {
console.log(`HTTP中间人代理启动成功,端口:${port}`);
});
httpTunnel.on('error', (e) => {
if (e.code == 'EADDRINUSE') {
console.error('HTTP中间人代理启动失败!!');
console.error(`端口:${port},已被占用。`);
} else {
console.error(e);
}
});
// http服务器必须在connect里面才能监听到https请求
// https的请求通过http隧道方式转发
// 这里就是上面第一步:客户端像http代理发起CONNECT请求
httpTunnel.on('connect', (req, cltSocket, head) => {
// connect to an origin server
var srvUrl = url.parse(`http://${req.url}`);
console.log(`CONNECT ${srvUrl.hostname}:${srvUrl.port}`);
var srvSocket = net.connect(srvUrl.port, srvUrl.hostname, () => {
cltSocket.write('HTTP/1.1 200 Connection Established\r\n' +
'Proxy-agent: MITM-proxy\r\n' +
'\r\n');
srvSocket.write(head);
srvSocket.pipe(cltSocket);
cltSocket.pipe(srvSocket);
});
srvSocket.on('error', (e) => {
console.error(e);
});
});具体源代码请看这里
不获取明文的代理,这种方式就是利用http得到请求信息然后借助net模块模拟客户端的TCP请求,这个实现方式跟Node官方文档的connect事件例子非常相似
const url = require('url');
const http = require('http');
const net = require('net');
const {
port
} = config = {
port: 6789
};
const server = http.createServer((req, res) => {
// 代理HTTP
const ip = res.socket.remoteAddress;
const port = res.socket.remotePort;
res.end(`您的 IP 地址是 ${ip},您的源端口是 ${port}`);
});
// HTTP 隧道 代理HTTPS 流量 但由于加密 不能处理
server.on('connect', (req, socket) => {
console.log('connect', req.url);
const parsedUrl = url.parse('http://' + req.url);
const {
port,
hostname
} = parsedUrl;
const clientSocket = net.connect(port, hostname, () => {
socket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
// 异常必须监听,不然会出现错误
socket.on('error', (e) => {
console.log('socket', e);
socket.end();
})
clientSocket.pipe(socket);
}).on('error', e => {
console.log('net', e);
socket.end();
});
socket.pipe(clientSocket);
});
// 开启server 监听指定端口
server.listen(port, () => {
console.log('server start');
});
server.on('error', (e) => {
if (e.code == 'EADDRINUSE') {
console.error('HTTP中间人代理启动失败!!');
console.error(`端口:${port},已被占用。`);
} else if (e.code == 'ECONNRESET') {
console.error(e);
} else {
console.error(e);
}
});具体源代码请看这里
具体代码请看:生成根证书
执行完npm run createRootCA后,CA根证书的公私钥会生成到项目根路径的rootCA文件夹下
| 公钥文件 | rootCA/rootCA.crt |
| 私钥文件 | rootCA/rootCA.key.pem |
- 1.必须要按照上面步骤先生成CA证书相关文件
- 2.每一次生成的证书和密钥都是独一无二的。
在MAC系统下找到钥匙串访问,然后双击打开证书文件rootCA/rootCA.crt完成安装,并且有可能需要在信任-使用此证书时里面设置始终信任。
或者在项目根路径下执行下面命令,然后输入用户密码或者指纹后即可安装成功。
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain rootCA/rootCA.crt执行完npm run createCertByRootCA后,CA子证书的公私钥会生成到项目根路径的cert文件夹下
| 公钥文件 | cert/my.crt |
| 私钥文件 | cert/my.key.pem |
另外和CA根证书最大的不同是,该子证书是用CA根证书的私钥签名,而CA根证书是用自己的私钥自签名。这也从代码的角度认识到了证书链的原理
// 用CA根证书私钥签名
cert.sign(caKey, forge.md.sha256.create());配合HTTP隧道和证书实现HTTPS代理,具体源代码请看https.js和createFakeHttpsWebSite.js
在系统偏好设置中打开网络
选择代理,只勾选网页代理(HTTP),设置右边的网页代理服务器和安全网页代理设置,可以把它设置为127.0.0.1:6789




