【逻辑漏洞】并发漏洞——经典案例

【逻辑漏洞】并发漏洞——经典案例

原创 狗头安全 狗头网络安全 2024-11-26 09:16

一.简单介绍

1.1 并发漏洞概述

并发漏洞是指在多线程或多进程环境中,由于对共享资源的访问没有正确的同步控制,导致程序行为异常或安全问题的漏洞

当多个线程或进程并发访问共享资源,并且操作的执行顺序未被正确同步时,可能会导致不可预知的结果。例如,两个线程同时检查一个共享变量,然后尝试修改它,而没有任何锁机制,可能会导致数据不一致

利用竞争条件的主要障碍是确保多个请求同时处理,且它们的处理时间几乎没有差异—最好小于 1 毫秒

1.2 并发漏洞常出现的功能点及解决方法

1. 用户注册/账户管理

  • 问题场景:    重复注册:多个线程同时验证用户名是否存在,可能导致两个用户使用相同的用户名注册成功。余额更新:并发更新账户余额,可能导致余额被覆盖或错误。
  • 重复注册:多个线程同时验证用户名是否存在,可能导致两个用户使用相同的用户名注册成功。
  • 余额更新:并发更新账户余额,可能导致余额被覆盖或错误。
  • 解决方法:
  • 在注册逻辑中加锁,或使用数据库的唯一约束。使用事务机制确保一致性。

2. 秒杀/抢购功能

  • 问题场景:    库存超卖:多个线程同时减库存,导致实际库存为负数。订单重复生成:同一用户重复提交请求,导致重复购买。
  • 库存超卖:多个线程同时减库存,导致实际库存为负数。订单重复生成:同一用户重复提交请求,导致重复购买。
  • 解决方法:
  • 对库存更新操作加分布式锁(如Redis分布式锁)。使用队列(如RabbitMQ)进行请求排队。

3. 支付与结算

  • 问题场景:
  • 重复支付:用户点击支付按钮多次,导致支付记录重复。金额计算错误:并发情况下对支付金额进行累加/扣减出现错误。
  • 解决方法:
  • 引入防重机制(如幂等性设计,使用唯一订单号)。使用数据库事务或原子操作进行金额更新。

4. 文件读写

  • 问题场景:    文件覆盖:多个线程/进程同时写入文件,导致内容混乱或丢失。文件锁失效:锁定机制未正确实现,导致多个线程读取未完成写入的文件。
  • 文件覆盖:多个线程/进程同时写入文件,导致内容混乱或丢失。
  • 文件锁失效:锁定机制未正确实现,导致多个线程读取未完成写入的文件。
  • 解决方法:
  • 对文件操作加锁或使用线程安全的I/O类。使用临时文件和替换机制避免部分写入文件被读取。

5. 日志记录

  • 问题场景:    多个线程同时写入日志文件,可能导致日志内容交错或部分日志丢失。
  • 多个线程同时写入日志文件,可能导致日志内容交错或部分日志丢失。

  • 解决方法:

  • 使用线程安全的日志框架(如Log4j、SLF4J)。对日志写入操作加锁或使用异步日志写入。

6. 缓存更新

  • 问题场景:      缓存击穿:多个线程同时发现缓存过期,导致瞬间大量请求击中数据库。缓存与数据库不一致:并发更新时,缓存未及时刷新。
  • 缓存击穿:多个线程同时发现缓存过期,导致瞬间大量请求击中数据库。缓存与数据库不一致:并发更新时,缓存未及时刷新。
  • 解决方法:    使用双写机制(更新数据库时同步更新缓存)。引入分布式锁或互斥更新策略。
  • 使用双写机制(更新数据库时同步更新缓存)。引入分布式锁或互斥更新策略。

7. 任务调度

  • 问题场景:多个实例同时运行定时任务,导致重复执行。未正确同步任务状态,导致多个线程对同一任务执行操作。
  • 多个实例同时运行定时任务,导致重复执行。未正确同步任务状态,导致多个线程对同一任务执行操作。

  • 解决方法:    使用分布式任务调度框架(如Quartz的分布式锁)。通过数据库表记录任务状态,确保同一任务只被执行一次。

  • 使用分布式任务调度框架(如Quartz的分布式锁)。通过数据库表记录任务状态,确保同一任务只被执行一次。

