当“同时发生”成为攻击武器

概述 (Overview)

假设我们正在测试一个在线购物 Web 应用程序的安全性。这时可能会引出许多问题:我们能否用一张 10 美元的礼品卡支付 100 美元的商品?我们能否多次在购物车中应用相同的折扣?答案是:可能!如果系统容易受到竞争条件漏洞的影响,我们就能做到这些,甚至更多。

竞争条件 (Race Condition) 是一种发生在计算机程序中的情况,其中事件的时序(timing)或顺序会意外地影响程序的行为和最终结果。这种情况通常发生在多个执行线程(或进程)并发访问和修改同一个共享资源(如变量、文件或数据库记录)时。由于缺乏适当的同步机制(如锁),不同的线程操作可能会交错执行,导致非预期的状态。

攻击者可能利用这种时序上的差异来滥用系统,例如:

  • 多次应用同一个一次性折扣。
  • 进行超出账户余额的货币交易。
  • 绕过某些检查或限制。

核心概念:多线程与并发 (Core Concepts: Multi-threading and Concurrency)

要理解竞争条件,首先需要了解程序、进程和线程的概念。

1. 程序 (Program)
  • 定义: 程序是一组用于完成特定任务的静态指令集。它就像一个食谱,包含了步骤,但本身不会执行任何操作。

  • 示例 : 下面的 Python Flask 代码是一个简单的 Web 服务器程序,定义了如何响应根 URL 的请求。但只有运行它,它才会实际监听端口并提供服务。

    python 复制代码
    # 导入 Flask 类
    from flask import Flask
    
    # 创建 Flask 应用实例
    app = Flask(__name__)
    
    # 定义根路由 ('/')
    @app.route('/')
    def hello_world():
        # 当访问根 URL 时执行此函数
        # 返回一个简单的 HTML 页面
        return '<html><head><title>Greeting</title></head><body><h1>Hello, World!</h1></body></html>'
    
    # 检查脚本是否被直接运行
    if __name__ == '__main__':
        # 运行 Flask 应用
        # host='0.0.0.0' 使服务器可从任何 IP 访问
        # port=8080 指定监听端口
        app.run(host='0.0.0.0', port=8080)
2. 进程 (Process)
  • 定义 : 进程是正在执行中的程序实例。它是一个动态实体,拥有自己的内存空间、状态(如运行、等待、就绪)以及程序代码。可以将进程看作是"正在按照食谱做菜"的活动。
  • 关键方面 :
    • 程序代码: 需要执行的指令。
    • 内存: 存储运行时数据(变量、堆栈等)。
    • 状态: 进程在其生命周期中会经历不同状态(新建、就绪、运行、等待、终止)。
  • 示例: 当我们运行上面的 Flask 代码时,操作系统会创建一个进程。这个进程会监听 8080 端口。它大部分时间处于"等待"状态,等待 HTTP 请求。收到请求后,它变为"就绪",等待 CPU 分配时间片。获得 CPU 后进入"运行"状态,处理请求(发送 HTML),然后通常返回"等待"状态。在单进程、单线程模式下,它一次只能处理一个请求,其他请求需要排队。
3. 线程 (Thread)
  • 定义 : 线程是进程内的一个轻量级执行单元。一个进程可以包含一个或多个线程,它们共享进程的内存空间和资源(如代码段、全局变量),但拥有各自的程序计数器、寄存器和栈。可以将进程比作一个工厂,而线程是工厂里的工人,可以同时处理不同的任务(或同一任务的不同部分)。

  • 优势: 相比于为每个任务创建新进程(开销大),使用多线程可以在同一进程内实现并发,提高效率和响应性,尤其是在处理 I/O 密集型任务(如 Web 请求)时。

  • 并发模型 :

    • 串行 (Serial): 单进程单线程,一次处理一个请求,后续请求排队。
    • 并行/并发 (Parallel/Concurrent) :
      • 多进程:启动多个独立的进程处理请求(如 Gunicorn 的 worker 进程)。
      • 多线程:单个进程内创建多个线程处理请求(如 Flask 默认模式,或 Gunicorn 的 --threads 选项)。
  • 示例 : 使用 Gunicorn 运行之前的 Flask 应用,并指定多个 worker 和线程。

    bash 复制代码
    # 使用 4 个 worker 进程,每个进程内有 2 个线程来运行 myapp.py 中的 app
    gunicorn --workers=4 --threads=2 myapp:app -b 0.0.0.0:8080

    在这种模式下,服务器可以同时处理多个(最多 4 * 2 = 8 个)客户端请求,提高了并发处理能力。

关键点 : 当多个线程并发执行,并且它们访问或修改共享资源时,竞争条件就有可能发生。

竞争条件详解 (Race Condition Explained)

竞争条件发生在多个线程的操作顺序影响最终结果时。核心问题在于"检查时间"和"使用时间"之间存在一个时间窗口,状态可能在这个窗口内发生变化。这被称为 检查时间到使用时间 (Time-of-Check to Time-of-Use, TOCTOU) 漏洞。

