• Natas27 Writeup(mysql溢出截断漏洞)


    Natas27:

    一个登录节界面,查看源码。

    <html>
    <head>
    <!-- This stuff in the header has nothing to do with the level -->
    <link rel="stylesheet" type="text/css" href="http://natas.labs.overthewire.org/css/level.css">
    <link rel="stylesheet" href="http://natas.labs.overthewire.org/css/jquery-ui.css" />
    <link rel="stylesheet" href="http://natas.labs.overthewire.org/css/wechall.css" />
    <script src="http://natas.labs.overthewire.org/js/jquery-1.9.1.js"></script>
    <script src="http://natas.labs.overthewire.org/js/jquery-ui.js"></script>
    <script src=http://natas.labs.overthewire.org/js/wechall-data.js></script><script src="http://natas.labs.overthewire.org/js/wechall.js"></script>
    <script>var wechallinfo = { "level": "natas27", "pass": "<censored>" };</script></head>
    <body>
    <h1>natas27</h1>
    <div id="content">
    <?
    
    // morla / 10111
    // database gets cleared every 5 min 
    
    
    /*
    CREATE TABLE `users` (
      `username` varchar(64) DEFAULT NULL,
      `password` varchar(64) DEFAULT NULL        //这是重点,用户名和密码均不超过64个字节
    );
    */
    
    
    function checkCredentials($link,$usr,$pass){
     
        //转义用户名和密码中的特殊字符,防止sql注入
        $user=mysql_real_escape_string($usr);
        $password=mysql_real_escape_string($pass);
        
        $query = "SELECT username from users where username='$user' and password='$password' ";
        $res = mysql_query($query, $link);
        if(mysql_num_rows($res) > 0){    //若查询出来的数组大于0,则验证用户名/密码成功
            return True;
        }
        return False;
    }
    
    
    function validUser($link,$usr){
        
        $user=mysql_real_escape_string($usr);
        
        $query = "SELECT * from users where username='$user'";
        $res = mysql_query($query, $link);
        if($res) {
            if(mysql_num_rows($res) > 0) {
                return True;
            }
        }
        return False;
    }
    
    
    function dumpData($link,$usr){
        
        $user=mysql_real_escape_string($usr);
        
        $query = "SELECT * from users where username='$user'";
        $res = mysql_query($query, $link);
        if($res) {
            if(mysql_num_rows($res) > 0) {
                while ($row = mysql_fetch_assoc($res)) { //mysqli_fetch_assoc() 函数从结果集中取得一行作为关联数组。
                    // thanks to Gobo for reporting this bug!  
                    //return print_r($row);
                    return print_r($row,true);
                }
            }
        }
        return False;
    }
    
    
    function createUser($link, $usr, $pass){
    
        $user=mysql_real_escape_string($usr);
        $password=mysql_real_escape_string($pass);
        
        $query = "INSERT INTO users (username,password) values ('$user','$password')";
        $res = mysql_query($query, $link);
        if(mysql_affected_rows() > 0){ //mysqli_affected_rows() 函数返回前一次 MySQL 操作所影响的记录行数
            return True;
        }
        return False;
    }
    
    //逻辑:查询username是否存在
    if(array_key_exists("username", $_REQUEST) and array_key_exists("password", $_REQUEST)) {
        $link = mysql_connect('localhost', 'natas27', '<censored>');
        mysql_select_db('natas27', $link); //mysql_select_db() 函数设置活动的 MySQL 数据库。
       
    
        if(validUser($link,$_REQUEST["username"])) {
            //user exists, check creds
            if(checkCredentials($link,$_REQUEST["username"],$_REQUEST["password"])){
                echo "Welcome " . htmlentities($_REQUEST["username"]) . "!<br>";    //htmlentities() 函数把字符转换为 HTML 实体。
                echo "Here is your data:<br>";
                $data=dumpData($link,$_REQUEST["username"]);
                print htmlentities($data);
            }
            else{
                echo "Wrong password for user: " . htmlentities($_REQUEST["username"]) . "<br>";
            }        
        } 
        else {
            //user doesn't exist
            if(createUser($link,$_REQUEST["username"],$_REQUEST["password"])){ 
                echo "User " . htmlentities($_REQUEST["username"]) . " was created!";
            }
        }
    
        mysql_close($link);
    } else {
    ?>
    
    <form action="index.php" method="POST">
    Username: <input name="username"><br>
    Password: <input name="password" type="password"><br>
    <input type="submit" value="login" />
    </form>
    <? } ?>
    <div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
    </div>
    </body>
    </html>
    natas28-sourcecode.html

    查看源码,本来想通过sql注入来获取密码,但是很难实现:
    一是源码通过mysql-real-escape-string()函数对输入的用户名和密码中的特殊字符进行了转义,以防止sql注入;
    二是想要绕过的话也很困难:$password使用''括起来了,无法用like wildchar和number绕过。
    (参见https://www.sqlinjection.net/advanced/php/mysql-real-escape-string/)

    继续分析,总结流程如下:
    Receive Input -> Check if user exist -> if exist check credentials -> show data.
    Receive Input -> check if user exist -> if dosen't -> create user.
    即后台获取到用户输入的用户名/密码后,首先查询此用户名是否存在,如果存在,验证密码并显示数据;如果不存在,创建用户。

    同时,验证用户名/密码是否正确也仅仅是看返回数组是否>0。

    很容易联想到,如果我们插入一个和目标账户相同的行,即使我们不知道密码,checkCredentials()函数查找的结果数组也会>0并返回true,接着dumpData()函数就可能回显真正的用户与密码了。(注意这里只会回显一行,虽然dumpData()里面是个while循环,但里层直接return print_r($row,true)了)

    插入数据需实现两点:
    一是validUser()函数查找不到用户;
    二是存储后的username要和目标username相同,密码自己定。

    那么如何实现插入的用户名既和目标username相同,又使validUser()函数查找不到呢?

    这里还要参考两个mysql里面的知识点 :

    一是字符串存储时若发生“溢出”,mysql会自动truncate到最大宽度;
    二是空格在varchar里面会被自动删除。

    所以,正确的插入为“natas28+超过64字节的连续空格+xxx”(注意,后面的xxx是必须的,因为在mysql中'natas28'='natas28+空格'),密码随意,可以为空。

    比较username时mysql并不会对提交的username进行truncate,所以判断用户名不存在,开始新建用户名和密码。一旦开始存储,就会发生溢出/截取,导致出现两个username同为‘natas28’的行。接着返回登录界面,输入natas28+密码。找寻操作会返回刚刚插入的数组(>0),所以查询成功,回显用户名和密码,这时回显的就是第一个nata28那行,即我们要获得的flag。

    首先输入
    用户名:natas28                                                             xxx
    密码:aaa
    返回如下:

    接着返回登录页面,重新输入
    用户名:natas28
    密码:aaa
    返回如下:

    flag:JWwR438wkgTsNKBbcJoowyysdM82YjeF

    另外,网友还给出了一个利用开头“5分钟重置”来进行注入的例子,思路是一样的,实现方法不一样:

    源码中提示了每五分钟将会清除一次数据
    这个清除的过程应该是全部清空->写入username=natas28,password=xxx记录
    如果卡在全部清空和写入之间抢先提交username=natas28,password=”的记录,那么由于提交时没有检测到natas28,将会写入我们的记录;等服务器重新写入本来的natas28,再检索就也能显示出来了

    import requests
    data={'username':'natas28','password':''}
    auth=requests.auth.HTTPBasicAuth('natas27','55TBjpPZUUJgVP5b3BnbG6ON9uDPVzCJ')
    while 1:
        re=requests.post("http://natas27.natas.labs.overthewire.org",auth=auth,data=data)
        if 'Wrong password' not in re.text:
            print(re.text)
            break

    用脚本不断发起请求有几率正好插入其中,但是数据库重置的时间很难把握,我们很难实现恰好在数据库重置的那个时刻将我们自己的用户名/密码写入数据库。如下图所示,程序跑了1.5小时,共发送了1311次请求,始终没有跑出结果。(也可能是我网速过慢的原因,如果是在内网跑,估计成功的可能性大一点)

    参考:

    https://www.cnblogs.com/liqiuhao/p/6906474.html
    https://blog.csdn.net/baidu_35297930/article/details/99732206?utm_source=distribute.pc_relevant.none-task
    https://blog.csdn.net/whklhhhh/article/details/77484274?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
    https://www.sqlinjection.net/advanced/php/mysql-real-escape-string/
    https://blog.csdn.net/lyover/article/details/50224821

  • 相关阅读:
    Java自带工具jstack故障分析的一个案例
    当spring 容器初始化完成后执行某个方法
    tomcat的maxThreads、acceptCount(最大线程数、最大排队数)
    RESTful API 设计指南
    GitHub简单使用入门
    Newton's method
    把一个文件夹下的多个excel文件合并到同一个excel的一个sheet里
    multimap的使用 in C++,同一个关键码存在多个值
    pandas把多个sheet读进一个DataFrame
    数据预处理之Minkowski距离计算
  • 原文地址:https://www.cnblogs.com/zhengna/p/12346481.html
走看看 - 开发者的网上家园