1.原理知識:
二階SQL注入攻擊(Second-Order SQL Injection)原理詳解
一、基本概念
二階注入是一種"存儲型"SQL注入,攻擊流程分為兩個階段:
- ??首次輸入??:攻擊者將惡意SQL片段存入數據庫
- ??二次調用??:當應用程序使用存儲的數據構造SQL查詢時觸發攻擊
二、典型攻擊場景(以密碼更新為例)
三、具體攻擊步驟分解
-
??初始注入階段??
-- 攻擊者注冊特殊用戶名 INSERT INTO users(username) VALUES ('admin''-- ')
存儲結果:
id username 1 admin'-- -
??觸發階段(密碼更新操作)??
// 應用代碼示例(存在漏洞) $newPassword = $_POST['password']; $username = $_SESSION['username']; // 從會話獲取"admin'--"$sql = "UPDATE users SET password='$newPassword' WHERE username='$username'";
實際執行SQL:
UPDATE users SET password='hacked' WHERE username='admin'-- '
??效果??:
--
注釋掉了后續條件- 所有用戶的密碼被更新為
hacked
四、與普通注入的關鍵區別
特性 | 普通SQL注入 | 二階注入 |
---|---|---|
觸發時機 | 即時執行 | 延遲執行(數據被使用時) |
輸入點 | 直接拼接的輸入參數 | 存儲在數據庫中的數據 |
防御難度 | 較易檢測 | 更難發現 |
五、防御方案
1. 數據存儲階段
// 存入前轉義+類型檢查
$username = $pdo->quote($_POST['username']);
// 或使用預處理
$stmt = $pdo->prepare("INSERT INTO users(username) VALUES (?)");
$stmt->execute([$_POST['username']]);
2. 數據使用階段
// 永遠使用參數化查詢(即使數據來自數據庫)
$stmt = $pdo->prepare("UPDATE users SET password=? WHERE username=?");
$stmt->execute([$newPassword, $dbStoredUsername]);
3. 深度防御措施
- ??數據凈化??:在讀取數據庫后仍進行過濾
function sanitizeDbValue($value) {return preg_replace('/[^\w@.-]/', '', $value); }
- ??權限分離??:
CREATE USER 'app_update'@'localhost' IDENTIFIED BY 'pass'; GRANT UPDATE(password) ON db.users TO 'app_update'@'localhost'; -- 無權限修改其他字段
4. 安全審計建議
-- 監控非常規模式
CREATE TRIGGER audit_password_changes
AFTER UPDATE ON users
FOR EACH ROW
INSERT INTO audit_log VALUES(NEW.id, '密碼變更', NOW());
六、實際案例檢測
檢測代碼是否脆弱:
// 危險代碼模式(可被二階注入利用)
function updatePassword($conn, $newPass){$user = getUserFromDB(); // 從數據庫獲取用戶名$conn->query("UPDATE users SET pass='$newPass' WHERE user='{$user['name']}'");
}// 安全版本
function safeUpdatePassword($pdo, $newPass){$user = getUserFromDB();$stmt = $pdo->prepare("UPDATE users SET pass=? WHERE user=?");$stmt->execute([$newPass, $user['name']]);
}
七、進階防護:ORM層防護
// 使用Laravel Eloquent示例
User::where('name', $request->username)->update(['password' => bcrypt($request->password)]);
// 自動參數化,無需手動處理
關鍵原則:??所有來自外部和內部的數據在拼接SQL時都應視為不可信的??,必須通過參數化查詢處理。
2.更新密碼的二階注入攻擊關系解釋
用戶注冊的特殊賬號會再更新密碼的過程中引起二階注入攻擊。
更新密碼場景下的二階SQL注入攻擊原理詳解
一、攻擊流程全貌(以密碼更新為例)
二、關鍵漏洞形成原理
-
??第一次存儲時的"無害"數據??
-- 注冊時(使用了參數化查詢,此時安全) INSERT INTO users(username) VALUES('admin''--')
數據庫實際存儲:
id username 1 admin'-- -
??密碼更新時的危險拼接??
// 漏洞代碼示例 $username = getUsernameFromDB(); // 獲取存儲的"admin'--" $newPass = $_POST['password'];// 直接拼接SQL(未使用參數化) $sql = "UPDATE users SET password='$newPass' WHERE username='$username'";
生成的惡意SQL:
UPDATE users SET password='hacked' WHERE username='admin'-- '
-
??注入效果??
--
注釋掉了原SQL的條件部分- 語句等效于:
UPDATE users SET password='hacked'
- ??所有用戶的密碼被修改??
三、與傳統注入的核心區別
特性 | 傳統注入 | 二階注入 |
---|---|---|
輸入點 | 直接來自HTTP請求 | 來自數據庫存儲的數據 |
觸發條件 | 立即執行 | 需要特定業務邏輯觸發 |
防御盲區 | 輸入過濾即可防御 | 需要全程參數化 |
檢測難度 | 容易被掃描器發現 | 需要業務流分析 |
四、完整攻擊實例分析
-
??攻擊準備階段??
POST /register HTTP/1.1 username=admin'--&password=123
- 后端安全地存儲了該用戶名(此時無風險)
-
??觸發攻擊階段??
POST /update-password HTTP/1.1 new_password=hacked
- 后端操作流程:
// 從數據庫獲取當前用戶名 $user = $db->query("SELECT username FROM users WHERE id = {$_SESSION['id']}")->fetch();// 危險操作:拼接SQL $db->exec("UPDATE users SET password='hacked' WHERE username='{$user['username']}'");
- 后端操作流程:
-
??實際執行的惡意SQL??
UPDATE users SET password='hacked' WHERE username='admin'-- '
五、針對性防御方案
1. 存儲階段防御
// 注冊時嚴格過濾
function validateUsername($name) {return preg_match('/^[a-z0-9_]{3,20}$/i', $name);
}// 使用預處理語句存儲
$stmt = $pdo->prepare("INSERT INTO users(username) VALUES(?)");
$stmt->execute([$_POST['username']]);
2. 使用階段防御
// 密碼更新操作(安全版本)
function updatePassword($userId, $newPass) {$pdo = new PDO(...);// 步驟1:獲取用戶名(使用參數化)$stmt = $pdo->prepare("SELECT username FROM users WHERE id = ?");$stmt->execute([$userId]);$user = $stmt->fetch();// 步驟2:更新密碼(再次參數化)$stmt = $pdo->prepare("UPDATE users SET password = ? WHERE username = ?");$stmt->execute([password_hash($newPass, PASSWORD_BCRYPT), $user['username']]);
}
3. 深度防御措施
-
??數據庫權限隔離??
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'pass'; GRANT SELECT, UPDATE(password) ON db.users TO 'app_user'@'localhost'; -- 禁止修改username等其他字段
-
??審計日志??
CREATE TRIGGER password_change_audit AFTER UPDATE ON users FOR EACH ROW INSERT INTO security_log VALUES(USER(), CONCAT('Password changed for ', OLD.username),NOW() );
六、漏洞檢測方法
-
??代碼審計關鍵點??
- 查找所有從數據庫讀取數據后直接拼接SQL的地方
- 特別關注:用戶資料更新、密碼重置、權限變更等敏感操作
-
??自動化測試腳本??
# 二階注入測試腳本示例
def test_second_order_injection():# 注冊測試賬號register("test'--", "123456")# 修改密碼resp = update_password("new_password")# 驗證是否所有用戶密碼被修改admin_pass = get_password("admin")assert admin_pass == "new_password" # 如果成立說明漏洞存在
七、現代框架的最佳實踐
// Laravel示例(自動防護二階注入)
User::where('username', $request->username)->update(['password' => bcrypt($request->new_password)]);
// Eloquent ORM自動使用參數化查詢// 即使這樣也是安全的:
$user = User::find($id);
DB::update("UPDATE users SET password = ? WHERE username = ?", [$newPass, $user->username]);
關鍵安全原則:??無論數據來自HTTP請求還是數據庫,在拼接SQL時永遠使用參數化查詢??。這種防御方式能同時防護一階和二階SQL注入攻擊。
3.建立具有密碼更新功能的網站
前端html頁面+后端php頁面
3.1用戶注冊頁面(html)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Register</title>
<style>#a{ width: 300px; text-align: right; }.b{width: 150px;height:20px;}
</style>
</head>
<body><div id=a><form name="form_register" method="post" action="check_register.php">Username: <input type="text" class=b name="username" /> <br>Psssword: <input type="password" class=b name="passwd" /> <br><input type="submit" name="Submit" value="Submit" /> <input type="reset" name="Reset" value="Reset" /></form></div>
</body>
</html>
3.1實現注冊功能(php)
<?php
//包含數據庫連接
include('con_database.php');//獲取輸入的信息
$username = isset($_POST['username']) ? mysqli_escape_string($con,$_POST['username']) : '';
$passwd = isset($_POST['passwd']) ? mysqli_escape_string($con, $_POST['passwd']) : '';
if($username == '' || $passwd == '' )
{ echo "<script>alert('信息不完整!'); history.go(-1);</script>"; exit;
}
//執行數據庫查詢,判斷用戶是否已經存在
$sql="select * from users where username = '$username' ";$query = mysqli_query($con,$sql)
or die('SQL語句執行失敗, : '.mysqli_error($con));$num = mysqli_fetch_array($query); //統計執行結果影響的行數
if($num) //如果已經存在該用戶
{ echo "<script>alert('用戶名已存在!'); history.go(-1);</script>"; exit;
} $sql = "insert into users (username,passcode) values('$username','$passwd')"; mysqli_query($con, $sql)
or die('注冊失敗, : '.mysqli_error($con));echo "注冊成功,請<a href='login.html'>登錄</a>";mysqli_close($con);
?>
3.3登錄驗證(php)
<?php
//包含數據庫連接
include('con_database.php');
//獲取輸入的信息
$username = isset($_POST['username']) ? mysqli_escape_string($con,$_POST['username']) : '';
$passwd = isset($_POST['passwd']) ? mysqli_escape_string($con, $_POST['passwd']) : '';
if($username == '' || $passwd == '' )
{ echo "<script>alert('請輸入用戶名和密碼!'); history.go(-1);</script>"; exit;
}//從數據庫查詢
$sql = "select * from users where username = '$username' and passcode = '$passwd' ";
$res = mysqli_query($con,$sql) or die('SQL語句執行失敗, : '.mysqli_error($con));
$row = mysqli_fetch_row($res);if ($row[0])
{session_start();$_SESSION['username'] = $row[1];echo $row[1].'歡迎訪問!';echo "<br>";echo "<a href='updatepasswd.html'>修改密碼</a>";
}
else
{echo "<script>alert('用戶名或密碼錯誤!'); history.go(-1);</script>";
}
mysqli_close($con);
?>
3.4更新密碼 (html)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>UpdatePassword</title>
<style>#a{ width: 300px; text-align: right; }.b{width: 150px;height:20px;}
</style>
</head>
<body><div id=a><form name="form_register" method="post" action="updatepasswd_mysqli.php">當前密碼: <input type="text" class=b name="current_passwd" /> <br>新密碼: <input type="password" class=b name="passwd" /> <br><input type="submit" name="Submit" value="Submit" /> <input type="reset" name="Reset" value="Reset" /></form></div>
</body>
</html>
3.5更新密碼功能(php)
<?php
session_start();
if(!isset($_SESSION['username']))
{//重新定位到注冊頁面header('Location: register.html');
}
if (isset($_POST['Submit'])){//包含數據庫連接include("con_database.php");// $username = $_SESSION['username'];$username = mysqli_real_escape_string($con,$_SESSION['username']);$curr_pass= mysqli_real_escape_string($con,$_POST['current_passwd']);$pass= mysqli_real_escape_string($con,$_POST['passwd']);$sql = "UPDATE users SET passcode = '$pass' WHERE username = '$username' and passcode = '$curr_pass' ";$res = mysqli_query($con,$sql) or die('SQL執行失敗 :'.mysqli_error($con));$row = mysqli_affected_rows($con);if($row != 0){echo "<script>alert('密碼更改成功!'); history.go(-1);</script>"; }else{echo "<script>alert('當前密碼錯誤!'); history.go(-1);</script>"; }
}
mysqli_close($con);
?>
4.用戶使用功能測試
檢查apache打開
報錯,原因是我是雙擊打開的html,應該用http協議打開
注冊之后需要用到登錄頁面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Login</title>
<style>#a{ width: 300px; text-align: right;}.b{width: 150px;height:20px;}
</style>
</head>
<body><div id=a><form name="form_login" method="post" action="check_login.php">Username: <input type="text" class=b name="username" /> <br>Psssword: <input type="password" class=b name="passwd" /> <br><input type="submit" name="Submit" value="Submit" /> <input type="reset" name="Reset" value="Reset" /></form></div>
</body>
</html>
修改密碼,用到updatepasword