类比:餐厅预订

  1. 你打电话给餐厅预订视野好且独立的 17 号桌。服务员检查发现桌上没有"已预订"牌子,口头确认预订。
  2. 同时,另一位顾客在餐厅内与另一位服务员交谈,也要求预订 17 号桌。这位服务员也看到桌上没有牌子,也确认了预订。
  3. 问题: 两位服务员都在检查(桌上无牌子)之后、但在实际放置"已预订"牌子(更新共享状态)之前接受了预订。由于更新状态需要时间,这个时间窗口导致了冲突。

示例 A:银行账户取款 (余额充足)

  1. 账户余额:100 美元。
  2. 线程 1: 检查余额 (100 美元),决定取款 45 美元。
  3. 线程 2 (在线程 1 更新余额前): 检查余额 (仍然看到 100 美元),决定取款 35 美元。
  4. 线程 1: 计算新余额 (100 - 45 = 55 美元),更新账户余额为 55 美元。
  5. 线程 2: 计算新余额 (100 - 35 = 65 美元),更新账户余额为 65 美元。
  6. 结果: 用户成功取款 45 + 35 = 80 美元,但最终账户余额却错误地显示为 65 美元(只扣除了第二次取款),因为线程 2 的更新覆盖了线程 1 的更新。

示例 B:银行账户取款 (余额不足)

  1. 账户余额:75 美元。
  2. 线程 1: 检查余额 (75 美元),决定取款 50 美元 (检查通过)。
  3. 线程 2 (在线程 1 更新余额前): 检查余额 (仍然看到 75 美元),决定取款 50 美元 (检查通过)。
  4. 线程 1: 更新余额为 75 - 50 = 25 美元。
  5. 线程 2: 更新余额为 75 - 50 = 25 美元 (或者更糟,如果逻辑是基于它检查时的余额)。
  6. 结果: 用户总共取款 100 美元,远超初始余额 75 美元。两次检查都通过了,因为在检查和实际扣款(更新余额)之间,另一个线程也在进行相同的操作。

代码示例:线程执行顺序的不确定性

以下 Python 代码创建两个线程,每个线程打印从 10% 到 100% 的完成度。

python 复制代码
import threading
import time

def increase_by_10():
    for i in range(1, 11):
        # time.sleep(0.01) # 可以取消注释以模拟一些工作负载,更容易看到交错
        print(f"{threading.current_thread().name}: {i}0% complete")

# 创建两个线程
thread1 = threading.Thread(target=increase_by_10, name="Thread-1")
thread2 = threading.Thread(target=increase_by_10, name="Thread-2")

# 启动线程
thread1.start()
thread2.start()

# 等待两个线程完成
thread1.join()
thread2.join()

print("Both threads have finished completely.")

多次运行此代码,你会发现输出顺序是不可预测的。有时 Thread-1 先打印某个百分比,有时是 Thread-2。哪个线程先完成 100% 也是不确定的。这说明了线程调度的不确定性,如果应用程序的逻辑或安全依赖于特定的执行顺序而没有适当的同步,就会出现问题。

常见成因 (Common Causes)

竞争条件通常由访问共享资源引起,常见原因包括:

  1. 并行执行 (Parallel Execution): Web 服务器为了处理并发用户请求,会并行执行多个请求(通过多进程或多线程)。如果这些并发请求在没有适当同步(如锁)的情况下访问和修改共享资源(如内存中的缓存、全局变量、文件),就可能导致竞争条件。
  2. 数据库操作 (Database Operations): 并发的数据库操作,特别是"读取-修改-写入"序列,是竞争条件的常见来源。例如,两个用户同时尝试更新同一个记录(如商品库存、用户积分),如果没有使用数据库提供的锁定机制(行锁、表锁)或事务隔离级别,可能导致数据不一致或丢失更新。
  3. 第三方库和服务 (Third-party Libraries and Services): Web 应用常常集成第三方库、API 或外部服务。如果这些外部组件本身没有被设计成线程安全的,或者没有正确处理并发访问,那么当多个请求同时与它们交互时,也可能引入竞争条件。
  4. 文件系统操作: 多个线程同时检查文件是否存在然后尝试创建或写入文件,也可能导致问题。

Web 应用中的竞争条件 (Race Conditions in Web Applications)

Web 应用架构回顾

Web 应用通常遵循客户端-服务器模型和多层架构:

  • 表示层 (Presentation Layer): 客户端(浏览器)负责渲染界面 (HTML, CSS, JS)。
  • 应用层 (Application Layer): 服务器端处理业务逻辑(如 PHP, Node.js, Python/Flask/Django),接收请求,与数据层交互。
  • 数据层 (Data Layer): 负责数据存储和检索(如 MySQL, PostgreSQL 数据库)。

服务器端的应用层为了处理来自多个用户的并发请求,通常会采用多线程或多进程模型,这就为竞争条件的发生创造了环境。

场景一:货币转账

考虑一个转账操作:

  1. 用户输入转账金额,点击"确认转账"。
  2. 检查 : 应用程序查询数据库,检查源账户余额是否足够
  3. 数据库返回查询结果。
  4. 操作 :
    • a. 如果余额足够,应用程序执行转账(扣除源账户余额,增加目标账户余额)。
    • b. 如果余额不足,显示错误信息。

