linux端口复用隐藏后门
文章最后更新时间为:2021年03月25日 11:33:54
1. socket绑定端口的“常识”
在socket绑定端口中,我们都知道有以下几个特点::
socket可以指定绑定到一个特定的ip和port,例如绑定到192.168.1.11:9000上;
同时也支持通配绑定方式,即绑定到本地"any address"(例如一个socket绑定为 0.0.0.0:21,那么它同时绑定了所有的本地地址);
默认情况下,任意两个socket都无法绑定到同一个源IP地址和源端口。比如以下几种情况都会出错。
- 0.0.0.0:21和192.168.1.11:21
- 0.0.0.0:21和127.0.0.1:21
- 127.0.0.1:21和127.0.0.1:21
- (但是不同进程可以同时绑定到127.0.0.1:21和192.168.1.11:21)
使用代码验证,写一个简单的demo如下:
# server1.py
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("127.0.0.1", 8002))
s.listen(5)
while True:
c,addr = s.accept()
c.send(b'hello, welcome to server 1')
c.close()
# server2.py
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("0.0.0.0", 8002))
s.listen(5)
while True:
c,addr = s.accept()
c.send(b'hello, welcome to server2')
c.close()
# server3.py
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("192.168.1.3", 8002))
s.listen(5)
while True:
c,addr = s.accept()
c.send(b'hello, welcome to server3')
c.close()
执行结果
的确,看起来确实是不能绑定到同一个ip+同一个端口。
2. 怎么实现端口复用
实际上操作系统内核支持通过配置socket参数的方式来实现多个进程绑定同一个端口,查看linux的socket手册https://man7.org/linux/man-pages/man7/socket.7.html会发现两个socket options:SO_REUSEADDR和SO_REUSEPORT。
SO_REUSEADDR参数
SO_REUSEADDR参数提供了以下两个主要的作用:
1.改变了系统对处于TIME_WAIT状态的socket的看待方式(和端口复用无关,可忽略)
要理解这个句话,首先先简单介绍以下什么是处于TIME_WAIT状态的socket?
socket通常都有发送缓冲区,当调用send()函数成功后,只是将数据放到了缓冲区,并不意味着所有数据真正被发送出去。对于TCP
socket,在加入缓冲区和真正被发送之间的时延会相当长。这就导致当close一个TCP
socket的时候,可能在发送缓冲区中保存着等待发送的数据。为了确保TCP的可靠传输,TCP的实现是close一个TCP
socket时,如果它仍然有数据等待发送,那么该socket会进入TIME_WAIT状态。这种状态将持续到数据被全部发送或者发生超时(这个超时时间通常被称为Linger
Time,大多数系统默认为2分钟)。
在未设置SO_REUSEADDR时,内核将一个处于TIME_WAIT状态的socketA仍然看成是一个绑定了指定ip和port的有效socket,因此此时如果另外一个socketB试图绑定相同的ip和port都将失败,直到socketA被真正释放后,才能够绑定成功。如果socketB设置SO_REUSEADDR(仅仅只需要socketB进行设置),这种情况下socketB的绑定调用将成功返回,但真正生效需要在socketA被真正释放后。总结一下:内核在处理一个设置了SO_REUSEADDR的socket绑定时,如果其绑定的ip和port和一个处于TIME_WAIT状态的socket冲突时,内核将忽略这种冲突。
比如以下示例:(未设置SO_REUSEADDR,和一个TIME_WAIT状态的socket冲突)
2.改变了通配绑定时处理源地址冲突的处理方式
对于linux socket,当未设置SO_REUSEADDR时,socketA先绑定到0.0.0.0:21,后socketB绑定192.168.0.1:21将失败,因为0.0.0.0意味着“任何本地 IP 地址” ,因此该套接字将考虑使用所有本地 IP 地址,这也包括192.168.0.1
但在设置SO_REUSEADDR后socketB将绑定成功。
回到端口复用上面来,开启这个选项,不同的进程可以同时绑定在0.0.0.0:21和127.0.0.1:21了,访问时是根据ip地址来区别的。
SO_REUSEPORT参数
SO_REUSEPORT参数是在linux 3.9版本引入的。有以下作用:
1、允许将多个socket绑定到完全相同的地址和端口,前提每个socket绑定前都需设置SO_REUSEPORT选项
对于TCP socket,这个选项允许通过为多个线程使用同一个套接字来改进多线程服务器中的负载均衡。比如nginx1.9.1引入的新功能:https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/
demo:
# server1.py
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
s.bind(("127.0.0.1", 8002))
s.listen(5)
while True:
c, addr = s.accept()
c.send('hello, welcome to server 1\n'.encode())
c.close()
# server2.py
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
s.bind(("127.0.0.1", 8002))
s.listen(5)
while True:
c, addr = s.accept()
c.send('hello, welcome to server 2\n'.encode())
c.close()
内核对套接字之间的传入连接进行负载平衡,可以看出分配的socket进程是随机的。
那么是否可以使用该参数创建socket来绑定已使用的端口,和别人竞争socket连接,从而破坏系统?谁来保障我的socket不被别的进程偷走?
内核开发者肯定也考虑到了,做出了以下安全性的限制:
- 为了阻止Port hijacking,所有使用相同ip和port的socket都必须拥有相同的UID。
- 如果第一个绑定的socket未设置SO_REUSEPORT,那么其他的socket无论有没有设置SO_REUSEPORT都无法绑定到该地址和端口,直到第一个socket释放了绑定。
3. 利用端口重定向变相实现“端口复用”
直接使用端口复用无法攻击xpra&vnc,但是我们可以使用端口重定向实现端口复用功能从而实现攻击。
设想如下场景:服务器10.10.179.70对外只开放了80端口,我们要复用80端口访问22端口的ssh服务,同时不影响原80端口的web服务。
最终实现的效果如下:
这里我们可以使用iptables实现一个简单的端口重定向:
# 将发送本机80端口,源ip为10.10.200.171的流量重定向至本机 22 端口
sudo iptables -t nat -A PREROUTING -p tcp -s 10.10.200.171 --dport 80 -j REDIRECT --to-port 22
(删除这条规则sudo iptables -t nat -D PREROUTING 2)
然后查看本地的80端口没有任何异常,正常ip访问也是正常。
然后通过10.10.200.171的客户端来访问80端口连接ssh服务。
利用此种方式,可以通过端口重定向来实现端口复用,同时绕过对服务端端口+进程的审计。
其中iptables中端口重定向的判断条件可以多变,比如识别某个tcp包的规则,从而更加方便和隐蔽。