并发漏洞 Race Condition 学习 | ChatGPT 就是nb!

今天看了P牛师傅的文章,讲并发漏洞的,https://mp.weixin.qq.com/s/9f5Hxoyw5ne8IcYx4uwwvQ

但师傅用的是 Django 搭建的环境,我试了一下,发现 Django 我是真不会啊,所以先是想用 PHP 去实现

环境搭建

image
首先,我让 ChatGPT 给我写了个简单的程序来测试

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>取款</title> </head> <body> <?php // 定义数据库连接信息 $servername = "localhost"; $username = "root"; $password = ""; $dbname = "mybank"; // 创建连接 $conn = new mysqli($servername, $username, $password, $dbname); // 检查连接是否成功 if ($conn->connect_error) { die("连接失败: " . $conn->connect_error); } // 处理表单 if ($_SERVER["REQUEST_METHOD"] == "POST") { $username = $_POST["username"]; $password = $_POST["password"]; $amount = $_POST["amount"]; // 查询用户数据 $sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'"; $result = $conn->query($sql); // 检查用户是否存在 if ($result->num_rows > 0) { $row = $result->fetch_assoc(); $balance = $row["balance"]; // 检查余额是否充足 if ($balance >= $amount) { // 扣除余额并更新数据库 $new_balance = $balance - $amount; $sql = "UPDATE users SET balance = '$new_balance' WHERE username = '$username'"; if ($conn->query($sql) === TRUE) { echo "取款成功!"; // 写入日志文件 $timestamp = date("Y-m-d H:i:s"); $log_message = "$timestamp: $username 取出 $amount 元。\n"; $log_file = fopen("log.txt", "a"); fwrite($log_file, $log_message); fclose($log_file); } else { echo "发生错误:" . $conn->error; } } else { echo "余额不足,无法完成取款。"; } } else { echo "用户名或密码错误,请重新输入。"; } } // 关闭连接 $conn->close(); ?> <h2>取款表单</h2> <form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]);?>"> 用户名:<input type="text" name="username"><br><br> 密码:<input type="password" name="password"><br><br> 取款金额:<input type="number" name="amount"><br><br> <input type="submit" name="submit" value="提交"> </form> </body> </html>

然后我发现不行啊,我还以为 Mysqli 是自带事物锁的,没有办法同时查询。。。。。(其实不是,是因为 PHP 是单线程的。。。。)
没办法,又让 AI 给我写了个不带事务锁的(我以为的),我不查数据库了,我查文件,哈哈哈。
image

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>取款</title> </head> <body> <?php // 定义文件路径 $file_path = "user_info.txt"; // 处理表单 if ($_SERVER["REQUEST_METHOD"] == "POST") { // 获取表单数据 $username = $_POST["username"]; $password = $_POST["password"]; $amount = $_POST["amount"]; // 读取文件中的用户数据 $user_info = file_get_contents($file_path); $user_info_array = explode(";", $user_info); $balance = ""; foreach ($user_info_array as $user) { $user_info_detail = explode(",", $user); if ($user_info_detail[0] == $username && $user_info_detail[1] == $password) { $balance = $user_info_detail[2]; break; } } // 检查用户名和密码是否正确 if ($balance !== "") { // 将余额从字符串转化为数字 $balance = intval($balance); // 检查余额是否充足 if ($balance >= $amount) { // 扣除余额并更新文件 $new_balance = $balance - $amount; for ($i = 0; $i < count($user_info_array); $i++) { $user_info_detail = explode(",", $user_info_array[$i]); if ($user_info_detail[0] == $username && $user_info_detail[1] == $password) { $user_info_array[$i] = "$username,$password,$new_balance"; break; } } // 将更新后的用户信息保存到文件中 $new_user_info = implode(";", $user_info_array); file_put_contents($file_path, $new_user_info); echo "取款成功!"; // 写入日志文件 $timestamp = date("Y-m-d H:i:s"); $log_message = "$timestamp: $username 取出 $amount 元。\n"; $log_file = fopen("log.txt", "a"); fwrite($log_file, $log_message); fclose($log_file); } else { echo "余额不足,无法完成取款。"; } } else { echo "用户名或密码错误,请重新输入。"; } } ?> <h2>取款表单</h2> <form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]);?>"> 用户名:<input type="text" name="username"><br><br> 密码:<input type="password" name="password"><br><br> 取款金额:<input type="number" name="amount"><br><br> <input type="submit" name="submit" value="提交"> </form> </body> </html>

不错不错,ChatGPT 完美实现了我的要求,但是我测试发现,咋回事呢,还是没法复习并发漏洞,而且我把 sleep 调高了之后发现,PHP 竟然是单线程执行的(我用 php -S 0.0.0.0:88989 开启的web服务器)
然后 ChatGPT 也回答了我的问题
image

啊,这。。。。
好吧,我换用 Python 的 Flask 去搭建环境吧(我以前学过 Flask ,而且 Flask 比 Django 简单的多)

image

image

app.py