问题点: 在第 2 步(检查余额)和第 4a 步(实际扣款)之间存在一个时间窗口。如果用户能在此窗口内发送多个并发的转账请求,并且服务器并行处理它们:

  • 请求 1: 检查余额 (足够),进入等待扣款状态。
  • 请求 2: 检查余额 (仍然足够,因为请求 1 还未扣款),进入等待扣款状态。
  • 请求 1: 执行扣款。
  • 请求 2: 执行扣款。

结果可能导致用户转出超过其实际余额的金额。

状态不仅仅是两个: 表面看只有"转账成功"和"转账失败"两种状态,但实际上存在中间状态,如"正在检查余额"、"余额检查通过,等待执行转账"。正是这些中间状态构成了竞争条件的"机会之窗"。

场景二:优惠券应用

考虑应用折扣优惠券:

  1. 用户在购物车页面输入优惠券代码。
  2. 检查 : 应用程序查询数据库,验证优惠券代码是否有效 ,以及是否已对该用户使用过(或满足其他限制条件)。
  3. 数据库返回验证结果。
  4. 操作 :
    • a. 如果代码有效且未使用过,应用折扣到购物车总价,并将该优惠券标记为已对此用户使用
    • b. 如果代码无效或已使用,显示错误信息。

问题点 : 在第 2 步(检查有效性和使用状态)和第 4a 步(应用折扣并标记为已用)之间存在一个时间窗口。如果用户能在此窗口内发送多个并发的应用同一优惠券的请求:

  • 请求 1: 检查优惠券 (有效且未使用),进入等待应用和标记状态。
  • 请求 2: 检查优惠券 (仍然是有效且未使用,因为请求 1 还未标记),进入等待应用和标记状态。
  • 请求 1: 应用折扣,标记优惠券为已用。
  • 请求 2: 应用折扣(可能再次应用或覆盖),标记优惠券为已用。

结果可能导致用户将同一个一次性优惠券应用了多次,获得了远超预期的折扣。

状态的复杂性: 同样,状态不止"未应用"和"已应用"。中间状态包括"正在检查有效性"、"正在检查使用限制"、"检查通过,等待应用"、"已应用,等待更新总价"。这个过程越复杂,步骤越多,时间窗口可能就越长。

机会之窗与利用 (Window of Opportunity and Exploitation)

竞争条件漏洞利用的关键在于抓住那个短暂的"机会之窗"------即从检查条件到条件状态被改变(资源被使用或标记)之间的时间差。这个窗口通常非常短,可能只有几毫秒或几十毫秒。

要成功利用,攻击者需要:

  1. 识别出可能存在 TOCTOU 问题的操作(通常涉及检查后修改共享状态)。
  2. 设法同时几乎同时地发送多个(通常是几十到几百个)相同的请求到服务器。

手动操作几乎不可能实现所需的速度和并发性。因此,需要使用专门的工具,如 Burp SuiteIntruderTurbo Intruder 扩展,或者编写自定义脚本,来发送大量并发请求,以增加在那个微小时间窗口内至少有两个请求被服务器交错处理的概率。


总结: 竞争条件是由于并发环境下对共享资源的不当访问控制引起的时序依赖性漏洞。理解多线程、进程和并发模型是分析这类漏洞的基础。在 Web 应用中,检查和操作分离的步骤(如检查余额后扣款、验证优惠券后标记使用)是常见的薄弱环节。利用通常需要借助工具发送大量并发请求,以期在短暂的机会之窗内触发非预期行为。防御措施主要包括使用锁(互斥锁、信号量)、数据库事务、原子操作等同步机制,确保检查和操作作为一个不可分割的单元执行。

相关推荐
梧六柒2 小时前
[闽盾杯 2021]日志分析 WP
网络安全
网安INF2 小时前
防火墙的分类与部署详解
服务器·安全·网络安全·防火墙
乾元2 小时前
AI 驱动的网络攻防演练与安全态势推演——从“规则检测”到“行为级对抗”的工程体系
网络·人工智能·安全·web安全·架构·自动化·运维开发
熙丫 133814823863 小时前
CISAW-SS安全软件认证|2026年培训日程公布,赋能安全开发,从代码源头筑牢防线
网络·安全·web安全
Whoami!4 小时前
❾⁄₆ ⟦ OSCP ⬖ 研记 ⟧ 防病毒软件规避 ➱ 内存中的逃避技术(下)
网络安全·信息安全·进程空洞化·内存逃避·内联挂钩
网安_秋刀鱼4 小时前
【java安全】URL链拆解
java·开发语言·安全·web安全·网络安全
白帽黑客-晨哥4 小时前
Web安全中SQL注入绕过WAF的具体手法和实战案例
sql·安全·web安全·职场和发展·渗透测试
tmj0113 小时前
Sqlmap命令详解
web安全·sqlmap
竹等寒17 小时前
TryHackMe-SOC-Section 1:蓝队介绍
安全·网络安全