8. 数据库操作

  • 问题场景:    脏读、幻读、不可重复读:事务隔离级别不足,导致数据不一致。更新丢失:两个线程同时修改同一行数据,后者覆盖前者的修改。
  • 脏读、幻读、不可重复读:事务隔离级别不足,导致数据不一致。更新丢失:两个线程同时修改同一行数据,后者覆盖前者的修改。
  • 解决方法:    设置合适的事务隔离级别(如Serializable或Repeatable Read)。使用悲观锁或乐观锁机制(如版本号控制)。
  • 设置合适的事务隔离级别(如Serializable
    或Repeatable Read
    )。使用悲观锁或乐观锁机制(如版本号控制)。

9. 队列消费

  • 问题场景:    消费者并发处理同一条消息,导致重复处理。消息未确认,导致重复投递。
  • 消费者并发处理同一条消息,导致重复处理。消息未确认,导致重复投递。

  • 解决方法:    消息队列(如Kafka、RabbitMQ)配置幂等性检查。确保消息处理完成后再进行确认(ACK)。

  • 消息队列(如Kafka、RabbitMQ)配置幂等性检查。确保消息处理完成后再进行确认(ACK)。

10. 多线程计算

  • 问题场景:
  • 多个线程同时写入共享变量,导致结果错误。临界区访问未同步,导致中间结果不一致。
  • 解决方法:
  • 使用线程安全的数据结构或锁机制。使用原子类(如Java的AtomicInteger)。

11. 微服务间的并发请求

  • 问题场景:    重复调用导致资源浪费。多个服务对同一资源更新,造成数据不一致。
  • 重复调用导致资源浪费。多个服务对同一资源更新,造成数据不一致。

  • 解决方法:   通过请求去重机制(如唯一请求ID)。使用分布式锁(如ZooKeeper、Redis)。

  • 通过请求去重机制(如唯一请求ID)。使用分布式锁(如ZooKeeper、Redis)。

12. 配置或状态更新

  • 问题场景:    配置被多个线程同时更新,导致最终结果不可预期。并发读取未完全更新的配置。
  • 配置被多个线程同时更新,导致最终结果不可预期。并发读取未完全更新的配置。

  • 解决方法:    使用原子操作或锁保护配置更新。引入版本号或时间戳,确保配置更新有序。

  • 使用原子操作或锁保护配置更新。引入版本号或时间戳,确保配置更新有序。

二.经典短信短信轰炸

2.1 一次中危提升到高危的肾透

在注册或者登录时会需要获取验证码,抓取获取验证码的请求包即可

mobile
即为我们的手机号参数,这里不对请求包进行修改直接进行并发

手机端查看,存在纵向并发漏洞

这里注意看请求包中包含code参数,很有可能就是我们的验证码,验证码在请求包中,这里对code参数进行修改

然后查看手机短信内容

成功将中危漏洞提升到高危,一定要对参数敏感,万物皆可并发

三.并发漏洞的思考与总结

1. 并发漏洞的本质

并发漏洞的核心问题在于多个线程或进程同时访问共享资源时,未能正确处理访问控制和协调机制。这种问题通常由以下原因引发:
共享资源竞争:
多个任务对同一资源(内存、文件、数据库等)进行修改,导致数据一致性问题。
执行顺序的不确定性:
由于操作的非确定性,程序在不同时间点表现出不可预测的行为。
缺乏同步机制:
开发者没有正确使用锁、事务或其他同步工具,导致竞态条件。
死锁与资源饥饿:
线程间相互依赖资源,形成环路或不公平竞争。
思考

并发本质上是利用时间或资源切片来提升性能,但如果设计和实现不当,可能带来更多的维护成本和复杂问题。

2. 设计并发系统时需要考虑的因素

2.1. 数据一致性

  • 问题:在并发环境下,数据的一致性和完整性是首要挑战。
  • 思考方向:
  • 确定一致性模型:强一致性、最终一致性还是弱一致性?是否需要事务支持?事务的隔离级别如何选择?使用锁还是无锁设计(如CAS操作)?

2.2. 性能与扩展性

  • 问题:引入锁或同步机制可能降低系统性能。
  • 思考方向:    是否存在资源竞争的瓶颈?是否可以通过分区或分片减少并发冲突?引入的锁粒度是否合适?能否采用读写分离优化?
  • 是否存在资源竞争的瓶颈?是否可以通过分区或分片减少并发冲突?引入的锁粒度是否合适?能否采用读写分离优化?