# 导入 Flask 库 from datetime import datetime from flask import Flask, render_template, request # 创建 Flask 应用 app = Flask(__name__) # 定义文件路径 file_path = "user_info.txt" # 定义首页 @app.route('/') def index(): return render_template('index.html') # 处理取款请求 @app.route('/withdraw', methods=['POST']) def withdraw(): # 获取表单数据 username = request.form['username'] password = request.form['password'] amount = int(request.form['amount']) # 读取文件中的用户信息 with open(file_path, 'r') as f: user_info = f.read() user_info_array = user_info.split(';') balance = -1 # 查找指定用户的余额 for user in user_info_array: user_info_detail = user.split(',') if user_info_detail[0] == username and user_info_detail[1] == password: balance = int(user_info_detail[2]) break # 检查用户名和密码是否正确 if balance != -1: # 检查余额是否充足 if balance >= amount: # 更新用户余额 for i in range(len(user_info_array)): user_info_detail = user_info_array[i].split(',') if user_info_detail[0] == username and user_info_detail[1] == password: user_info_array[i] = f'{username},{password},{balance - amount}' break # 将更新后的用户信息保存到文件中 new_user_info = ';'.join(user_info_array) with open(file_path, 'w') as f: f.write(new_user_info) # 输出提示信息 message = f'{username} 取款 {amount} 成功!' # 写入日志文件 timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") log_message = f'{timestamp}: {username} 取出 {amount} 元。\n' with open('log.txt', 'a') as f: f.write(log_message) else: message = '余额不足,无法完成取款。' else: message = '用户名或密码错误,请重新输入。' return render_template('withdraw.html', message=message) if __name__ == '__main__': app.run(host="192.168.0.66", debug=True)

index.html

<!DOCTYPE html> <html> <head> <title>取款系统</title> <meta charset="utf-8"> </head> <body> <h1>取款系统</h1> <form action="{{ url_for('withdraw') }}" method="post"> <label>用户名:</label> <input type="text" name="username"><br><br> <label>密码:</label> <input type="password" name="password"><br><br> <label>取款金额:</label> <input type="number" name="amount"><br><br> <input type="submit" value="提交"> </form> </body> </html>

withdraw.html

<!DOCTYPE html> <html> <head> <title>取款结果</title> <meta charset="utf-8"> </head> <body> <h1>{{ message }}</h1> <a href="{{ url_for('index') }}">返回</a> </body> </html>

文件结构:
image

ChatGPT 永远的神!!!太 NB!了,我要失业了。

并发测试

我们给用户设置 100 块的余额
image

然后 10 个线程同时开跑
image
然后发现取出来了 200 元
image

整体逻辑如下:
image

导致漏洞的原因就是我们访问文件的时候没有加锁,导致了第一个进程还没有修改完余额的时候,后面已经有进程又读取了文件,拿到了还没来得及修改的余额。

然后我再这里加了1秒的延迟
image

我们在这里加上 1 秒的延迟,模拟业务处理的比较慢,记得重启服务器。
可以发现一次性能取出来好多。基本上 100 块能稳定取出来很多很多了。
image

image

修复

TMD 还能一键修复
image
直接让 ChatGPT 给加了个锁,不过是个悲观锁,可能会影响性能。

from flask import Flask, render_template, request from datetime import datetime import threading import time app = Flask(__name__) file_path = "user_info.txt" lock = threading.Lock() @app.route('/') def index(): return render_template('index.html') @app.route('/withdraw', methods=['POST']) def withdraw(): username = request.form['username'] password = request.form['password'] amount = int(request.form['amount']) # 加锁 lock.acquire() try: user_info = read_user_info() time.sleep(1) balance = -1 for user in user_info: user_info_detail = user.split(',') if user_info_detail[0] == username and user_info_detail[1] == password: balance = int(user_info_detail[2]) break if balance != -1: if balance >= amount: new_balance = balance - amount for i in range(len(user_info)): user_info_detail = user_info[i].split(',') if user_info_detail[0] == username and user_info_detail[1] == password: user_info[i] = f'{username},{password},{new_balance}' break write_user_info(user_info) message = f'{username} 取款 {amount} 成功!' timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") log_message = f'{timestamp}: {username} 取出 {amount} 元。\n' with open('log.txt', 'a') as f: f.write(log_message) else: message = '余额不足,无法完成取款。' else: message = '用户名或密码错误,请重新输入。' finally: # 释放锁 lock.release() return render_template('withdraw.html', message=message) def read_user_info(): with open(file_path, 'r') as f: user_info = f.read().split(';') return user_info def write_user_info(user_info): new_user_info = ';'.join(user_info) with open(file_path, 'w') as f: f.write(new_user_info) if __name__ == '__main__': app.run(debug=True)

其他

然后关于并发漏洞的测试还涉及到: 无锁无事务时的竞争攻击、无锁有事务时的竞争攻击、悲观锁加事务防御Race Condition、乐观锁加事务防御Race Condition 这些大家就去 P 牛的文章里去看吧。


__EOF__

本文作者Nestar
本文链接https://www.cnblogs.com/Nestar/p/17303194.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   Nestar  阅读(315)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示