基于约束的SQL攻击
首先需要注意的是,本文所述的是基于约束的SQL攻击而非SQL注入攻击
碰到这个问题,是在做CTF题时碰到的(题目网址:http://123.206.31.85:49163/)
这一道题是要求我们以管理员admin的身份登陆系统方可查看flag,方法就是在注册时注册一个admin (admin后面带一个或几个空格),然后登录系统,系统会误认为是管理员登录了系统,所以赋予管理员权限。
那么,就算我们采用PHP中的PDO以及预查寻的方式(详见简单的PDO技术以及预处理方法预防SQL注入)来处理是否能预防基于约束的SQL攻击呢?这里给出靶场的源代码,环境使用的是Windows上的phpstudy(php-5.6.27+apache)
前端代码loginPDO.php
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录界面</title>
</head>
<body>
<form action = "actionPDO.php" method = "post">
<div id = "main" class = "main">
<center>
<h2>
登录
</h2>
<p><lable>用户名称:</lable><input type="text" name="userName" placeholder="*************"></p>
<p><lable>用户密码:</lable><input type="text" name="userPassword" placeholder="*************"></p>
<input type="submit" name="submit" value="登录">
<input type="reset" value="清空">
</center>
</form>
<form action = "registerPDO.php" method = "post">
<center>
<h2>
注册
</h2>
<p><lable>用户名称:</lable><input type="text" name="userName" placeholder="*************"></p>
<p><lable>用户密码:</lable><input type="text" name="userPassword" placeholder="*************"></p>
<input type="submit" name="submit" value="注册">
<input type="reset" value="清空">
</center>
</form>
</body>
</html>
注册模块代码registerPDO.php
<?php
$name = $_POST['userName'];
$password = $_POST['userPassword'];
if($name == null || $password == null){
header("location:loginPDO.php");
return;
}
try {
//创建PDO对象
$pdo = new PDO("mysql:host=localhost;dbname=php10", "root", "root");
}catch(PDOException $e) {
echo "数据库连接失败:".$e->getMessage();
exit;
}
$sql = "INSERT INTO loginPDO(userName,userPassword) VALUES (?,?)";
//$sql = "INSERT INTO loginPDO(userName,userPassword) VALUES (:userName,:userPassword)";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(1,$name);
$stmt->bindParam(2,$password);
//$stmt->bindParam(":userName",$name); //绑定 一个 PHP 变量到预处理语句中对应的命名占位符或问号占位符
//$stmt->bindParam(":userPassword",$password);
$pdo->quote($name);
$pdo->quote($password);
$result = $stmt->execute();
if($result){
echo "<script>alert('注册成功')</script>";
echo "<h2>注册成功!,即将跳转至登录页面...</h2>";
header("refresh:3; url = //localhost/loginPDO.php");
}else{
echo "<script>alert('注册值错误!')</script>";
echo "<h2>注册失败,用户名已经注册或注册值为空...</h2>";
}
?>
登陆模块actionPDO.php
<?php
$name = $_POST['userName'];
$password = $_POST['userPassword'];
if($name == null || $password == null){
header("location:loginPDO.php");
return;
}
try {
//创建对象
$con = new PDO("mysql:host=localhost;dbname=php10", "root", "root");
}catch(PDOException $e) {
echo "数据库连接失败:".$e->getMessage();
exit;
}
$sql = "SELECT * FROM loginPDO WHERE userName = :userName AND userPassword = :userPassword";
//$sql = "SELECT * FROM login WHERE userName = ? AND userPassword = ?";
$stmt = $con->prepare($sql);
$stmt->bindParam(':userName',$name); //bindParam方法与bindValue方法的区别在于bindParam的第二个参数可以传值用变量,而bindValue第二个参数只能传值用常量或字符串
$stmt->bindParam(':userPassword',$password);
//$stmt->bindParam('1',$name);
//$stmt->bindParam('2',$password);
$con->quote($name); //quote方法是为普通的字符串添加引号
$con->quote($password);
$re=$stmt->execute();
if($stmt->rowCount()!=0){
echo "<script>alert('登录成功!')</script>";
echo "<h2>欢迎您{$name}</h2>";
}else{
echo "<script>alert('登录失败!')</script>";
echo "<h2>登录失败,3秒后自动跳转...</h2>";
header("refresh:3; url = //localhost/loginPDO.php");
}
?>
靶场效果如下
根据数据库中的初始数据,账号admin,密码root可以成功登录,注册不允许有相同的用户名。
首先我尝试了在admin后面加一个空格,密码不变,提示登录成功
然后尝试注册一个后面带空格的admin,结果提示注册失败,注册用户已存在,并不像刚开始所期望的那样。于是我猜测是否是因为bindParam()在绑定参数的时候去掉了末尾的空格,于是开始翻阅php手册,结果并没有发现手册中有提到bindParam()会去掉参数的首尾空格
于是换一个方式,登录时在admin前面加一个空格,密码不变,点击登录提示登陆失败
再注册一个admin前有一个空格的用户,结果发现注册成功
那么猜测应该是成立的,bindParam()会截断绑定参数值后面的空格,而不会截断前面的空格,也就是允许注册用户名的第一个字符为空格
也就是说,在一个有严格权限区分的系统,如果得知了管理员的用户名,可以利用管理员用户名后加空格的方法注册,再登录,系统也会默认为管理员,类似CTF的这道题一样,产生基于约束的SQL攻击
防御方法
1、使用过滤空格的函数,$data = trim($data),去掉传入参数的首位空格,再进行数据库查询、插入等工作
2、对用户名的长度进行限制
3、验证成功后返回的必须是用户传递进来的用户名,而不是从数据库取出的用户名。因为当我们以用户admin和密码root登陆时,其实数据库返回的是我们自己的用户信息,而我们的用户名其实是[admin ],如果此后的业务逻辑以该用户名为准,那么就不能达到越权的目的了。
4、将要求或者预期具有唯一性的那些列加上UNIQUE约束
5、使用id作为数据库表的主键并设置自动递增,并且数据应该通过程序中的id进行跟踪