2.3. 并发控制粒度

  • 问题:锁粒度过细会增加复杂性,过粗则影响性能。
  • 思考方向:    资源的并发访问是否可以分区处理?需要保护的临界区范围是否可以缩小?是否可以引入读写锁或分段锁?
  • 资源的并发访问是否可以分区处理?需要保护的临界区范围是否可以缩小?是否可以引入读写锁或分段锁?

2.4. 异常与恢复机制

  • 问题:程序在并发条件下的异常处理可能导致资源泄漏或数据不一致。
  • 思考方向:    如果操作失败,如何回滚或重试?在系统崩溃时,如何确保数据一致性?是否需要持久化日志或状态以实现幂等性?
  • 如果操作失败,如何回滚或重试?在系统崩溃时,如何确保数据一致性?是否需要持久化日志或状态以实现幂等性?

2.5. 死锁与活锁

  • 问题:多个线程/进程间相互依赖资源可能导致系统阻塞。
  • 思考方向:    是否可以采用无锁编程?是否存在固定的锁获取顺序?是否引入了优先级反转等问题?
  • 是否可以采用无锁编程?是否存在固定的锁获取顺序?是否引入了优先级反转等问题?

3. 编程实现中的常见错误与对策

3.1. 忽略共享资源的同步

  • 错误:假定访问共享变量是原子的。
  • 对策:
  • 使用线程安全的数据结构(如ConcurrentHashMap),或通过锁保护共享资源。

3.2. 鉴别和避免竞态条件

  • 错误:在检查和修改共享资源的操作之间缺乏同步。
  • 对策:
  • 引入原子操作或CAS(Compare-And-Swap)来避免竞态。

3.3. 滥用锁

  • 错误:加锁范围过大或锁粒度过小,导致性能下降或代码复杂度增加。
  • 对策:    优化锁的范围,仅对必要的临界区加锁。使用读写锁或分布式锁。
  • 优化锁的范围,仅对必要的临界区加锁。使用读写锁或分布式锁。

3.4. 忽视锁的公平性

  • 错误:使用非公平锁可能导致某些线程长期得不到执行机会。
  • 对策:
  • 在需要公平性的场景下,选择公平锁(如ReentrantLock(true))。

3.5. 不考虑系统资源的限制

  • 错误:线程数量设置不当,导致CPU饱和或资源浪费。
  • 对策:
  • 根据实际情况设置线程池大小,避免无限制创建线程。

4. 系统层面的优化方向

4.1. 无锁编程

  • 思路:通过CAS操作或版本控制避免锁的使用,减少锁带来的开销。
  • 适用场景:多线程环境下共享变量的简单更新。

4.2. 使用事务或分布式事务

  • 思路:通过事务机制保证操作的原子性和一致性,特别是对数据库的操作。
  • 挑战:分布式事务的性能和复杂度需要权衡。

4.3. 幂等性设计

  • 思路:保证并发操作的幂等性,使得重复操作不会带来额外的影响。
  • 适用场景:支付系统、订单系统等需要保证操作结果一致性的场景。

4.4. 分布式锁

  • 思路:在分布式系统中使用Redis、ZooKeeper等实现分布式锁。
  • 注意:需要考虑锁的超时问题,避免死锁。

4.5. 任务排队

  • 思路:通过消息队列(如Kafka、RabbitMQ)进行任务排队,降低瞬时并发量。
  • 适用场景:秒杀活动、批量数据处理等场景。

5. 预防并发漏洞的思维习惯

5.1. 从最坏的情况出发

  • 思考可能出现的竞态条件、资源冲突以及异常情况。设计代码时要假设所有线程都可能同时竞争资源。

5.2. 使用正确的工具

  • 善用语言和框架提供的并发工具(如线程池、锁、原子操作等)。避免“造轮子”,直接使用成熟的并发库。

5.3. 充分测试

  • 引入并发测试工具,模拟高并发场景,查找潜在问题。使用静态分析工具(如FindBugs)检测并发问题。

5.4. 持续监控

  • 通过日志、指标监控并发热点和瓶颈。定期进行代码审查,优化并发处理。

6. 总结

并发漏洞本质上是程序对资源的不正确协调或访问控制引发的问题。在设计和实现并发程序时,既要考虑性能和效率,也要关注一致性和安全性。通过合理的设计模式、合适的同步机制以及全面的测试手段,可以有效减少并发漏洞的发生,提升系统的稳定性和可靠性