環境搭建
這個滲透環境的搭建有以下幾點
- 基於session的會話
- 登入介面
- 登入成功介面
- 登出介面
- 資料庫搭建
- 資料庫連線
session會話
- 伺服器端利用session_start()函式發起一次session的會話
- 此時我們登入成功後使用者的資料被儲存在伺服器端的Cookie: session= ,即sessionID
- 如果需要再次訪問
- 伺服器端的
$_SESSION['...']
會獲取使用者session - 然後與原本存在於伺服器的sessionID進行比對,如果比對成功,則證明使用者正確
環境搭建程式碼
建立資料庫指令碼
在MySQL中使用source命令即可執行指令碼:
drop database if exists lab;
create database lab;
use lab;
create table users
(
id int not null auto_increment,
username char(32) not null,
passcode char(32) not null,
primary key(id)
);
insert into users(username,passcode) values('admin','admin123');
insert into users(username,passcode) values('alice','alice456');
登入介面html:
<html>
<head>
<meta charset="UTF-8">
<title>Login</title>
<style>
#a {
width: 500px;
text-align: center;
}
.b {
width: 200px;
height: 30px;
}
</style>
</head>
<body>
<div id=a>
<h2>Login!</h2>
<form name="form_login" method="POST" action="check_login.php">
Username:<input type="text" class="b" name="username" /><br> <br>
Password:<input type="password" class="b" name="password" /><br>
<input type="submit" name="Submit" value="Submit" />
<input type="reset" name="reset" value="Reset" />
</form>
</div>
</body>
</html>
查詢資料庫是否為正確的賬號密碼php程式碼
<?php
include('con_database.php');
$username=isset($_POST['username'])?$_POST['username']:'';
$password=isset($_POST['password'])?$_POST['password']:'';
if($username=='' || $password==''){
echo "<script>alert('請輸入賬號和密碼!')</script>";
exit;
}
$sql="select * from users where username='$username' and passcode='$password'";
$query=mysqli_query($con,$sql) or die('SQL語句執行失敗'.mysqli_error($con));
if ($row=mysqli_fetch_array($query)){
session_start();
$_SESSION['username']=$row[1];
echo "<a href='welcome.php'>歡迎訪問</a>";
}else{
echo "<script>alert('登入失敗!');history.go(-1)</script>";
}
mysqli_close($con);
?>
連線資料庫php程式碼:
<?php
$con=mysqli_connect('127.0.0.1','root','root') or die("資料庫連線失敗!");
mysqli_select_db($con,'lab')or die("資料庫連線失敗");
?>
登出登入程式碼(即關閉session會話)
<?php
session_start();
session_unset();
session_destroy();
echo "登出成功";
?>
登入成功歡迎介面:
<?php
session_start();
if(isset($_SESSION['username'])){
echo "歡迎使用者".$_SESSION['username']."登入";
echo "<br>";
echo "<a href=logout.php>退出登入</a>";
}else{
echo "您沒有許可權訪問";
}
?>
至此,我們的滲透環境就構建好了
萬能密碼漏洞剖析
- 使用者名稱輸入
' or 1=1 or'
,密碼隨意,發現可以登入進去 - 密碼輸入
'or '1=1
也可以登入進去
當然登入方法不止一種:
原來查詢語句是這樣的:
$sql="select * from users where username='$username' and passcode='$password'";
經過注入之後,變成:
$sql="select * from users where username='' or 1=1 or ' and passcode='****'";
我們觀察到,where後面呃字句中的username被閉合,並且字句分成三個句子並用or連線。
在SQL語句中 and的優先順序要大於or,所以1=1先判斷,為真,即where後面的語句為真,即整個SQL語句為真,即表示查詢正確
而形成的語句可以將整個users表查詢,後面的$row=mysqli_fetch_array($query)
選擇的是查詢的第一行值,這樣滿足了SQL語句並跳過了登入驗證
由此可以引申出,只要where後面字句為真,即可跳過驗證,有如下衍生方法:
' or 1=1 #
' or 1=1 --
(後面有空格)'or"="or'
萬能密碼攻擊防護
使用正規表示式限制使用者輸入:
可以使用正規表示式限制使用者的使用者名稱輸入,比如:/^[a-z0-9A-Z_]{5,16}$/
這個限制了使用者5位以上16位以下的字母數字下劃線為使用者名稱的輸入
這個限制在check_login.php中新增
<?php
include('con_database.php');
$username=isset($_POST['username'])?$_POST['username']:'';
$password=isset($_POST['password'])?$_POST['password']:'';
if (!preg_match("/^[a-Z0-9A-Z_]{5,16}$/",$username)){
echo "<script>alert('使用者名稱格式錯誤')</script>";
exit;
if($username=='' || $password==''){
echo "<script>alert('請輸入賬號和密碼!')</script>";
exit;
}
$sql="select * from users where username='$username' and passcode='$password'";
$query=mysqli_query($con,$sql) or die('SQL語句執行失敗'.mysqli_error($con));
if ($row=mysqli_fetch_array($query)){
session_start();
$_SESSION['username']=$row[1];
echo "<a href='welcome.php'>歡迎訪問</a>";
}else{
echo "<script>alert('登入失敗!');history.go(-1)</script>";
}
mysqli_close($con);
}
?>
使用PHP轉義函式:
-
addslashes()函式:
能夠將單引號、雙引號、反斜槓和null轉義 -
mysql_escape_string()函式、mysql_real_escape_string()函式
這個是轉義SQL語句中的符號,php7.x版本的都要變成mysqli
$username=isset($_POST['username'])?addslashes($_POST['username']):'';
$password=isset($_POST['password'])?mysqli_real_escape_string($con,$_POST['password']):'';
轉義函式的弊端
因為使用的是UTF-8編碼,不是寬位元組編碼,形成的'會被變成%5c%27
Windows下預設的是寬位元組的gbk編碼
如果在%5c前面加上一個字元形成一個複雜的漢字,那麼單引號仍然會被輸出
MySQLi 引數化查詢
在使用引數化查詢的情況下,伺服器不會將引數的內容是為SQL指令中的一部分
而是在資料庫完成SQL指令的編譯之後,再代入引數執行
此時就算引數裡面有惡意資料
但是此時SQL語句以及編譯完成
就不會被資料庫執行
PHP提供了三種訪問mysql資料庫的擴充:
- MySQL (PHP5.5起,已經廢除)
- MySQLi
- PDO(PHP Data Object PHP資料物件)
PDO和MySQLi提供物件導向的api
MySQLi也存在程式導向的api,所以容易從MySQL轉換到MySQLi
下面是mysqli形式的check_login.php 寫法,新建check_login_mysqli.php
<?php
include('con_database.php');
$username=isset($_POST['username'])?$_POST['username']:'';
$password=isset($_POST['password'])?$_POST['password']:'';
if($username==''||$password==''){
echo "<script>alert('錯誤!');history.go(-1);</script>";
exit;
}
$sql="select * from users where username=? and passcode=? ;";//問號表示需要一個引數
$stmt=$con->prepare($sql);//預編譯SQL語句
if(!$stmt){
echo 'prepare 執行錯誤';
}
else{
$stmt->bind_param("ss",$username,$password); //為預編譯繫結SQL引數,ss表示兩個字串
//i——int d——double s——string b——boolean
$stmt->execute();
$result=$stmt->get_result();
$row=$result->fetch_row();
if($row){
session_start();
$_SESSION['username']=$row[1];
echo $row[1]."<a href='welcome.php'>歡迎訪問</a>";
}else{
echo "<script>alert('登入失敗!!');history.go(-1);</script>";
}
$stmt->close();
}
$con->close();
?>
一些內容已經標記在程式碼的註釋裡面
引數化的PHP程式碼真的能夠很有效地防止SQL隱碼攻擊。