SQL-登录漏洞-实现原理
SQL注入漏洞
新建一个源代码login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatiable" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-sacle=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/1.JPG); background-size: cover;">
<div class="login top-100 font-30">登 录</div>
<form action="login.php" method="get">
<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>
</div>
</form>
<div class="footer top-100">版权所有@成都蜗牛创新科技有限公司 备案:蜀ICP备15014130号</div>
</body>
</html>
然后新建一个login.php文件
<?php
//获取用户提交的登录请求的数据
$username = $_GET['username'];
$password = $_GET['password'];
$vcode = $_GET['vcode'];
//验证码的验证,此处启用了万能验证码,存在安全漏洞,OWASP-认证和授权失败
if($vcode !== '0000'){
die("vcode-error");
}
//连接到数据库
$conn = mysqli_connect('127.0.0.1','root','root','learn') or die("数据库连接不成功.");
//设置编码格式的两种方式
mysqli_query($conn,"set names utf8");
mysqli_set_charset($conn,'utf8');
//以下代码没有进行爆破的防护,违背了OwASP-认证与授权失败
$sql = "select * from learn2 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.html'</script>"
}else{
echo "login-fail";
echo "<script>location.href='login.php'</script>"
}
//关闭数据库
mysqli_close($conn);
?>
然后进行确认,找到正确的用户名和密码,看看是否能够登录成功,可以试着对代码进行改进,如果登录成功,则会跳转到另外一个页面,页面欢迎你,如果登录失败,则继续跳转回login.php这个页面中。
所以我们创建一个welcome.php这个文件
<?php
//以下代码违背了OWASP-失效的访问控制
echo "欢迎来到网络安全";
?>
使用bp进行登录的操作,hackbar也是同样的操作,使用GET方式传入参数即可
然后我们对代码进行一个简单的修改
如果成功登录,则跳转到welcome.php,页面给我们显示"欢迎来到网络安全"
如果登录失败,则跳转到登录界面login.html进行重新登录
//修改好的代码(只改if判断条件的那一部分,其余的不变)
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>";
}
接下来进入登录的渗透测试
由于我们没有对传入的参数做任何的限制,所以我们在登录页面输入一个单引号'作为用户名,密码任意,验证码为0000,响应如下:
以上响应出现MySQL的报错信息,上述报错信息存在两个可能的漏洞:
1、单引号可以成功引起SQL语句报错,说明后台没有专门对单引号进行处理
select * from learn2 where username = '$username' and password = '$password'
正常情况:select * from learn2 where username = 'zhangsan' and password = '123456'
试探情况:select * from learn2 where username = ''' and password = '123456'
由于'没有完全的闭合成功,所以会导致SQL报错
攻击Payload:
username:x' or userid=1#'
将上面这个username放入到SQL语句中
select * from learn2 where username = 'x' or userid=1#'' and password = '$password'
该SQL语句直接将密码的验证给绕过了,所以登录的时候密码正确与否已经不重要了
2、在报错信息里面暴露了敏感信息:
/Applications/phpstudy/WWW/sql/login.php,当前代码的绝对路径
注入类攻击的核心点
(1) 拼接为有效的语句或代码
(2) 确保完成了闭合,并且可以改变原有执行逻辑
通常并非一下就可以完成拼接和闭合,需要尝试,直到出现不一样的不断地结果。那么,建议这个尝试过程,用Python+字典快速处理,而非人工逐步排查
总结
上述代码一共发现6个漏洞
1、welcome.php页面谁都可以访问,没有进行登录判断(中)
2、在登录页面输入'作为用户名,报错信息存在login.php的绝对路径,暴露了系统后台的敏感信息(低)
3、保存用户信息的数据表中,密码字段是明文保存,不够安全(中)
4、登录页面可以进行SQL注入,进而轻易实现登录(高)
5、login.php页面中使用了万能验证码(中)
6、登录功能可以被爆破,没有进行爆破防护(中)
对于以上六个漏洞的修复
一、利用Python进行注入测试
import requests
#利用Python对PHP的登录页面进行FUZZ测试
def login_fuzz():
url = 'http://10.37.129.2/sql/login.php'
data = {'username':"'",'password':'12435','vcode':'0000'}
resp = requests.post(url=url,data=data)
print(resp.text)
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':'12435','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()
运行结果如下:
二、任意访问授权页面
无论用户是否登录成功,均可以直接输入:http://10.37.129.2/sql/welcome.php访问,而该页面是要求登录成功后才能访问,所以在该页面需要进行登录判断,代码修改为:
1、在common.php中添加session_start(),让其他页面引入,便于直接使用Session
2、在welome.php页面中,源代码修改为:
include "commond.php";
//修复该漏洞,在显示文本之前,先进行SEIION变量的验证
if (!isset($_SESSION['islogin']) or $_SESSION['islogin'] != 'true'){
die ("你还没有登录,无法访问本页面.");
}
echo '欢迎登录安全测试平台</br>';
3、在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>";
}
首先编写common.php,便于重复的代码直接进行使用,而不是编写很多遍
session_start();
//基于面向过程的连接方式
function create_connection(){
//连接到数据库
$conn = mysqli_connect('127.0.0.1','root','root','learn') or die("数据库连接不成功.");
//设置编码格式的两种方式
mysqli_query($conn,"set names utf8");
mysqli_set_charset($conn,'utf8');
return $conn;
}
//基于面向对象的连接方式
function create_connection_oop(){
$conn = new mysqli('127.0.0.1','root','root','learn') or die("数据库连接不成功.");
$conn->query("set names utf8");
$conn->set_charset('utf8');
return $conn;
}
三、修复login.php暴露文件路径
当在用户输入单引号时,会引起后台报错,一方面说明后台没有对单引号进行转义处理,导致单引号可以被注入到SQL语句中,进而导致SQL语句存在单独的一个引号,SQL语句无法闭合,发生错误。同时,还将该源代码的绝对路径暴露出来,这是敏感信息,应该将其屏蔽,修复代码如下:
$result = mysqli_query($conn,$sql) or die("SQL语句执行错误.");
四、修改用户表密码为明文
使用md5函数
$source = 'hello123';
echo md5($source);
//提示一:user表中password字段必须是32+位
//提示二:在用户注册时,必须使用md5函数将密码加密保存
五、SQL注入-登录漏洞-SQL注入防护
引入:
登录漏洞:登录页面可以进行SQL注入,进而轻易实现登录(高)
1、从代码和SQL语句的逻辑层面进行考虑,不能轻易让密码对比失效
2、基于将用户输入的引导(单引号和双引号)进行转义处理的前提,可以使用PHP内置函数addslashes进行强制转义。另外一种方法:可以使用MySQLi的预处理功能,也可以达到预防SQL注入放入目的
登录SQL语句的逻辑问题
//该SQL语句在实现登录操作时,存在严重的逻辑问题,用户名和密码的对比不应该放在同一条SQL语句中
//应该先通过用户名查询user表,如果确实找到一条记录(用户名唯一的情况下),找到记录后再进行密码的单独对比
//$sql = "select * from learn2 where username = '$username' and password = '$password'";
//修复后的代码如下:
$sql = "select * from learn2 where username = '$username'";
$result = mysqli_query($conn,$sql) or die("SQL语句执行错误."); //$result获取到的查询结果,称结果集
//如果用户名真实存在,刚好找到一条,则再单独进行密码的比较,即使用户名出现SQL注入漏洞,但是只要密码不正确,也无法登录
if (mysqli_num_rows($result) == 1){
//$row = mysqli_num_all($result,MYSQLI_ASSOC);
//$row = mysqli_fetch_row($result);
$row = mysqli_fetch_assoc($result);
var_dump($row); //var_dump在PHP中是一个调试函数,用于判断一个变量的类型与长度,并输出变量的数值
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>";
}
使用addslashes函数
addslashes函数可以将字符串中的单引号、双引号、反斜杠、NULL值自动添加转义符,从而防止SQL注入中对单引号和双引号的预防
原始SQL语句如下:
$sql = "select * from learn2 where username = '$username' and password = '$password'";
如果用户输入 x' or userid=1#' ,则SQL语句变成
$sql = "select * from learn2 where username = 'x' or userid=1#'' and password = '$password'";
如果使用addslashes强制为用户输入添加转义符,则变成:
$sql = "select * from learn2 where username = 'x\' or userid=1#\'' and password = '$password'";
上述SQL语句的用户名为: x\' or userid=1#\',则密码为$password,逻辑上保持不变
面向过程和面向对象
<?php
session_start();
//基于面向过程的连接方式
function create_connection(){
//连接到数据库
$conn = mysqli_connect('127.0.0.1','root','root','learn') or die("数据库连接不成功.");
//设置编码格式的两种方式
mysqli_query($conn,"set names utf8");
mysqli_set_charset($conn,'utf8');
return $conn;
}
//基于面向对象的连接方式
function create_connection_oop(){
$conn = new mysqli('127.0.0.1','root','root','learn') or die("数据库连接不成功.");
$conn->query("set names utf8");
$conn->set_charset('utf8');
return $conn;
}
//执行SQL语句
function test_connection_oop(){
$conn = create_connection_oop();
$sql = "select * from learn2 where userid<=3";
$result = $conn->query($sql);
//获取结果集行数
//echo $result->num_rows;
//获取结果集数组
$rows = $result->fetch_all(MYSQLI_ASSOC);
//var_dump($rows);
foreach ($rows as $row){
echo "username: " . $row['username'] . ", password: " . $row['password'] . "</br>";
}
}
test_connection_oop();
?>
使用mysqli预处理功能
预处理功能的用法
预处理的过程,就是先交给MySQL数据库进行SQL语句的准备,准备好之后再将SQL语句中的参数进行值的替换,引号会进行转义处理,将所有参数变成普通字符串,再进行第二次正式的SQL语句执行,MySQL的预处理即支持面向过程,也支持面向对象方式,但是我们后续直接使用面向对象的方式
使用预处理来防止SQL注入
<?php
session_start();
//基于面向过程的连接方式
function create_connection(){
//连接到数据库
$conn = mysqli_connect('127.0.0.1','root','root','learn') or die("数据库连接不成功.");
//设置编码格式的两种方式
mysqli_query($conn,"set names utf8");
mysqli_set_charset($conn,'utf8');
return $conn;
}
//基于面向对象的连接方式
function create_connection_oop(){
$conn = new mysqli('127.0.0.1','root','root','learn') or die("数据库连接不成功.");
$conn->query("set names utf8");
$conn->set_charset('utf8');
return $conn;
}
//执行SQL语句
function test_connection_oop(){
$conn = create_connection_oop();
$sql = "select * from learn2 where userid<=3";
$result = $conn->query($sql);
//获取结果集行数
//echo $result->num_rows;
//获取结果集数组
$rows = $result->fetch_all(MYSQLI_ASSOC);
//var_dump($rows);
foreach ($rows as $row){
echo "username: " . $row['username'] . ", password: " . $row['password'] . "</br>";
}
}
//MySQLi预处理功能(面向对象)
function mysqli_prepare_stmt(){
$conn = create_connection_oop();
//?在预处理语句中用于代替参数
//$sql = "select * from learn2 where username = ? and password = ?";
//$stmt->bind_param("ss",$username,$password); //bind_param()函数主要用于将参数绑定到预处理语句,从而执行数据库操作,它通过指定参数的数据类型来防止SQL注入,提高代码的安全性
//$sql = "select * from learn2 where userid < ?";
$sql = "update learn2 set username = ? where userid = ?";
//实例化Prepare Statement预处理对象
//使用prepare()方法通常需要两个步骤:1、准备sql语句2、执行sql语句,通常使用excute()方法,并绑定参数
$stmt = $conn->prepare($sql); //prepare()方法用于准备一个预编译的SQL语句
//实例化后需要将参数值进行绑定并在执行时替换
//bind_param第一个参数为数据类型:i 整数,s 字符串,d 小数,b 二进制
$stmt->bind_param("si",$username,$userid);
$username = 'hahahahahaha';
$userid = 5;
//正式执行SQL语句,如果是更新类操作,如update,insert,selete,执行后不做其他操作没有问题
//$stmt->execute(); //excute()方法返回一个布尔类型,表示执行成功与否
//$conn->commit(); //默认情况下,更新类操作会自动提交,但是也可以手工处理
//如果是针对查询语句呢,单纯只是执行无法取得查询结果的,需要进行结果绑定
$sql = "select * from learn2 where userid < ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("i",$userid);
$username = 5;
//要获取查询结果,还需要绑定结果参数
$stmt->bind_result($userid,$username,$password);
$stmt->execute();
//调用结果并进行处理
$stmt->store_result();
//输出行数
echo $stmt->affected_rows . "</br>";
echo $stmt->num_rows() . "</br>";
//遍历结果
while ($stmt->fetch()){
echo $userid,$username,$password, "</br>";
}
}
//test_connection_oop();
//mysqli_prepare_stmt();
?>
配置MySQL临时日志查看SQL语句
在MySQL数据库中运行以下语句,开启临时日志,将临时日志保存到表哥mysql数据库的general_log表中
#开启
use mysql;
set global log_output = 'TABLE';
set global general_log = 'ON';
#确认
show variables like "general_log";
并通过执行以下语句进行查询确认。
select * from general_log where argument like "%username%" order by thread_id DESC limit 10;
MySQL的预处理功能同样支持面向过程和面向对象。
除了MySQL用于处理数据库外,在PHP中还有最传统MySQL和PDO两种方式。
login-3.php
<?php
include "commond.php";
//获取用户提交的登录请求的数据
$username = $_GET['username'];
$password = $_GET['password'];
$vcode = $_GET['vcode'];
//验证码的验证,此处启用了万能验证码,存在安全漏洞,OWASP-认证和授权失败
if($vcode !== '0000'){
die("vcode-error");
}
//基于面向对象和MySQLi预处理功能实现SQL注入的防护
$conn = create_connection_oop();
$sql = "select userid,username,password from learn2 where username = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("s",$username); //绑定查询参数
$stmt->bind_result($userid,$username2,$password2);
$stmt->execute();
$stmt->store_result(); //用于将预处理语句的结果存储在客户端。这个方法通常用于将整个结果集从数据库服务器检索并存储在客户端内存中,以便后续处理。
if ($stmt->num_rows == 1){
$stmt->fetch(); //是一个用于从预编译的SQL语句中获取一行结果的方法
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>";
}
//关闭数据库
$conn->close();
?>