并发漏洞 Race Condition 学习 | ChatGPT 就是nb!
今天看了P牛师傅的文章,讲并发漏洞的,https://mp.weixin.qq.com/s/9f5Hxoyw5ne8IcYx4uwwvQ
但师傅用的是 Django 搭建的环境,我试了一下,发现 Django 我是真不会啊,所以先是想用 PHP 去实现
环境搭建
首先,我让 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 给我写了个不带事务锁的(我以为的),我不查数据库了,我查文件,哈哈哈。
<!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 也回答了我的问题
啊,这。。。。
好吧,我换用 Python 的 Flask 去搭建环境吧(我以前学过 Flask ,而且 Flask 比 Django 简单的多)
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>
文件结构:
ChatGPT 永远的神!!!太 NB!了,我要失业了。
并发测试
我们给用户设置 100 块的余额
然后 10 个线程同时开跑
然后发现取出来了 200 元
整体逻辑如下:
导致漏洞的原因就是我们访问文件的时候没有加锁,导致了第一个进程还没有修改完余额的时候,后面已经有进程又读取了文件,拿到了还没来得及修改的余额。
然后我再这里加了1秒的延迟
我们在这里加上 1 秒的延迟,模拟业务处理的比较慢,记得重启服务器。
可以发现一次性能取出来好多。基本上 100 块能稳定取出来很多很多了。
修复
TMD 还能一键修复
直接让 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 牛的文章里去看吧。
本文来自博客园,作者:Nestar,转载请注明原文链接:https://www.cnblogs.com/Nestar/p/17303194.html